import React, { useState, useRef, useEffect, useCallback } from "react"; import DownloadButton from "./DownloadButton"; export default function DownloadDropdown({ options, title, author }) { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); const triggerRef = useRef(null); const menuRef = useRef(null); const close = useCallback((returnFocus) => { setIsOpen(false); if (returnFocus) triggerRef.current?.focus(); }, []); // Close when a click or focus lands outside the dropdown, so the menu cannot // be left hanging open after tabbing or clicking away. useEffect(() => { if (!isOpen) return; const closeIfOutside = (event) => { if (!containerRef.current?.contains(event.target)) setIsOpen(false); }; document.addEventListener("pointerdown", closeIfOutside); document.addEventListener("focusin", closeIfOutside); return () => { document.removeEventListener("pointerdown", closeIfOutside); document.removeEventListener("focusin", closeIfOutside); }; }, [isOpen]); const menuItems = () => Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') || []); const focusItem = (index) => { const items = menuItems(); if (!items.length) return; items[(index + items.length) % items.length].focus(); }; const handleTriggerKeyDown = (event) => { if (event.key === "ArrowUp" || event.key === "ArrowDown") { event.preventDefault(); setIsOpen(true); // The menu opens upward, so ArrowUp enters it at the item nearest the button. const target = event.key === "ArrowUp" ? -1 : 0; requestAnimationFrame(() => focusItem(target)); } else if (event.key === "Escape" && isOpen) { event.preventDefault(); close(false); } }; const handleMenuKeyDown = (event) => { const items = menuItems(); const current = items.indexOf(document.activeElement); switch (event.key) { case "ArrowDown": event.preventDefault(); focusItem(current + 1); break; case "ArrowUp": event.preventDefault(); focusItem(current - 1); break; case "Home": event.preventDefault(); focusItem(0); break; case "End": event.preventDefault(); focusItem(-1); break; case "Escape": event.preventDefault(); close(true); break; case "Tab": // Let focus move on naturally, but do not leave the menu open behind it. setIsOpen(false); break; default: break; } }; return (
setIsOpen(true)} onMouseLeave={() => setIsOpen(false)} > {isOpen && ( /* * The gap above the trigger is padding on this wrapper, not a margin on * the panel, so it stays inside the hover area. With a margin the cursor * crossed dead space on its way to the menu, which fired mouseleave and * closed it before it could be reached. */
setIsOpen(true)} onMouseLeave={() => setIsOpen(false)} >
{/* Arrow pointing down */}
{options.map((opt) => ( close(true)} /> ))}
)}
); }