import type { JSX } from "@solidjs/web"; /** * `createNavigationMenu` — headless primitive for the NavigationMenu compound. * * Owns: * - `value` — currently active item value (consumer-owned signal). * - `open` — derived as `value() != null`. * - `activationDirection` — animation hint for content panels. * - `viewportInert` — whether the viewport is inert (pointer-events guard). * - `setValue` — pure event-emitter via `runWithOwner(null, ...)`. * * DOM concerns (FloatingTree, floating-root context, refs, transition, * focus return, popupElement, positionerElement) stay in * ``. */ import { type Accessor, createContext, createMemo, createSignal, runWithOwner, type Setter, untrack, useContext, } from "solid-js"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; import type { NavigationMenuActivationDirection, NavigationMenuRootChangeEventDetails, NavigationMenuRootChangeEventReason, } from "./root/navigation-menu-root-context"; // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- export type NavigationMenuChangeReason = NavigationMenuRootChangeEventReason; export interface CreateNavigationMenuOptions { /** * Currently active item value. Consumer-owned signal — `null` means closed. * The primitive reads through the accessor and does NOT keep an internal * copy. Writes happen via `onValueChange`. */ value: MaybeAccessor; /** * Called via `runWithOwner(null, ...)` so consumer signal writes don't * trigger SIGNAL_WRITE_IN_OWNED_SCOPE. */ onValueChange?: ( value: Value | null, details: NavigationMenuRootChangeEventDetails, ) => void; /** * Hover open delay in ms. * @default 50 */ delay?: MaybeAccessor; /** * Hover close delay in ms. * @default 50 */ closeDelay?: MaybeAccessor; /** * Orientation of the navigation menu. * @default "horizontal" */ orientation?: MaybeAccessor<"horizontal" | "vertical">; } export interface NavigationMenuApi { /** Currently active item value (or null). */ value: Accessor; /** Whether any item is currently active. */ open: Accessor; /** Hover open delay. */ delay: Accessor; /** Hover close delay. */ closeDelay: Accessor; /** Orientation. */ orientation: Accessor<"horizontal" | "vertical">; /** Direction of the most recent activation transition. */ activationDirection: Accessor; /** Whether the viewport is inert (pointer-events guard during transitions). */ viewportInert: Accessor; /** * Pure event-emitter — fires `onValueChange` via `runWithOwner(null, ...)`. * The consumer owns the signal write. */ setValue: ( next: Value | null, details?: NavigationMenuRootChangeEventDetails, ) => void; /** Update the activation direction. Internal coordination for animations. */ setActivationDirection: Setter; /** Toggle the viewport inert state. Internal coordination. */ setViewportInert: Setter; /** Context provider — expose the api via `useNavigationMenu()`. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- const NavigationMenuApiContext = createContext | null>(null); /** * Read the nearest {@link NavigationMenuApi}. Throws unless `optional` is true. */ export function useNavigationMenu(): NavigationMenuApi; export function useNavigationMenu( optional: true, ): NavigationMenuApi | null; export function useNavigationMenu( optional?: boolean, ): NavigationMenuApi | null { const ctx = useContext(NavigationMenuApiContext); if (ctx == null && !optional) { throw new Error( "Base UI: useNavigationMenu() must be called inside a .", ); } return ctx as NavigationMenuApi | null; } // --------------------------------------------------------------------------- // Primitive // --------------------------------------------------------------------------- /** * Headless primitive for the NavigationMenu compound. Owns the value / * derived-open state, activation direction, and viewport-inert flag. DOM * concerns (FloatingTree, refs, transition, focus return, positioner) live * in ``. */ export function createNavigationMenu( options: CreateNavigationMenuOptions, ): NavigationMenuApi { // Value reads through the consumer's accessor — no internal copy. const value = createMemo( () => { const raw = readOption(options.value) as Value | null | undefined; return raw ?? null; }, { name: "navigationMenu.value" }, ); // Open is a pure derivation — non-null value means something is active. const open = createMemo(() => value() != null, { name: "navigationMenu.open", }); const delay = createMemo(() => readOption(options.delay) ?? 50, { name: "navigationMenu.delay", }); const closeDelay = createMemo( () => readOption(options.closeDelay) ?? 50, { name: "navigationMenu.closeDelay" }, ); const orientation = createMemo<"horizontal" | "vertical">( () => readOption(options.orientation) ?? "horizontal", { name: "navigationMenu.orientation" }, ); const [activationDirection, setActivationDirection] = createSignal(null, { ownedWrite: true, name: "navigationMenu.activationDirection", }); const [viewportInert, setViewportInert] = createSignal(false, { ownedWrite: true, name: "navigationMenu.viewportInert", }); // Pure event-emitter — escapes the reactive owner so consumer signal // writes inside onValueChange don't trip SIGNAL_WRITE_IN_OWNED_SCOPE. const setValue = ( next: Value | null, details?: NavigationMenuRootChangeEventDetails, ): void => { const d: NavigationMenuRootChangeEventDetails = details ?? ({ reason: "none", isCanceled: false, } as NavigationMenuRootChangeEventDetails); // Reset activation direction on close — consistent with Root behavior. if (!next) { setActivationDirection(null); } const prev = untrack(value); if (next !== prev) { runWithOwner(null, () => options.onValueChange?.(next, d)); } }; const api: NavigationMenuApi = { value, open, delay, closeDelay, orientation, activationDirection, viewportInert, setValue, setActivationDirection, setViewportInert, Provider: (props) => ( }> {props.children} ), }; (api as NavigationMenuApi & Snapshottable)[SNAPSHOT_KEYS] = NAVIGATION_MENU_SNAPSHOT_KEYS; return api; } const NAVIGATION_MENU_SNAPSHOT_KEYS = snapshotKeys( "value", "open", "orientation", "activationDirection", "viewportInert", );