import type { JSX } from "@solidjs/web"; /** * `createMenubar` — headless primitive for the Menubar compound. * * Owns: * - `orientation` — horizontal or vertical. * - `modal` — modal mode flag. * - `disabled` — whole-menubar disabled state. * - `loopFocus` — whether arrow-key navigation wraps. * - `hasSubmenuOpen` — whether any child menu is currently open. * - `registerOpenMenu` / `closeAll` — single-open coordinator. * * DOM concerns (FloatingTree, FloatingNode, role="menubar" element, keyboard * handler) stay in ``. */ import { type Accessor, createContext, createMemo, createSignal, useContext, } from "solid-js"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; import type { MenuRootOrientation } from "../menu/root/menu-root-context"; // --------------------------------------------------------------------------- // Public types // --------------------------------------------------------------------------- export interface CreateMenubarOptions { /** * The orientation of the menubar. * @default "horizontal" */ orientation?: MaybeAccessor; /** * Whether the menubar is modal. * @default true */ modal?: MaybeAccessor; /** * Whether the whole menubar is disabled. * @default false */ disabled?: MaybeAccessor; /** * Whether arrow-key focus loops at the ends. * @default true */ loopFocus?: MaybeAccessor; } export interface MenubarApi { /** Visual/keyboard orientation. */ orientation: Accessor; /** Whether the menubar is modal. */ modal: Accessor; /** Whether the whole menubar is disabled. */ disabled: Accessor; /** Whether arrow-key focus loops at the ends. */ loopFocus: Accessor; /** Whether any child menu is currently open. */ hasSubmenuOpen: Accessor; /** * Called by a child menu when it opens. Returns an unregister fn the * child should call on close / unmount. Enforces single-open invariant * by closing the previously-open sibling. */ registerOpenMenu: (closer: () => void) => () => void; /** Force-close all registered open menus. */ closeAll: () => void; /** * Internal — called by a child `` when its open state changes. * Both register and direct setHasSubmenuOpen paths are preserved for * idempotency (matches existing Menubar implementation). */ setHasSubmenuOpen: (open: boolean) => void; /** Context provider — expose the api via `useMenubar()`. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- const MenubarApiContext = createContext(null); /** * Read the nearest {@link MenubarApi}. Throws unless `optional` is true. */ export function useMenubar(): MenubarApi; export function useMenubar(optional: true): MenubarApi | null; export function useMenubar(optional?: boolean): MenubarApi | null { const ctx = useContext(MenubarApiContext); if (ctx == null && !optional) { throw new Error( "Base UI: useMenubar() must be called inside a .", ); } return ctx; } // --------------------------------------------------------------------------- // Primitive // --------------------------------------------------------------------------- /** * Headless primitive for the Menubar compound. Owns logical state — orientation, * modal, disabled, loopFocus, hasSubmenuOpen, and the single-open coordinator. * DOM concerns (FloatingTree, role="menubar" element, keyboard handler) live * in ``. */ export function createMenubar(options: CreateMenubarOptions = {}): MenubarApi { const orientation = createMemo( () => readOption(options.orientation) ?? "horizontal", { name: "menubar.orientation" }, ); const modal = createMemo(() => readOption(options.modal) ?? true, { name: "menubar.modal", }); const disabled = createMemo( () => Boolean(readOption(options.disabled)), { name: "menubar.disabled" }, ); const loopFocus = createMemo( () => readOption(options.loopFocus) ?? true, { name: "menubar.loopFocus" }, ); const [hasSubmenuOpen, setHasSubmenuOpen] = createSignal(false, { ownedWrite: true, name: "menubar.hasSubmenuOpen", }); // Single-open coordinator — only one child menu open at a time. let currentCloser: (() => void) | null = null; const registerOpenMenu = (closer: () => void): (() => void) => { if (currentCloser && currentCloser !== closer) { const prev = currentCloser; currentCloser = null; prev(); } currentCloser = closer; setHasSubmenuOpen(true); return () => { // Only the currently-open menu clears the flag: a sibling that // closes because another one opened must not reset it. if (currentCloser === closer) { currentCloser = null; setHasSubmenuOpen(false); } }; }; const closeAll = (): void => { if (currentCloser) { const prev = currentCloser; currentCloser = null; setHasSubmenuOpen(false); prev(); } }; const api: MenubarApi = { orientation, modal, disabled, loopFocus, hasSubmenuOpen, registerOpenMenu, closeAll, setHasSubmenuOpen: (open) => { setHasSubmenuOpen(open); }, Provider: (props) => ( {props.children} ), }; (api as MenubarApi & Snapshottable)[SNAPSHOT_KEYS] = MENUBAR_SNAPSHOT_KEYS; return api; } const MENUBAR_SNAPSHOT_KEYS = snapshotKeys( "orientation", "modal", "disabled", "loopFocus", "hasSubmenuOpen", );