import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import type { Completion } from '../api' import { exportFileName, toDecklistText } from '../lib/deck' interface ExportOption { label: string hint: string run: (completion: Completion) => Promise // feedback text for the button, if any } function download(text: string, fileName: string, type: string) { const url = URL.createObjectURL(new Blob([text], { type })) const a = document.createElement('a') a.href = url a.download = fileName a.click() URL.revokeObjectURL(url) } // Copies, or downloads the same text if the browser blocks the clipboard. async function copyOrDownload(text: string, completion: Completion): Promise { try { await navigator.clipboard.writeText(text) return 'Copied!' } catch { download(text, exportFileName(completion, 'txt'), 'text/plain') return 'Downloaded' } } const OPTIONS: ExportOption[] = [ { label: 'Copy decklist', hint: 'Plain list for Moxfield, Archidekt, and most sites', run: (c) => copyOrDownload(toDecklistText(c), c), }, { label: 'Download .txt', hint: 'Plain list as a file', run: async (c) => { download(toDecklistText(c), exportFileName(c, 'txt'), 'text/plain') return null }, }, ] // Menu button (WAI-ARIA pattern): arrow keys / Home / End move between // items, Enter or click runs one, Escape or clicking outside closes. export function ExportMenu({ completion }: { completion: Completion }) { const id = useId() const [open, setOpen] = useState(false) const [feedback, setFeedback] = useState(null) const buttonRef = useRef(null) const menuRef = useRef(null) const itemRefs = useRef<(HTMLButtonElement | null)[]>([]) // The menu hangs from the button's right edge; on a narrow screen that can // push it off the left side, so nudge it back inside the window (8px margin) // before it paints. useLayoutEffect(() => { const menu = menuRef.current if (!open || !menu) return menu.style.transform = '' const rect = menu.getBoundingClientRect() const margin = 8 const shift = rect.left < margin ? margin - rect.left : Math.min(0, window.innerWidth - margin - rect.right) if (shift) menu.style.transform = `translateX(${shift}px)` }, [open]) useEffect(() => { if (!open) return itemRefs.current[0]?.focus() const onPointerDown = (e: PointerEvent) => { const target = e.target as Node if (!menuRef.current?.contains(target) && !buttonRef.current?.contains(target)) setOpen(false) } document.addEventListener('pointerdown', onPointerDown) return () => document.removeEventListener('pointerdown', onPointerDown) }, [open]) useEffect(() => { if (!feedback) return const timer = setTimeout(() => setFeedback(null), 1800) return () => clearTimeout(timer) }, [feedback]) const close = (refocus: boolean) => { setOpen(false) if (refocus) buttonRef.current?.focus() } const choose = async (option: ExportOption) => { close(true) setFeedback(await option.run(completion)) } const onMenuKeyDown = (e: React.KeyboardEvent) => { const items = itemRefs.current.filter((el): el is HTMLButtonElement => el !== null) const index = items.indexOf(document.activeElement as HTMLButtonElement) const focusAt = (i: number) => items[(i + items.length) % items.length]?.focus() if (e.key === 'ArrowDown') focusAt(index + 1) else if (e.key === 'ArrowUp') focusAt(index - 1) else if (e.key === 'Home') focusAt(0) else if (e.key === 'End') focusAt(items.length - 1) else if (e.key === 'Escape') close(true) else if (e.key === 'Tab') close(false) else return if (e.key !== 'Tab') e.preventDefault() } return (
{open && ( )} {feedback ?? ''}
) }