import React, { useRef, cloneElement } from 'react'; import { type AriaMenuProps, Key, Placement, useMenuTrigger } from 'react-aria'; import { useMenu, useMenuItem, useMenuSection, useSeparator } from 'react-aria'; import { Item, Section } from 'react-stately'; import { type MenuTriggerProps, Node, TreeState, useMenuTriggerState, useTreeState, } from 'react-stately'; import Button, { ButtonProps } from '~/components/Button'; import IconButton, { IconButtonProps } from '~/components/IconButton'; import Popover from '~/components/Popover'; import cn from '~/utils/cn'; interface MenuProps extends MenuTriggerProps { placement?: Placement; isDisabled?: boolean; children: [ React.ReactElement | React.ReactElement, React.ReactElement, ]; } // TODO: onAction is called twice for some reason? // TODO: isDisabled per-prop function Menu(props: MenuProps) { const { placement = 'bottom', isDisabled } = props; const state = useMenuTriggerState(props); const ref = useRef(null); const { menuTriggerProps, menuProps } = useMenuTrigger( {}, state, ref, ); // cloneElement is necessary because the button is a union type // of multiple things and we need to join props from our hooks const [button, panel] = props.children; return (
{cloneElement(button, { ...menuTriggerProps, isDisabled: isDisabled, ref, })} {state.isOpen && ( {cloneElement(panel, { ...menuProps, autoFocus: state.focusStrategy ?? true, onClose: () => state.close(), })} )}
); } interface MenuPanelProps extends AriaMenuProps { onClose?: () => void; } function Panel(props: MenuPanelProps) { const state = useTreeState(props); const ref = useRef(null); const { menuProps } = useMenu(props, state, ref); return (
    {[...state.collection].map((item) => ( ))}
); } interface MenuSectionProps { section: Node; state: TreeState; } function MenuSection({ section, state }: MenuSectionProps) { const { itemProps, groupProps } = useMenuSection({ heading: section.rendered, 'aria-label': section['aria-label'], }); const { separatorProps } = useSeparator({ elementType: 'li', }); return ( <> {section.key !== state.collection.getFirstKey() ? (
  • ) : undefined}
    • {[...section.childNodes].map((item) => ( ))}
  • ); } interface MenuItemProps { item: Node; state: TreeState; } function MenuItem({ item, state }: MenuItemProps) { const ref = useRef(null); const { menuItemProps } = useMenuItem({ key: item.key }, state, ref); const isFocused = state.selectionManager.focusedKey === item.key; const isDisabled = state.selectionManager.isDisabled(item.key); return (
  • {item.rendered}
  • ); } export default Object.assign(Menu, { Button, IconButton, Panel, Section, Item, });