/** * PopoverStore — thin Solid 2 port of Base UI React's `PopoverStore` * (`packages/react/src/popover/store/PopoverStore.ts`). * * The React store carries the full popover state machine (open, modal, * mounted, transition, floating root context, trigger map, title id, * description id, stick-if-open timer, nested flag, etc.) and exposes it * through a selector API consumed by every sub-part. The Solid port uses * the ``'s local signals as the source of truth for * open/mount/transition-status; this store is a _thin_ imperative-API * wrapper that multiplies the minimal subset the handle exposes * (`open` / `close` / `isOpen` / trigger registry). * * Concretely: * * - Holds a `PopupTriggerMap` so `` can register * itself and `PopoverHandle.open(id)` can resolve the correct trigger. * - Holds the current `activeTriggerId` / `activeTriggerElement` so the * implicit-active-trigger logic behaves across multi-trigger setups. * - Exposes `setOpen(next, details)` which is the writer the `` * binds via `actionsRef` / `PopoverHandle`. */ import { createChangeEventDetails, REASONS, } from "../../internals/change-event-details"; import { PopupTriggerMap } from "../../internals/floating-ui/popup-trigger-map"; import type { PopoverRootChangeEventDetails, PopoverRootChangeEventReason, } from "../popover-root-context"; /** * Writer signature for `setOpen`. Matches the Solid root's internal * `setOpen(next, details)` callback. */ export type PopoverStoreSetOpen = ( next: boolean, details: PopoverRootChangeEventDetails, ) => void; /** * Minimal store shape consumed by the handle + imperative callers. * * Matches how the React source's `PopoverStore` is consumed — only * `.context.triggerElements` and `.setOpen` are read externally through * the handle. */ export class PopoverStore<_Payload = unknown> { /** * Registry of triggers keyed by id. `` adds * itself here on mount, and `PopoverHandle.open(id)` / * the implicit-active-trigger logic looks up elements out of it. */ public readonly context: { triggerElements: PopupTriggerMap }; /** * Internal setOpen slot. `` binds this during setup via * {@link attachSetOpen} so the handle's `open()` / `close()` calls * reach the root's writer. */ private _setOpen: PopoverStoreSetOpen | null = null; /** * Last-seen reactive open flag. Used by `PopoverHandle.isOpen` so the * caller can read synchronously without needing a subscription hook. */ private _open = false; constructor() { this.context = { triggerElements: new PopupTriggerMap(), }; } /** * Bind the root's `setOpen` writer. Called by `` during * setup; clears on teardown. * * @internal */ attachSetOpen(fn: PopoverStoreSetOpen | null): void { this._setOpen = fn; } /** * Mirror the root's current open flag. Called by ``'s * effect so `handle.isOpen` returns the latest value synchronously. * * @internal */ setOpenMirror(next: boolean): void { this._open = next; } /** * Imperative open-state writer. Mirrors React's `PopoverStore.setOpen`. * * If the root is not currently mounted (handle was constructed but * never used inside a live ``), this is a no-op. */ setOpen(next: boolean, details: PopoverRootChangeEventDetails): void { this._setOpen?.(next, details); } /** * Synchronous snapshot of the current open state — does NOT subscribe * to Solid's reactive graph. For reactive reads, consumers use the * root context's `open()` accessor. */ get isOpen(): boolean { return this._open; } } /** * `PopoverHandle` — imperative + cross-root trigger-association surface. * Matches Base UI React's `PopoverHandle` 1:1 on the public methods * (`open(triggerId)` / `close()` / `isOpen`) and carries an internal * store reference that `` adopts. */ export class PopoverHandle { /** * Internal store holding the popover's state. * @internal */ public readonly store: PopoverStore; constructor() { this.store = new PopoverStore(); } /** * Opens the popover, associating it with the trigger registered under * `triggerId`. Throws if no trigger with that id is registered. */ open(triggerId: string): void { const triggerElement = this.store.context.triggerElements.getById( triggerId, ) as HTMLElement | undefined; if (!triggerElement) { throw new Error( `Base UI: PopoverHandle.open: No trigger found with id "${triggerId}".`, ); } this.store.setOpen( true, createChangeEventDetails( REASONS.imperativeAction as PopoverRootChangeEventReason, undefined, triggerElement, ) as PopoverRootChangeEventDetails, ); } /** * Closes the popover. */ close(): void { this.store.setOpen( false, createChangeEventDetails( REASONS.imperativeAction as PopoverRootChangeEventReason, undefined, undefined, ) as PopoverRootChangeEventDetails, ); } /** * Whether the popover is currently open. Synchronous — mirrors the * root's most recent open-state write. */ get isOpen(): boolean { return this.store.isOpen; } } /** * Factory for `PopoverHandle` — matches React's `createPopoverHandle()`. */ export function createPopoverHandle< Payload = unknown, >(): PopoverHandle { return new PopoverHandle(); }