import { autoUpdate, flip, FloatingFocusManager, FloatingList, FloatingNode, FloatingPortal, FloatingTree, offset, safePolygon, shift, useClick, useDismiss, useFloating, useFloatingNodeId, useFloatingParentNodeId, useFloatingTree, useHover, useInteractions, useListItem, useListNavigation, useMergeRefs, useRole, useTypeahead, } from "@floating-ui/react"; import * as React from "react"; import { Icon } from "@nice/iconer"; const MenuContext = React.createContext<{ getItemProps: ( userProps?: React.HTMLProps ) => Record; activeIndex: number | null; setActiveIndex: React.Dispatch>; setHasFocusInside: React.Dispatch>; isOpen: boolean; }>({ getItemProps: () => ({}), activeIndex: null, setActiveIndex: () => {}, setHasFocusInside: () => {}, isOpen: false, }); interface MenuProps { label?: string; node?: React.ReactNode; nested?: boolean; icon?: React.ReactNode; children?: React.ReactNode; } export const MenuComponent = React.forwardRef< HTMLButtonElement, MenuProps & React.HTMLProps >(({ children, label, node, icon, ...props }, forwardedRef) => { const [isOpen, setIsOpen] = React.useState(false); const [hasFocusInside, setHasFocusInside] = React.useState(false); const [activeIndex, setActiveIndex] = React.useState(null); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); const parent = React.useContext(MenuContext); const tree = useFloatingTree(); const nodeId = useFloatingNodeId(); const parentId = useFloatingParentNodeId(); const item = useListItem(); const isNested = parentId != null; const { floatingStyles, refs, context } = useFloating({ nodeId, open: isOpen, onOpenChange: setIsOpen, placement: isNested ? "right-start" : "bottom-start", middleware: [ offset({ mainAxis: isNested ? 4 : 4, alignmentAxis: isNested ? -4 : 0, }), flip(), shift(), ], whileElementsMounted: autoUpdate, }); const hover = useHover(context, { enabled: isNested, delay: { open: 75 }, handleClose: safePolygon({ blockPointerEvents: true }), }); const click = useClick(context, { event: "mousedown", toggle: !isNested, ignoreMouse: isNested, }); const role = useRole(context, { role: "menu" }); const dismiss = useDismiss(context, { bubbles: true }); const listNavigation = useListNavigation(context, { listRef: elementsRef, activeIndex, nested: isNested, onNavigate: setActiveIndex, }); const typeahead = useTypeahead(context, { listRef: labelsRef, onMatch: isOpen ? setActiveIndex : undefined, activeIndex, }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ hover, click, role, dismiss, listNavigation, typeahead, ]); // Event emitter allows you to communicate across tree components. // This effect closes all menus when an item gets clicked anywhere // in the tree. React.useEffect(() => { if (!tree) return; function handleTreeClick() { setIsOpen(false); } function onSubMenuOpen(event: { nodeId: string; parentId: string }) { if (event.nodeId !== nodeId && event.parentId === parentId) { setIsOpen(false); } } tree.events.on("click", handleTreeClick); tree.events.on("menuopen", onSubMenuOpen); return () => { tree.events.off("click", handleTreeClick); tree.events.off("menuopen", onSubMenuOpen); }; }, [tree, nodeId, parentId]); React.useEffect(() => { if (isOpen && tree) { tree.events.emit("menuopen", { parentId, nodeId }); } }, [tree, isOpen, nodeId, parentId]); return ( {isOpen && (
setIsOpen(false)}>
{children}
)}
); }); interface MenuItemProps { label: string; disabled?: boolean; icon?: React.ReactNode; } export const MenuItem = React.forwardRef< HTMLButtonElement, MenuItemProps & React.ButtonHTMLAttributes >(({ label, disabled, icon, ...props }, forwardedRef) => { const menu = React.useContext(MenuContext); const item = useListItem({ label: disabled ? null : label }); const tree = useFloatingTree(); const isActive = item.index === menu.activeIndex; return ( ); }); export const Menu = React.forwardRef< HTMLButtonElement, MenuProps & React.HTMLProps >((props, ref) => { const parentId = useFloatingParentNodeId(); if (parentId === null) { return ( ); } return ( <> ); });