//
// 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,
};