import React, { Fragment, type HTMLAttributes, memo, type ReactElement, createContext, useState, useContext, useRef, useCallback, type ComponentPropsWithoutRef, } from 'react'; import { Menu, Transition } from '@headlessui/react'; import { type ReactTag, type ElementType, useMenuButton } from './a11y'; import ActionMenuItemList, { type IActionMenuItem } from './list'; import { blockContext } from '../../fields/fieldsContext'; interface IActionMenuContext { disabled: boolean; expanded: boolean; moveTo(x: number, y: number): void; open(): void; close(): void; _items: readonly IActionMenuItem[]; } const Context = createContext(null); interface IActionMenuProps { menu?: IActionMenuItem[]; disabled?: boolean; /** @default false */ enableContextMenu?: boolean; title?: string; } const ActionMenu: React.FC, keyof IActionMenuProps>> = (props) => { const { menu = [], disabled = false, enableContextMenu = false, title, ...attrs } = props; const [coord, setCoord] = useState<[x: number, y: number]>([0, 0]); const buttonRef = useRef(null); const isDisabled = disabled || menu.length === 0; const block = useContext(blockContext); return ( {({ open, close }) => { return (
{ if (isDisabled) return; e.preventDefault(); e.stopPropagation(); const blockRect = block.current?.getBoundingClientRect(); const { x, y } = blockRect ?? { x: 0, y: 0 }; setCoord([e.clientX - x, e.clientY - y]); if (!open) { buttonRef.current?.click(); } }} {...attrs} > {props.children}
{open &&
); }; type IActionMenuButtonProps = ( | { /** @default "button" */ as: T; } | { /** @default "button" */ as?: ReactTag; } ) & { onPress?: (ctx: IActionMenuContext | undefined) => void; /** @deprecated use `onPress()` instead */ onClick?: () => void; }; const ActionMenuButton = function ActionMenuButton( props: IActionMenuButtonProps & Omit, keyof IActionMenuProps> ): ReactElement { const { as: _as = 'button', onPress, children, ...attrs } = props; const Component = _as as T; const ctx = useContext(Context); const buttonRef = useRef>(null); const handlePress = useCallback(() => { if (ctx?.disabled) { return; } const btn = buttonRef.current; if (btn) { const rect = (btn as unknown as HTMLElement | SVGElement).getBoundingClientRect(); ctx?.moveTo(rect.x + rect.width, rect.y); } if (onPress) { return onPress(ctx ?? undefined); } if (ctx?.expanded) { ctx.close(); } else { ctx?.open(); } }, [ctx, onPress]); const buttonProps = useMenuButton({ ...attrs, 'aria-expanded': ctx?.expanded ?? false, onPress: handlePress, }); if (ctx?.disabled || !ctx?._items.length) { return
; } return ( // @ts-expect-error Expression produces a union type that is too complex to represent {children} ); }; export default Object.assign(ActionMenu, { Button: memo(ActionMenuButton), });