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; disabledKeys?: Key[]; 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, disabledKeys = [] } = 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(), disabledKeys, })} )}
); } interface MenuPanelProps extends AriaMenuProps { onClose?: () => void; disabledKeys?: Key[]; } 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; disabledKeys?: Key[]; } function MenuSection({ section, state, disabledKeys }: 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; isDisabled?: boolean; } function MenuItem({ item, state, isDisabled }: MenuItemProps) { const ref = useRef(null); const { menuItemProps } = useMenuItem({ key: item.key }, state, ref); const isFocused = state.selectionManager.focusedKey === item.key; return (
  • {item.rendered}
  • ); } export default Object.assign(Menu, { Button, IconButton, Panel, Section, Item, });