/** * Solid 2 port of Base UI React's `NavigationMenuRootContext` * (`packages/react/src/navigation-menu/root/NavigationMenuRootContext.ts`). * * Thread the navigation-menu root's reactive state + mutable slots to every * descendant part. Every reactive field is exposed as an accessor (no bare * values) so consumers don't need to unwrap signals. * * `createContext(null)` + `== null` sentinel — the * nested `` branch checks the parent context before * wrapping in a second `FloatingTree`, so the context must tolerate being * read from above a provider. */ import { type Accessor, createContext, type Setter, useContext, } from "solid-js"; import type { BaseUIChangeEventDetails, REASONS, } from "../../internals/change-event-details"; import type { FloatingRootStore } from "../../internals/floating-ui/components/floating-root-store"; import type { MutableRef } from "../../internals/floating-ui/types"; import type { TransitionStatus } from "../../internals/use-transition-status/use-transition-status"; /** * Shared slot between triggers for canceling a stale popup auto-size reset. * Mutable — React uses a `useRef`; Solid uses a plain object carried on * context. The `owner` key is the item value that scheduled the reset, so a * newly-active trigger can detect whether it "owns" the abort controller * before clobbering it. */ export interface NavigationMenuPopupAutoSizeResetState { abortController: AbortController | null; owner: unknown; } /** * Direction of activation — set when a new trigger takes over from the * previously active one. Used by the content panel to choose an enter * direction for its animation. */ export type NavigationMenuActivationDirection = | "left" | "right" | "up" | "down" | null; /** * Change-event reason union for `NavigationMenu.Root`. Ported 1:1 from * React's `NavigationMenuRootChangeEventReason`. */ export type NavigationMenuRootChangeEventReason = | typeof REASONS.triggerPress | typeof REASONS.triggerHover | typeof REASONS.outsidePress | typeof REASONS.listNavigation | typeof REASONS.focusOut | typeof REASONS.escapeKey | typeof REASONS.linkPress | typeof REASONS.none; export type NavigationMenuRootChangeEventDetails = BaseUIChangeEventDetails; export interface NavigationMenuRootContextValue { /** Reactive open state (true once `value` is non-nullish). */ open: Accessor; /** Currently active item value (or `null`). */ value: Accessor; /** Change the active item value. Accepts cancel-able event details. */ setValue: ( next: Value | null, details: NavigationMenuRootChangeEventDetails, ) => void; /** Enter/exit transition phase, or undefined when settled. */ transitionStatus: Accessor; /** Whether the popup DOM subtree is currently mounted. */ mounted: Accessor; popupElement: Accessor; setPopupElement: Setter; positionerElement: Accessor; setPositionerElement: Setter; viewportElement: Accessor; setViewportElement: Setter; viewportTargetElement: Accessor; setViewportTargetElement: Setter; activationDirection: Accessor; setActivationDirection: Setter; floatingRootContext: Accessor; setFloatingRootContext: Setter; currentContentRef: MutableRef; nested: boolean; rootRef: MutableRef; beforeInsideRef: MutableRef; afterInsideRef: MutableRef; beforeOutsideRef: MutableRef; afterOutsideRef: MutableRef; prevTriggerElementRef: MutableRef; popupAutoSizeResetRef: MutableRef; delay: Accessor; closeDelay: Accessor; orientation: Accessor<"horizontal" | "vertical">; viewportInert: Accessor; setViewportInert: Setter; } export const NavigationMenuRootContext = createContext(null); export function useNavigationMenuRootContext(): NavigationMenuRootContextValue; export function useNavigationMenuRootContext( optional: true, ): NavigationMenuRootContextValue | null; export function useNavigationMenuRootContext( optional?: boolean, ): NavigationMenuRootContextValue | null { const value = useContext(NavigationMenuRootContext); if (value == null && !optional) { throw new Error( "Base UI: NavigationMenuRootContext is missing. Navigation Menu parts must be placed within .", ); } return value; } /** * Tree-scope context — carries the `FloatingTree` node id the current root * registered itself under. Nested menus read it so `isOutsideMenuEvent` can * consult child-node floatings. * * `null` sentinel because top-level roots may be mounted without a parent * `FloatingTree` (first-mount path wraps itself). */ export const NavigationMenuTreeContext = createContext(null); export function useNavigationMenuTreeContext(): string | undefined { const v = useContext(NavigationMenuTreeContext); return v ?? undefined; }