import { useEffect, useRef, useState } from 'react' import { NavLink, useLocation } from 'react-router-dom' // One dropdown section in the public header โ a menu an admin created from // Admin โ Navigation (THEMING_AND_NAV.md ยง7, Phase 10). // // It **opens on click, never on hover**. Hover menus are unusable on touch, and // the alternative (make the trigger a link too) means tapping to open navigates // away instead. A section is a container, not a destination, so the trigger has // no `to` at all. // // Everything else here is the keyboard and dismissal contract a menu needs: // Escape closes and returns focus to the trigger, an outside press closes, // navigating closes, and Arrow Up/Down walk the items. `aria-haspopup` + // `aria-expanded` are what let a screen reader announce it as a menu rather than // as a button that mysteriously changes the page. export default function NavDropdown({ label, items, linkStyle }) { const [open, setOpen] = useState(false) const wrapRef = useRef(null) const triggerRef = useRef(null) const location = useLocation() // The trigger shows the active treatment when the page you are on lives in // this menu โ otherwise entering a section makes the header look like nothing // is selected. const holdsActive = items.some((i) => (i.end ? location.pathname === i.to : location.pathname.startsWith(i.to))) // Close on navigation. The menu is rendered inside a sticky header that // survives route changes, so nothing else would dismiss it. useEffect(() => setOpen(false), [location.pathname]) useEffect(() => { if (!open) return undefined const onKey = (e) => { if (e.key !== 'Escape') return setOpen(false) triggerRef.current?.focus() } // `mousedown`, not `click`: closing on the press means a press that lands on // another trigger opens that one in the same gesture. const onOutside = (e) => { if (!wrapRef.current?.contains(e.target)) setOpen(false) } document.addEventListener('keydown', onKey) document.addEventListener('mousedown', onOutside) return () => { document.removeEventListener('keydown', onKey) document.removeEventListener('mousedown', onOutside) } }, [open]) // Roving focus with the arrow keys, wrapping at both ends. const onMenuKeyDown = (e) => { if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return e.preventDefault() const links = [...(wrapRef.current?.querySelectorAll('[data-menu-item]') || [])] if (links.length === 0) return const at = links.indexOf(document.activeElement) const next = e.key === 'ArrowDown' ? (at + 1) % links.length : (at - 1 + links.length) % links.length links[at === -1 ? 0 : next].focus() } return (