import React, { createContext, useContext, useState, useRef, useEffect } from 'react' import { useClickOutside } from '../../core/hooks/useInteractions' interface MenuContextType { isOpen: boolean setIsOpen: (open: boolean) => void buttonRef: React.RefObject itemsRef: React.RefObject } const MenuContext = createContext(null) export interface MenuProps { children: React.ReactNode className?: string } export const Menu: React.FC = ({ children, className = '' }) => { const [isOpen, setIsOpen] = useState(false) const menuRef = useClickOutside(() => setIsOpen(false)) const buttonRef = useRef(null); const itemsRef = useRef(null); const contextValue = { isOpen, setIsOpen, buttonRef, itemsRef, } return (
{children}
) } export interface MenuButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode } export const MenuButton: React.FC = ({ children, className = '', ...props }) => { const context = useContext(MenuContext) if (!context) throw new Error('MenuButton must be used within a Menu') const { isOpen, setIsOpen } = context; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown' || e.key === 'Enter') { e.preventDefault(); setIsOpen(true); } }; return ( ) } export interface MenuItemsProps { children: React.ReactNode className?: string } export const MenuItems: React.FC = ({ children, className = '' }) => { const context = useContext(MenuContext) if (!context) throw new Error('MenuItems must be used within a Menu') const { isOpen, setIsOpen, buttonRef, itemsRef } = context // Set focus on the first item when the menu opens useEffect(() => { if (isOpen && itemsRef.current) { const items = Array.from(itemsRef.current.children) as HTMLElement[]; const firstFocusableItem = items.find( item => item.getAttribute('role') === 'menuitem' && item.getAttribute('disabled') === null ); // Use timeout to ensure focus is set after render and state updates. setTimeout(() => firstFocusableItem?.focus(), 0); } }, [isOpen]); const handleKeyDown = (e: React.KeyboardEvent) => { if (!itemsRef.current) return; const items = Array.from(itemsRef.current.children) as HTMLElement[]; const focusableItems = items.filter( item => item.getAttribute('role') === 'menuitem' && item.getAttribute('disabled') === null ); if (focusableItems.length === 0) return; if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const currentFocusIndex = focusableItems.findIndex(item => item === document.activeElement); let nextIndex; if (e.key === 'ArrowDown') { nextIndex = currentFocusIndex >= 0 ? (currentFocusIndex + 1) % focusableItems.length : 0; } else { // ArrowUp nextIndex = currentFocusIndex > 0 ? (currentFocusIndex - 1) : focusableItems.length - 1; } focusableItems[nextIndex]?.focus(); } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (document.activeElement && focusableItems.includes(document.activeElement as HTMLElement)) { (document.activeElement as HTMLElement).click(); } } else if (e.key === 'Escape') { e.preventDefault(); setIsOpen(false); buttonRef.current?.focus(); } } if (!isOpen) return null return (
{children}
) } export interface MenuItemProps extends React.ButtonHTMLAttributes { children: React.ReactNode } export const MenuItem: React.FC = ({ children, className = '', onClick, ...props }) => { const context = useContext(MenuContext) if (!context) throw new Error('MenuItem must be used within a Menu') const { setIsOpen } = context const handleClick = (e: React.MouseEvent) => { onClick?.(e) setIsOpen(false) } return ( ) }