import type * as React from 'react'; import type { AutoFocusEvent } from '../utils/dismissal.js'; import type { MenuSubContentProps as SharedMenuSubContentProps, MenuSubProps as SharedMenuSubProps, MenuSubTriggerProps as SharedMenuSubTriggerProps } from '../utils/menuSub.js'; import type { PassThroughProps } from '../utils/passThrough.js'; import type { Align, PopperContentProps, Side } from '../utils/popper.js'; import type { PortalProps } from '../utils/portal.js'; import type { PolymorphicTag, PrimitivePropsWithRef } from '../utils/primitive.js'; import type { FocusOutsideEvent, InteractOutsideEvent } from '../utils/useDismiss.js'; /** * Menu entry: plain action item, separator, checkbox, radio group, or submenu. */ export type MenuEntry = { label: string; /** * Called when item is activated; fires before recipe's `onSelect`. * @default undefined — no-op if omitted. */ onSelect?: (() => void) | undefined; /** * Keyboard hint rendered as `MenuShortcut`; aria-hidden. * @default undefined — no shortcut is rendered. */ shortcut?: string | undefined; /** * Marks item aria-disabled; blocks selection, stays focusable. * @default false */ disabled?: boolean | undefined; /** * Text for typeahead matching; reflected to `data-text-value`. * @default undefined — the item's rendered text is used. */ textValue?: string | undefined; } | { /** Renders a horizontal rule separating item groups. */ type: 'separator'; } | { type: 'checkbox'; label: string; /** * Checked state, always externally controlled via `onCheckedChange`. * @default false */ checked?: boolean | undefined; /** * Fires with new checked state when item is selected. * @default undefined — no-op if omitted. */ onCheckedChange?: ((checked: boolean) => void) | undefined; /** * Keyboard hint rendered as `MenuShortcut`; aria-hidden. * @default undefined — no shortcut is rendered. */ shortcut?: string | undefined; /** * Marks item aria-disabled; blocks selection, stays focusable. * @default false */ disabled?: boolean | undefined; /** * Text for typeahead matching; reflected to `data-text-value`. * @default undefined — the item's rendered text is used. */ textValue?: string | undefined; } | { type: 'radio-group'; /** * Selected option's `value`, externally controlled. * @default undefined — no option is selected. */ value?: string | undefined; /** * Fires with newly selected option's `value`. * @default undefined — no-op if omitted. */ onValueChange?: ((value: string) => void) | undefined; items: { label: string; /** Compared against group's `value` for checked state and `onValueChange`. */ value: string; /** * Marks option aria-disabled; blocks selection, stays focusable. * @default false */ disabled?: boolean | undefined; /** * Text for typeahead matching; reflected to `data-text-value`. * @default undefined — the option's rendered text is used. */ textValue?: string | undefined; }[]; } | { type: 'submenu'; label: string; items: MenuEntry[]; /** * Marks SubTrigger aria-disabled; blocks submenu open, stays focusable. * @default false */ disabled?: boolean | undefined; /** * Text for typeahead matching on SubTrigger. * @default undefined — the trigger's rendered text is used. */ textValue?: string | undefined; } | { type: 'group'; /** * Heading rendered as `MenuLabel`; the only accessible name for the group. * @default undefined — the group is rendered unnamed. */ label?: string | undefined; items: MenuEntry[]; }; /** Which end of item list the opening key asks focus to land: `first` (Enter/Space/ArrowDown) or `last` (ArrowUp). */ export type MenuOpenIntent = 'first' | 'last'; /** Which gesture opens the menu: a click on a trigger button, or a right-click/long-press on a region. */ export type MenuOpenOn = 'click' | 'contextmenu'; /** Viewport coordinates the panel is placed at under `openOn="contextmenu"`. */ export interface Point { /** Distance in CSS pixels from the left edge of the viewport. */ x: number; /** Distance in CSS pixels from the top edge of the viewport. */ y: number; } /** @internal Shared state distributed from {@link Menu} to its parts via context. */ export interface MenuContextValue { triggerRef: React.RefObject; contentRef: React.RefObject; contentId: string; /** Trigger's id; Content's `aria-labelledby` points at it for proper menu naming. */ triggerId: string; open: boolean; modal: boolean; closeOnSelect: boolean; /** Gesture the Trigger listens for, and the placement mode Content uses. */ openOn: MenuOpenOn; /** Coordinates Content places itself at under `contextmenu`: the root's `point`, else the recorded pointer position. */ point: Point; setPoint: (point: Point) => void; onOpenChange: (open: boolean) => void; /** Returns focus to Trigger, lifting `modal` inertness so trigger can receive it. */ focusTrigger: () => void; /** Content's `onCloseAutoFocus` handler; ref so root can own focus movement. */ closeAutoFocusRef: React.RefObject<((event: AutoFocusEvent) => void) | undefined>; /** Which end of menu the last open request asked for; ref read by Content's open effect. */ openIntentRef: React.RefObject; /** Resolved writing direction for portalled content via DirectionProvider or DOM attribute. */ dir: string; /** Elements outside the panel that still count as inside its dismissal layer, such as a menubar's row of triggers. */ insideRefs?: readonly React.RefObject[] | undefined; /** Bumped on every open request so re-requesting an already-open panel still moves focus back into it. */ openRequestId?: number | undefined; } /** * Props for the {@link Menu} recipe. * No `children`: the menu is `trigger` plus `items`. Use primitives for manual composition. */ export interface MenuProps extends PassThroughProps { /** * Element that opens the menu; decorated via `asChild` and merged directly. * @default undefined — no trigger renders. */ trigger?: React.ReactElement | undefined; /** * Menu's entries rendered into the `MenuContent` panel. * @default undefined — an empty menu. */ items?: MenuEntry[] | undefined; /** * Fires with the activated entry after its own callback. * @default undefined — no-op if omitted. */ onSelect?: ((item: MenuEntry) => void) | undefined; /** * Which side of trigger the panel opens on. * @default 'bottom' */ side?: Side | undefined; /** * Alignment of the automatically rendered menu panel relative to the `side` it opens on. * @default 'start' */ align?: Align | undefined; /** * Render item contents in place of label; not called for separators or groups. * @default undefined — the entry's `label` is rendered. */ renderItem?: ((item: MenuEntry) => React.ReactNode) | undefined; /** * Whether menu is open; for controlled usage pair with `onOpenChange`. * @default undefined — uncontrolled. */ open?: boolean | undefined; /** * Whether menu is initially open for uncontrolled usage; under `contextmenu` with no `point` the panel opens clamped to the viewport's top-left corner. * @default false */ defaultOpen?: boolean | undefined; /** * Which gesture opens the menu: `click` anchors the panel to a trigger button and honours `side`/`align`; `contextmenu` turns the trigger into a right-clickable region opening the panel at the pointer. * @default 'click' */ openOn?: MenuOpenOn | undefined; /** * Viewport coordinates the panel is placed at under `openOn="contextmenu"`, overriding the recorded pointer position; ignored under `click`. * @default undefined — the position of the pointer that opened the menu. */ point?: Point | undefined; /** * Stands the derived trigger down so it opens nothing; under `openOn="contextmenu"` the native browser menu is let through instead. * @default false */ disabled?: boolean | undefined; /** * Whether the open menu marks outside content inert and locks body scroll; focus is never trapped, since Tab closes the menu per the WAI-ARIA pattern. * @default true — false under `openOn="contextmenu"`. */ modal?: boolean | undefined; /** * Whether selecting an item closes the menu, overridable per item; checkbox items stay open by default. * @default true */ closeOnSelect?: boolean | undefined; /** * Fires with new open state whenever menu opens or closes. * @default undefined — no-op if omitted. */ onOpenChange?: ((open: boolean) => void) | undefined; /** * Fires once the open state change finishes on screen, after animation ends, for work that must not race the transition. * @default undefined — no-op if omitted. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * Panel density rung the skin reads, fed to the derived Content and SubContent. * @default undefined — the skin's own default panel size. */ size?: 'sm' | 'lg' | (string & {}) | undefined; /** * DOM node the derived Portal renders the overlay into, so it inherits that subtree's theme and stacking context (e.g. a `[data-skdx-theme]` wrapper). * @default undefined — `document.body`. */ portalContainer?: Element | DocumentFragment | null | undefined; } /** Props for {@link MenuTrigger}, the button (or, under `openOn="contextmenu"`, the right-clickable region) that opens the menu. */ export interface MenuTriggerProps extends PrimitivePropsWithRef<'button'> { /** * Stands the trigger down so it opens nothing; under `openOn="contextmenu"` the native browser menu is let through instead. * @default false */ disabled?: boolean | undefined; } /** Props for {@link MenuPortal}; teleports children to `document.body`. */ export interface MenuPortalProps extends PortalProps { /** * Render portal while menu is closed instead of unmounting; for animation libraries. * @default false */ forceMount?: boolean | undefined; } /** Props for {@link MenuContent}; extends popper positioning props. */ export interface MenuContentProps extends PopperContentProps { /** * Panel density rung the skin reads, mirrored to `data-size` on the panel. * @default undefined — the skin's own default panel size. */ size?: 'sm' | 'lg' | (string & {}) | undefined; /** * Keep panel mounted while closed for animation libraries needing outgoing content. * @default false */ forceMount?: boolean | undefined; /** * Whether Up/Down arrow-key navigation wraps from last item to first. * @default true */ loop?: boolean | undefined; /** * Fires when Escape is pressed while menu is open; call `preventDefault()` to stop close. * @default undefined — no-op if omitted. */ onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; /** * Fires on pointer-down outside content; call `preventDefault()` to stop close. * @default undefined — no-op if omitted. */ onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; /** * Fires when focus moves outside content, carrying the original `focusin` event as `detail`; call `preventDefault()` to stop close. * @default undefined — no-op if omitted. */ onFocusOutside?: ((event: FocusOutsideEvent) => void) | undefined; /** * Fires for either outside interaction (pointer-down or focus), before the specific handlers above; one `preventDefault()` here vetoes dismissal either way. * @default undefined — no-op if omitted. */ onInteractOutside?: ((event: InteractOutsideEvent) => void) | undefined; /** * Fires just before focus returns to trigger as menu closes; call `preventDefault()` to suppress. * @default undefined — no-op if omitted. */ onCloseAutoFocus?: ((event: AutoFocusEvent) => void) | undefined; } /** Props for a single {@link MenuItem}. */ export interface MenuItemProps extends Omit, 'onSelect'> { /** * Tag name from the library's allowlist instead of the default `
`; use `asChild` for elements needing their own attributes. * @default 'div' */ as?: PolymorphicTag | undefined; /** * Marks item aria-disabled; blocks selection, stays focusable. * @default false */ disabled?: boolean | undefined; /** * Text for typeahead matching, reflected to `data-text-value`; required for items rendering more than a label or icon-only items. * @default undefined — the item's rendered text is used. */ textValue?: string | undefined; /** * Called when item is activated; call `preventDefault()` to keep menu open. * @default undefined — no-op if omitted. */ onSelect?: ((event: Event) => void) | undefined; /** * Whether selecting this item closes the menu, overriding the root's `closeOnSelect`; the item's own setting or `preventDefault()` can each veto independently. * @default undefined — inherits root's `closeOnSelect`. */ closeOnSelect?: boolean | undefined; } /** Props for {@link MenuCheckboxItem}, a toggleable menu item. */ export interface MenuCheckboxItemProps extends MenuItemProps { /** * Whether the item is checked, mirroring the checkbox's checked/unchecked visual state. * @default false */ checked?: boolean | undefined; /** * Fires with the new checked state when the item is selected. * @default undefined — no-op if omitted. */ onCheckedChange?: ((checked: boolean) => void) | undefined; /** * Whether selecting this item closes the menu; unlike other item types, checkbox items stay open by default since they exist to toggle multiple times. * @default false */ closeOnSelect?: boolean | undefined; } /** @internal Shared selected-value state distributed from {@link MenuRadioGroup} to its `MenuRadioItem`s via context. Not part of the public API. */ export interface MenuRadioGroupContextValue { value?: string | undefined; onValueChange?: ((value: string) => void) | undefined; } /** Props for {@link MenuRadioGroup}, a container coordinating a set of mutually-exclusive `MenuRadioItem`s. */ export interface MenuRadioGroupProps extends PrimitivePropsWithRef<'div'> { /** * Selected item's value; compared against each radio item's own `value`. * @default undefined — no item is selected. */ value?: string | undefined; /** * Fires with the new value when a `MenuRadioItem` in this group is selected. * @default undefined — no-op if omitted. */ onValueChange?: ((value: string) => void) | undefined; } /** Props for a single {@link MenuRadioItem} inside a `MenuRadioGroup`. */ export interface MenuRadioItemProps extends MenuItemProps { /** Unique identifier compared against the group's selected `value`. */ value: string; } /** Props for {@link MenuItemIndicator}, the checked-state glyph inside a checkbox/radio item. */ export interface MenuItemIndicatorProps extends PrimitivePropsWithRef<'span'> { /** * Keep indicator mounted while unchecked for animation libraries needing outgoing content. * @default false */ forceMount?: boolean | undefined; } /** Props for {@link MenuGroup}, a semantic grouping of related items. */ export interface MenuGroupProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link MenuLabel}, a non-interactive heading for a group of items. */ export interface MenuLabelProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link MenuSeparator}, a visual divider between items or groups. */ export interface MenuSeparatorProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link MenuShortcut}, the keyboard hint at an item's trailing edge. */ export interface MenuShortcutProps extends PrimitivePropsWithRef<'span'> { } /** Props for {@link MenuSub}, the root of a nested submenu. */ export interface MenuSubProps extends SharedMenuSubProps { } /** Props for {@link MenuSubTrigger}, the parent-menu item that opens a submenu and anchors it. */ export interface MenuSubTriggerProps extends SharedMenuSubTriggerProps { } /** Props for {@link MenuSubContent}, the submenu's floating panel. */ export interface MenuSubContentProps extends SharedMenuSubContentProps { }