import { type ReactNode, useEffect, useRef, useState, type ComponentProps, type ComponentType, type SVGProps, type CSSProperties, } from 'react'; import { Button } from './Button'; import { Text } from './Text'; import { theme } from './theme'; import { Toggle } from './Toggle'; import { View } from './View'; const MenuLine: unique symbol = Symbol('menu-line'); const MenuLabel: unique symbol = Symbol('menu-label'); Menu.line = MenuLine; Menu.label = MenuLabel; type KeybindingProps = { keyName: ReactNode; }; function Keybinding({ keyName }: KeybindingProps) { return ( {keyName} ); } export type MenuItemObject = { type?: Type; name: NameType; disabled?: boolean; icon?: ComponentType>; iconSize?: number; text: string; key?: string; toggle?: boolean; tooltip?: string; }; export type MenuItem = | MenuItemObject | MenuItemObject | typeof Menu.line; function isLabel( item: MenuItemObject | MenuItemObject, ): item is MenuItemObject { return item.type === Menu.label; } type MenuProps = { header?: ReactNode; footer?: ReactNode; items: Array>; onMenuSelect?: (itemName: NameType) => void; style?: CSSProperties; className?: string; getItemStyle?: (item: MenuItemObject) => CSSProperties; slot?: ComponentProps['slot']; }; export function Menu({ header, footer, items: allItems, onMenuSelect, style, className, getItemStyle, slot, }: MenuProps) { const elRef = useRef(null); const items = allItems.filter(x => x); const [hoveredIndex, setHoveredIndex] = useState(null); useEffect(() => { const el = elRef.current; el?.focus(); const onKeyDown = (e: KeyboardEvent) => { const filteredItems = items.filter( item => item && item !== Menu.line && item.type !== Menu.label, ); const currentIndex = filteredItems.indexOf(items[hoveredIndex || 0]); const transformIndex = (idx: number) => items.indexOf(filteredItems[idx]); switch (e.key) { case 'ArrowUp': e.preventDefault(); setHoveredIndex( hoveredIndex === null ? 0 : transformIndex(Math.max(currentIndex - 1, 0)), ); break; case 'ArrowDown': e.preventDefault(); setHoveredIndex( hoveredIndex === null ? 0 : transformIndex( Math.min(currentIndex + 1, filteredItems.length - 1), ), ); break; case 'Enter': e.preventDefault(); const item = items[hoveredIndex || 0]; if (hoveredIndex !== null && item !== Menu.line && !isLabel(item)) { onMenuSelect?.(item.name); } break; default: } }; el?.addEventListener('keydown', onKeyDown); return () => { el?.removeEventListener('keydown', onKeyDown); }; }, [hoveredIndex]); return ( {header} {items.map((item, idx) => { if (item === Menu.line) { return ( ); } else if (isLabel(item)) { return ( {item.name} ); } const Icon = item.icon; return ( ); })} {footer} ); }