// // Copyright 2024 DXOS.org // // This is based upon `@radix-ui/react-dropdown-menu` fetched 25 Oct 2024 at https://github.com/radix-ui/primitives at commit 06de2d4. import { composeEventHandlers } from '@radix-ui/primitive'; import { composeRefs } from '@radix-ui/react-compose-refs'; import { createContextScope } from '@radix-ui/react-context'; import type { Scope } from '@radix-ui/react-context'; import { useId } from '@radix-ui/react-id'; import * as MenuPrimitive from '@radix-ui/react-menu'; import { createMenuScope } from '@radix-ui/react-menu'; import { Primitive } from '@radix-ui/react-primitive'; import { Slot } from '@radix-ui/react-slot'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import React, { type ComponentPropsWithoutRef, type ComponentPropsWithRef, type ComponentRef, type FC, PropsWithChildren, type ReactNode, type RefObject, forwardRef, useCallback, useEffect, useMemo, useRef, } from 'react'; import { useElevationContext, useSafeCollisionPadding, useThemeContext } from '../../hooks'; import { type ThemedClassName } from '../../util'; type Direction = 'ltr' | 'rtl'; // // DropdownMenu // const DROPDOWN_MENU_NAME = 'DropdownMenu'; type ScopedProps

= P & { __scopeDropdownMenu?: Scope }; const [createDropdownMenuContext, createDropdownMenuScope] = createContextScope(DROPDOWN_MENU_NAME, [createMenuScope]); const useMenuScope: (scope?: Scope) => any = createMenuScope(); type DropdownMenuContextValue = { triggerId: string; triggerRef: RefObject; contentId: string; open: boolean; onOpenChange(open: boolean): void; onOpenToggle(): void; modal: boolean; }; const [DropdownMenuProvider, useDropdownMenuContext] = createDropdownMenuContext(DROPDOWN_MENU_NAME); type DropdownMenuRootProps = PropsWithChildren<{ dir?: Direction; modal?: boolean; open?: boolean; defaultOpen?: boolean; onOpenChange?(open: boolean): void; }>; const DropdownMenuRoot = ({ __scopeDropdownMenu, children, dir, modal = true, open: openProp, defaultOpen, onOpenChange, }: ScopedProps) => { const menuScope = useMenuScope(__scopeDropdownMenu); const triggerRef = useRef(null); const [open = false, setOpen] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChange, }); return ( setOpen((prevOpen) => !prevOpen), [setOpen])} modal={modal} > {children} ); }; DropdownMenuRoot.displayName = DROPDOWN_MENU_NAME; // // DropdownMenuTrigger // const TRIGGER_NAME = 'DropdownMenu.Trigger'; type DropdownMenuTriggerElement = ComponentRef; type PrimitiveButtonProps = ComponentPropsWithoutRef; type DropdownMenuTriggerProps = PrimitiveButtonProps; const DropdownMenuTrigger = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, disabled = false, ...triggerProps } = props; const context = useDropdownMenuContext(TRIGGER_NAME, __scopeDropdownMenu); const menuScope = useMenuScope(__scopeDropdownMenu); return ( { // only call handler if it's the left button (mousedown gets triggered by all mouse buttons) // but not when the control key is pressed (avoiding MacOS right click) if (!disabled && event.button === 0 && event.ctrlKey === false) { context.onOpenToggle(); // prevent trigger focusing when opening // this allows the content to be given focus without competition if (!context.open) { event.preventDefault(); } } })} onKeyDown={composeEventHandlers(props.onKeyDown, (event) => { if (disabled) { return; } if (['Enter', ' '].includes(event.key)) { context.onOpenToggle(); } if (event.key === 'ArrowDown') { context.onOpenChange(true); } // prevent keydown from scrolling window / first focused item to execute // that keydown (inadvertently closing the menu) if (['Enter', ' ', 'ArrowDown'].includes(event.key)) { event.preventDefault(); } })} /> ); }, ); DropdownMenuTrigger.displayName = TRIGGER_NAME; // // DropdownMenuVirtualTrigger // const VIRTUAL_TRIGGER_NAME = 'DropdownMenu.VirtualTrigger'; type DropdownMenuVirtualTriggerProps = { virtualRef: RefObject; }; const DropdownMenuVirtualTrigger = (props: ScopedProps) => { const { __scopeDropdownMenu, virtualRef } = props; const context = useDropdownMenuContext(VIRTUAL_TRIGGER_NAME, __scopeDropdownMenu); const menuScope = useMenuScope(__scopeDropdownMenu); useEffect(() => { if (virtualRef.current) { context.triggerRef.current = virtualRef.current; } }); return } />; }; DropdownMenuVirtualTrigger.displayName = VIRTUAL_TRIGGER_NAME; // // DropdownMenuPortal // const PORTAL_NAME = 'DropdownMenu.Portal'; type MenuPortalProps = ComponentPropsWithoutRef; type DropdownMenuPortalProps = MenuPortalProps; const DropdownMenuPortal: FC = (props: ScopedProps) => { const { __scopeDropdownMenu, ...portalProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }; DropdownMenuPortal.displayName = PORTAL_NAME; // // DropdownMenuViewport // type DropdownMenuViewportProps = ThemedClassName> & { asChild?: boolean; }; const DropdownMenuViewport = forwardRef( ({ classNames, asChild, children, ...props }, forwardedRef) => { const { tx } = useThemeContext(); const Comp = asChild ? Slot : Primitive.div; return ( {children} ); }, ); // // DropdownMenuContent // const CONTENT_NAME = 'DropdownMenu.Content'; type DropdownMenuContentElement = ComponentRef; type MenuContentProps = ThemedClassName>; type DropdownMenuContentProps = Omit; const DropdownMenuContent = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, collisionPadding = 8, collisionBoundary, ...contentProps } = props; const { tx } = useThemeContext(); const context = useDropdownMenuContext(CONTENT_NAME, __scopeDropdownMenu); const elevation = useElevationContext(); const menuScope = useMenuScope(__scopeDropdownMenu); const hasInteractedOutsideRef = useRef(false); const safeCollisionPadding = useSafeCollisionPadding(collisionPadding); // Check for the closest annotated collision boundary in the DOM tree. const computedCollisionBoundary = useMemo(() => { const closestBoundary = context.triggerRef.current?.closest( '[data-popover-collision-boundary]', ) as HTMLElement | null; return closestBoundary ? Array.isArray(collisionBoundary) ? [closestBoundary, ...collisionBoundary] : collisionBoundary ? [closestBoundary, collisionBoundary] : [closestBoundary] : collisionBoundary; }, [context.open, collisionBoundary, context.triggerRef.current]); return ( { if (!hasInteractedOutsideRef.current) { context.triggerRef.current?.focus(); } hasInteractedOutsideRef.current = false; // Always prevent auto focus because we either focus manually or want user agent focus event.preventDefault(); })} onInteractOutside={composeEventHandlers(props.onInteractOutside, (event) => { const originalEvent = event.detail.originalEvent as PointerEvent; const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true; const isRightClick = originalEvent.button === 2 || ctrlLeftClick; if (!context.modal || isRightClick) { hasInteractedOutsideRef.current = true; } })} data-arrow-keys='up down' className={tx('menu.content', { elevation }, classNames)} style={{ ...props.style, // re-namespace exposed content custom properties ...{ '--radix-dropdown-menu-content-transform-origin': 'var(--radix-popper-transform-origin)', '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)', '--radix-dropdown-menu-content-available-height': 'var(--radix-popper-available-height)', '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)', '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)', }, }} /> ); }, ); DropdownMenuContent.displayName = CONTENT_NAME; // // DropdownMenuGroup // const GROUP_NAME = 'DropdownMenu.Group'; type DropdownMenuGroupElement = ComponentRef; type MenuGroupProps = ComponentPropsWithoutRef; type DropdownMenuGroupProps = MenuGroupProps; const DropdownMenuGroup = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...groupProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuGroup.displayName = GROUP_NAME; // // DropdownMenuLabel // const LABEL_NAME = 'DropdownMenu.Label'; type DropdownMenuLabelElement = ComponentRef; type MenuLabelProps = ThemedClassName>; type DropdownMenuLabelProps = MenuLabelProps; const DropdownMenuGroupLabel = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...labelProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuGroupLabel.displayName = LABEL_NAME; // // DropdownMenuItem // const ITEM_NAME = 'DropdownMenu.Item'; type DropdownMenuItemElement = ComponentRef; type MenuItemProps = ThemedClassName>; type DropdownMenuItemProps = MenuItemProps; const DropdownMenuItem = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...itemProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuItem.displayName = ITEM_NAME; // // DropdownMenuCheckboxItem // const CHECKBOX_ITEM_NAME = 'DropdownMenu.CheckboxItem'; type DropdownMenuCheckboxItemElement = ComponentRef; type MenuCheckboxItemProps = ThemedClassName>; type DropdownMenuCheckboxItemProps = MenuCheckboxItemProps; const DropdownMenuCheckboxItem = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...checkboxItemProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME; // // DropdownMenuRadioGroup // const RADIO_GROUP_NAME = 'DropdownMenu.RadioGroup'; type DropdownMenuRadioGroupElement = ComponentRef; type MenuRadioGroupProps = ComponentPropsWithoutRef; type DropdownMenuRadioGroupProps = MenuRadioGroupProps; const DropdownMenuRadioGroup = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...radioGroupProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuRadioGroup.displayName = RADIO_GROUP_NAME; // // DropdownMenuRadioItem // const RADIO_ITEM_NAME = 'DropdownMenu.RadioItem'; type DropdownMenuRadioItemElement = ComponentRef; type MenuRadioItemProps = ComponentPropsWithoutRef; type DropdownMenuRadioItemProps = ThemedClassName; const DropdownMenuRadioItem = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...itemProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuRadioItem.displayName = RADIO_ITEM_NAME; // // DropdownMenuItemIndicator // const INDICATOR_NAME = 'DropdownMenu.ItemIndicator'; type DropdownMenuItemIndicatorElement = ComponentRef; type MenuItemIndicatorProps = ComponentPropsWithoutRef; type DropdownMenuItemIndicatorProps = MenuItemIndicatorProps; const DropdownMenuItemIndicator = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...itemIndicatorProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuItemIndicator.displayName = INDICATOR_NAME; // // DropdownMenuSeparator // const SEPARATOR_NAME = 'DropdownMenu.Separator'; type DropdownMenuSeparatorElement = ComponentRef; type MenuSeparatorProps = ThemedClassName>; type DropdownMenuSeparatorProps = MenuSeparatorProps; const DropdownMenuSeparator = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...separatorProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuSeparator.displayName = SEPARATOR_NAME; // // DropdownMenuArrow // const ARROW_NAME = 'DropdownMenu.Arrow'; type DropdownMenuArrowElement = ComponentRef; type MenuArrowProps = ThemedClassName>; type DropdownMenuArrowProps = MenuArrowProps; const DropdownMenuArrow = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, ...arrowProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const { tx } = useThemeContext(); return ( ); }, ); DropdownMenuArrow.displayName = ARROW_NAME; // // DropdownMenuSub // type DropdownMenuSubProps = { children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?(open: boolean): void; }; const DropdownMenuSub: FC = (props: ScopedProps) => { const { __scopeDropdownMenu, children, open: openProp, onOpenChange, defaultOpen } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const [open = false, setOpen] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChange, }); return ( {children} ); }; // // DropdownMenuSubTrigger // const SUB_TRIGGER_NAME = 'DropdownMenu.SubTrigger'; type DropdownMenuSubTriggerElement = ComponentRef; type MenuSubTriggerProps = ComponentPropsWithoutRef; type DropdownMenuSubTriggerProps = MenuSubTriggerProps; const DropdownMenuSubTrigger = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...subTriggerProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuSubTrigger.displayName = SUB_TRIGGER_NAME; // // DropdownMenuSubContent // const SUB_CONTENT_NAME = 'DropdownMenu.SubContent'; type DropdownMenuSubContentElement = ComponentRef; type MenuSubContentProps = ComponentPropsWithoutRef; type DropdownMenuSubContentProps = MenuSubContentProps; const DropdownMenuSubContent = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...subContentProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ( ); }, ); DropdownMenuSubContent.displayName = SUB_CONTENT_NAME; // // DropdownMenu // export const DropdownMenu = { Root: DropdownMenuRoot, Trigger: DropdownMenuTrigger, VirtualTrigger: DropdownMenuVirtualTrigger, Portal: DropdownMenuPortal, Content: DropdownMenuContent, Viewport: DropdownMenuViewport, Group: DropdownMenuGroup, GroupLabel: DropdownMenuGroupLabel, Item: DropdownMenuItem, CheckboxItem: DropdownMenuCheckboxItem, RadioGroup: DropdownMenuRadioGroup, RadioItem: DropdownMenuRadioItem, ItemIndicator: DropdownMenuItemIndicator, Separator: DropdownMenuSeparator, Arrow: DropdownMenuArrow, Sub: DropdownMenuSub, SubTrigger: DropdownMenuSubTrigger, SubContent: DropdownMenuSubContent, }; type DropdownMenuScope = Scope; const useDropdownMenuMenuScope: (scope?: DropdownMenuScope) => any = useMenuScope; export { createDropdownMenuScope, useDropdownMenuContext, useDropdownMenuMenuScope }; export type { DropdownMenuArrowProps, DropdownMenuCheckboxItemProps, DropdownMenuContentProps, DropdownMenuGroupProps, DropdownMenuItemIndicatorProps, DropdownMenuItemProps, DropdownMenuLabelProps, DropdownMenuPortalProps, DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps, DropdownMenuRootProps, DropdownMenuSeparatorProps, DropdownMenuSubContentProps, DropdownMenuSubProps, DropdownMenuSubTriggerProps, DropdownMenuTriggerProps, DropdownMenuViewportProps, DropdownMenuVirtualTriggerProps, };