// // 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 ReactNode, type FC, useRef, type ElementRef, useCallback, type ComponentPropsWithoutRef, forwardRef, type ComponentPropsWithRef, useEffect, type MutableRefObject, type RefObject, } from 'react'; import { useElevationContext, useThemeContext } from '../../hooks'; import { useSafeCollisionPadding } from '../../hooks/useSafeCollisionPadding'; 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 = createMenuScope(); type DropdownMenuContextValue = { triggerId: string; triggerRef: MutableRefObject; contentId: string; open: boolean; onOpenChange(open: boolean): void; onOpenToggle(): void; modal: boolean; }; const [DropdownMenuProvider, useDropdownMenuContext] = createDropdownMenuContext(DROPDOWN_MENU_NAME); interface DropdownMenuRootProps { children?: ReactNode; dir?: Direction; open?: boolean; defaultOpen?: boolean; onOpenChange?(open: boolean): void; modal?: boolean; } const DropdownMenuRoot: FC = (props: ScopedProps) => { const { __scopeDropdownMenu, children, dir, open: openProp, defaultOpen, onOpenChange, modal = true } = props; const menuScope = useMenuScope(__scopeDropdownMenu); const triggerRef = useRef(null); const [open = false, setOpen] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChange, }); return ( } contentId={useId()} open={open} onOpenChange={setOpen} onOpenToggle={useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen])} modal={modal} > {children} ); }; DropdownMenuRoot.displayName = DROPDOWN_MENU_NAME; /* ------------------------------------------------------------------------------------------------- * DropdownMenuTrigger * ----------------------------------------------------------------------------------------------- */ const TRIGGER_NAME = 'DropdownMenuTrigger'; type DropdownMenuTriggerElement = ElementRef; type PrimitiveButtonProps = ComponentPropsWithoutRef; interface DropdownMenuTriggerProps extends 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 = 'DropdownMenuVirtualTrigger'; interface 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 = 'DropdownMenuPortal'; type MenuPortalProps = ComponentPropsWithoutRef; interface DropdownMenuPortalProps extends 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 Root = asChild ? Slot : Primitive.div; return ( {children} ); }, ); /* ------------------------------------------------------------------------------------------------- * DropdownMenuContent * ----------------------------------------------------------------------------------------------- */ const CONTENT_NAME = 'DropdownMenuContent'; type DropdownMenuContentElement = ElementRef; type MenuContentProps = ThemedClassName>; interface DropdownMenuContentProps extends Omit {} const DropdownMenuContent = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, classNames, collisionPadding = 8, ...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); 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; } })} className={tx('menu.content', 'menu', { 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 = 'DropdownMenuGroup'; type DropdownMenuGroupElement = ElementRef; type MenuGroupProps = ComponentPropsWithoutRef; interface DropdownMenuGroupProps extends MenuGroupProps {} const DropdownMenuGroup = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...groupProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuGroup.displayName = GROUP_NAME; /* ------------------------------------------------------------------------------------------------- * DropdownMenuLabel * ----------------------------------------------------------------------------------------------- */ const LABEL_NAME = 'DropdownMenuLabel'; type DropdownMenuLabelElement = ElementRef; type MenuLabelProps = ThemedClassName>; interface DropdownMenuLabelProps extends 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 = 'DropdownMenuItem'; type DropdownMenuItemElement = ElementRef; type MenuItemProps = ThemedClassName>; interface DropdownMenuItemProps extends 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 = 'DropdownMenuCheckboxItem'; type DropdownMenuCheckboxItemElement = ElementRef; type MenuCheckboxItemProps = ThemedClassName>; interface DropdownMenuCheckboxItemProps extends 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 = 'DropdownMenuRadioGroup'; type DropdownMenuRadioGroupElement = ElementRef; type MenuRadioGroupProps = ComponentPropsWithoutRef; interface DropdownMenuRadioGroupProps extends 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 = 'DropdownMenuRadioItem'; type DropdownMenuRadioItemElement = ElementRef; type MenuRadioItemProps = ComponentPropsWithoutRef; interface DropdownMenuRadioItemProps extends MenuRadioItemProps {} const DropdownMenuRadioItem = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...radioItemProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuRadioItem.displayName = RADIO_ITEM_NAME; /* ------------------------------------------------------------------------------------------------- * DropdownMenuItemIndicator * ----------------------------------------------------------------------------------------------- */ const INDICATOR_NAME = 'DropdownMenuItemIndicator'; type DropdownMenuItemIndicatorElement = ElementRef; type MenuItemIndicatorProps = ComponentPropsWithoutRef; interface DropdownMenuItemIndicatorProps extends MenuItemIndicatorProps {} const DropdownMenuItemIndicator = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...itemIndicatorProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ; }, ); DropdownMenuItemIndicator.displayName = INDICATOR_NAME; /* ------------------------------------------------------------------------------------------------- * DropdownMenuSeparator * ----------------------------------------------------------------------------------------------- */ const SEPARATOR_NAME = 'DropdownMenuSeparator'; type DropdownMenuSeparatorElement = ElementRef; type MenuSeparatorProps = ThemedClassName>; interface DropdownMenuSeparatorProps extends 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 = 'DropdownMenuArrow'; type DropdownMenuArrowElement = ElementRef; type MenuArrowProps = ThemedClassName>; interface DropdownMenuArrowProps extends 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 * ----------------------------------------------------------------------------------------------- */ interface 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 = 'DropdownMenuSubTrigger'; type DropdownMenuSubTriggerElement = ElementRef; type MenuSubTriggerProps = ComponentPropsWithoutRef; interface DropdownMenuSubTriggerProps extends 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 = 'DropdownMenuSubContent'; type DropdownMenuSubContentElement = ElementRef; type MenuSubContentProps = ComponentPropsWithoutRef; interface DropdownMenuSubContentProps extends MenuSubContentProps {} const DropdownMenuSubContent = forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeDropdownMenu, ...subContentProps } = props; const menuScope = useMenuScope(__scopeDropdownMenu); return ( ); }, ); DropdownMenuSubContent.displayName = SUB_CONTENT_NAME; /* ----------------------------------------------------------------------------------------------- */ 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, }; const useDropdownMenuMenuScope = useMenuScope; export { createDropdownMenuScope, useDropdownMenuContext, useDropdownMenuMenuScope }; export type { DropdownMenuRootProps, DropdownMenuTriggerProps, DropdownMenuVirtualTriggerProps, DropdownMenuPortalProps, DropdownMenuContentProps, DropdownMenuViewportProps, DropdownMenuGroupProps, DropdownMenuLabelProps, DropdownMenuItemProps, DropdownMenuCheckboxItemProps, DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps, DropdownMenuItemIndicatorProps, DropdownMenuSeparatorProps, DropdownMenuArrowProps, DropdownMenuSubProps, DropdownMenuSubTriggerProps, DropdownMenuSubContentProps, };