import type { JSX } from "@solidjs/web"; /** * `` — Solid 2 port of Base UI React's `ContextMenuRoot`. * * React source: `packages/react/src/context-menu/root/ContextMenuRoot.tsx`. * * A thin wrapper around `` that supplies the virtual-anchor * plumbing: the trigger captures `(clientX, clientY)` on `contextmenu` (or * long press) and writes a `VirtualElement` into the root's `anchor` signal. * `` detects the parent `ContextMenuRootContext`, reads the * anchor, threads it into the floating-root context, and exposes an * imperative `setOpen` via `actionsRef` so the trigger can open the menu. * * Renders no DOM of its own — pure context provider chained around * ``. * * ### Port deviations * * - Nested `` is detected via an optional outer * `ContextMenuRootContext` read; the inner root exposes * `nestedInsideContextMenu` so the inner `` routes its * parent-type to `'nested-context-menu'`. * - Long-press touch gesture is ported (matches React's 500ms delay + * 10px move threshold) with unit coverage for the timer state * machine + move-threshold cancellation. Full `pointercancel` path * needs browser coverage. */ import { createUniqueId, useContext } from "solid-js"; import type { MutableRef } from "../../internals/floating-ui/types"; import { type MaybeAccessor, readOption } from "../../internals/maybe-accessor"; import { Menu } from "../../menu"; import type { MenuRootChangeEventDetails, MenuRootChangeEventReason, } from "../../menu/root/menu-root-context"; import { MenuRootContext } from "../../menu/root/menu-root-context"; import { createContextMenu } from "../create-context-menu"; import { ContextMenuRootContext, type ContextMenuRootContextValue, } from "./context-menu-root-context"; export type ContextMenuRootChangeEventReason = MenuRootChangeEventReason; export type ContextMenuRootChangeEventDetails = MenuRootChangeEventDetails; export type ContextMenuRootState = Record; // biome-ignore lint/correctness/noUnusedVariables: Payload is part of the public generic surface (parity with >). export interface ContextMenuRootProps { /** * Controlled open state. When provided (resolved value not * `undefined`), the consumer owns the state — pair with * {@link onOpenChange}. When omitted, the context menu manages its own * open state (seeded from {@link defaultOpen}) and the right-click / * long-press gesture opens it with zero wiring. */ open?: MaybeAccessor; /** * Initial open state when uncontrolled. Read once at mount. * @default false */ defaultOpen?: MaybeAccessor; /** Whether arrow keys loop at the ends. @default true */ loopFocus?: boolean | undefined; /** Highlight items on hover. @default true */ highlightItemOnHover?: boolean | undefined; /** Called when the menu opens or closes. */ onOpenChange?: | ((open: boolean, eventDetails: ContextMenuRootChangeEventDetails) => void) | undefined; /** Called after any animations complete when the menu is closed. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** Whether the menu ignores user interaction. @default false */ disabled?: MaybeAccessor; /** Visual/keyboard orientation. @default "vertical" */ orientation?: "horizontal" | "vertical" | undefined; /** * Children. May be a regular node or a render function that receives the * active trigger's payload. */ children?: JSX.Element; } /** * Creates a context menu activated by right-click or long-press. * Doesn't render its own HTML element — pure context provider chained * around ``. */ export function ContextMenuRoot( props: ContextMenuRootProps, ): JSX.Element { // Detect when this `` is itself nested // inside another ``. The inner root still owns its // own anchor + actionsRef; the flag is read downstream by the inner // `` to route its parent-type to `'nested-context-menu'`. const parentContextMenu = useContext(ContextMenuRootContext); const nestedInsideContextMenu = parentContextMenu != null; // Compose the headless ContextMenu primitive — owns open (gesture-driven // internal signal + consumer override), disabled, and anchor. The wrapper // layers virtual-anchor threading, actionsRef wiring, and context provision. const contextMenuPrimitive = createContextMenu({ open: () => readOption(props.open), defaultOpen: () => readOption(props.defaultOpen), disabled: () => readOption(props.disabled) ?? false, onOpenChange: (next, details) => { props.onOpenChange?.(next, details); }, }); const open = contextMenuPrimitive.open; const anchor = contextMenuPrimitive.anchor; const setAnchor = contextMenuPrimitive.setAnchor; const actionsRef: MutableRef<{ setOpen: ( nextOpen: boolean, eventDetails: MenuRootChangeEventDetails, ) => void; } | null> = { current: null }; const positionerRef: MutableRef = { current: null }; const backdropRef: MutableRef = { current: null }; const internalBackdropRef: MutableRef = { current: null, }; const allowMouseUpTriggerRef: MutableRef = { current: true }; const initialCursorPointRef: MutableRef<{ x: number; y: number } | null> = { current: null, }; const rootId = createUniqueId(); const contextValue: ContextMenuRootContextValue = { anchor, setAnchor, actionsRef, backdropRef, internalBackdropRef, positionerRef, allowMouseUpTriggerRef, initialCursorPointRef, rootId, nestedInsideContextMenu, }; // Chain the providers around ``. Per P4, evaluating children // eagerly at parent JSX would mask the ContextMenu context from the // inner ``'s own render — but here the provider IS the // parent, and `` evaluates within this tree, so the // straightforward `{children}` shape is safe. // // The inner `` is React-parity: it // prevents a nested `` from inheriting an outer menu's // context when `` itself is mounted inside another // menu. This is what lets `` identify the context-menu // parent by reading "no menu parent but yes context-menu parent". return ( loopFocus={props.loopFocus} highlightItemOnHover={props.highlightItemOnHover} onOpenChange={(next, details) => { // Route through the primitive so the gesture-driven internal // open signal stays in sync and onOpenChange fires once. contextMenuPrimitive.setOpen(next, details); }} onOpenChangeComplete={props.onOpenChangeComplete} open={open()} disabled={readOption(props.disabled)} orientation={props.orientation} modal > {props.children} ); }