/** * `DialogStore` — thin Solid 2 port of Base UI React's `DialogStore` * (`packages/react/src/dialog/store/DialogStore.ts`). * * Mirrors `PopoverStore` from the popover compound: the actual state * machine lives on ``'s local signals (open / mount / * transition status / interactions). This store is a thin imperative-API * wrapper that exposes: * * - a `PopupTriggerMap` so `` can register * itself and `DialogHandle.open(id)` can resolve the trigger element. * - `setOpen(next, details)` — the writer `` binds via * `attachSetOpen` during setup. The handle's `open()` / `close()` / * `openWithPayload()` all route through this. * - `setPayload(payload)` — same deal, for the payload slot. * - Synchronous `isOpen` read for imperative callers who don't want to * subscribe to reactivity. */ import { createChangeEventDetails, REASONS, } from "../../internals/change-event-details"; import { PopupTriggerMap } from "../../internals/floating-ui/popup-trigger-map"; import type { DialogRootChangeEventDetails, DialogRootChangeEventReason, } from "../dialog-root-context"; export type DialogStoreSetOpen = ( next: boolean, details: DialogRootChangeEventDetails, ) => void; export type DialogStoreSetPayload = ( payload: Payload | undefined, ) => void; /** * Internal store shape for ``. Public surface via * `DialogHandle` is stable; everything else is `@internal`. */ export class DialogStore<_Payload = unknown> { /** * Trigger registry. `` adds itself on mount; * `DialogHandle.open(id)` looks up the element to seed the change * details' `trigger` slot. */ public readonly context: { triggerElements: PopupTriggerMap }; private _setOpen: DialogStoreSetOpen | null = null; private _setPayload: DialogStoreSetPayload<_Payload> | null = null; private _open = false; constructor() { this.context = { triggerElements: new PopupTriggerMap(), }; } /** @internal */ attachSetOpen(fn: DialogStoreSetOpen | null): void { this._setOpen = fn; } /** @internal */ attachSetPayload(fn: DialogStoreSetPayload<_Payload> | null): void { this._setPayload = fn; } /** @internal */ setOpenMirror(next: boolean): void { this._open = next; } /** * Imperative open-state writer. 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: DialogRootChangeEventDetails): void { this._setOpen?.(next, details); } /** Imperative payload writer. No-op when no root is bound. */ setPayload(payload: _Payload | undefined): void { this._setPayload?.(payload); } /** Synchronous snapshot — does NOT subscribe to Solid's reactive graph. */ get isOpen(): boolean { return this._open; } } /** * `DialogHandle` — imperative + cross-tree trigger-association surface. * * Matches Base UI React's `DialogHandle` on the public method surface * (`open(triggerId)` / `openWithPayload(payload)` / `close()` / `isOpen`) * and carries an internal store that `` adopts. * * Usage: * * ```ts * const handle = Dialog.createHandle(); * * // ... inside the component * * Open * {/* ... *\/} * * * // Later, from outside: * handle.open("t1"); * handle.openWithPayload({ title: "Hello" }); * handle.close(); * ``` */ export class DialogHandle { /** @internal */ public readonly store: DialogStore; constructor(store?: DialogStore) { this.store = store ?? new DialogStore(); } /** * Opens the dialog. If `triggerId` is provided and a trigger with * that id is registered, the dialog is associated with it (focus * returns there on close; `aria-labelledby` / positioning downstream * anchors against it). A missing `triggerId` still opens the dialog * but skips the association. */ open(triggerId: string | null): void { const triggerElement = triggerId !== null ? (this.store.context.triggerElements.getById(triggerId) as | HTMLElement | undefined) : undefined; if (process.env.NODE_ENV !== "production") { if (triggerId !== null && !triggerElement) { console.warn( `Base UI: DialogHandle.open: No trigger found with id "${triggerId}". The dialog will open, but the trigger will not be associated with the dialog.`, ); } } this.store.setOpen( true, createChangeEventDetails< DialogRootChangeEventReason, { preventUnmountOnClose: () => void } >( REASONS.imperativeAction as DialogRootChangeEventReason, undefined, triggerElement, { preventUnmountOnClose: () => {} }, ) as DialogRootChangeEventDetails, ); } /** * Opens the dialog and sets the payload. Does NOT associate the * dialog with any trigger — the render-function children receive * `{ payload }` but there's no anchor element. */ openWithPayload(payload: Payload): void { this.store.setPayload(payload); this.store.setOpen( true, createChangeEventDetails< DialogRootChangeEventReason, { preventUnmountOnClose: () => void } >( REASONS.imperativeAction as DialogRootChangeEventReason, undefined, undefined, { preventUnmountOnClose: () => {} }, ) as DialogRootChangeEventDetails, ); } /** * Closes the dialog. */ close(): void { this.store.setOpen( false, createChangeEventDetails< DialogRootChangeEventReason, { preventUnmountOnClose: () => void } >( REASONS.imperativeAction as DialogRootChangeEventReason, undefined, undefined, { preventUnmountOnClose: () => {} }, ) as DialogRootChangeEventDetails, ); } /** Synchronous snapshot of the open state. */ get isOpen(): boolean { return this.store.isOpen; } } /** Factory for `DialogHandle` — matches React's `createDialogHandle()`. */ export function createDialogHandle(): DialogHandle { return new DialogHandle(); }