import type { JSX } from "@solidjs/web"; /** * `` — Solid 2 port of Base UI React's `DialogRoot` * (`packages/react/src/dialog/root/DialogRoot.tsx`). A thin wrapper * around `useDialogRootInternal` (shared with ``). */ import { createControlledState } from "../../internals/create-controlled-state"; import { FloatingTree, useFloatingParentNodeId, } from "../../internals/floating-ui/components/floating-tree"; import { type MaybeAccessor, readOption } from "../../internals/maybe-accessor"; import { createDialog } from "../create-dialog"; import type { DialogModalKind, DialogRootChangeEventDetails, DialogRootChangeEventReason, } from "../dialog-root-context"; import { useDialogRootContext } from "../dialog-root-context"; import type { DialogHandle } from "../store/dialog-store"; import { type DialogPayloadChildRenderFunction, useDialogRootInternal, } from "./use-dialog-root-internal"; export type { DialogModalKind, DialogPayloadChildRenderFunction, DialogRootChangeEventDetails, DialogRootChangeEventReason, }; /** * Imperative actions surface written into `actionsRef` on ``. * Mirrors React's `DialogRoot.Actions`. */ export interface DialogRootActions { /** Close the dialog imperatively. */ close: () => void; /** * Force-unmount the dialog subtree. Useful when a consumer's * animation library controls unmount timing and the dialog needs to * be dropped after the animation finishes — pair with * `onOpenChange({ preventUnmountOnClose })`. */ unmount: () => void; } export interface DialogRootProps { /** * Controlled open state. When provided (resolved value not * `undefined`), the consumer owns the state — pair with * {@link onOpenChange}. When omitted, the dialog manages its own open * state (seeded from {@link defaultOpen}). */ open?: MaybeAccessor; /** * Initial open state when uncontrolled. Read once at mount. * @default false */ defaultOpen?: MaybeAccessor; /** * Modal kind. `true` (default) = focus-trap + scroll-lock + outside * pointer block. `false` = neither. `"trap-focus"` = focus trap only. * @default true */ modal?: MaybeAccessor; /** Fires on every open/close request. */ onOpenChange?: | ((open: boolean, eventDetails: DialogRootChangeEventDetails) => void) | undefined; /** * Fires once the open/close transition has settled. Called with the * new `open` value. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * When true, outside-press dismissal is disabled. Escape + Close + * imperative close still work. * @default false */ disablePointerDismissal?: MaybeAccessor; /** * Ref (callback or `{ current }`) that receives the imperative * actions object on mount. */ actionsRef?: | ((actions: DialogRootActions | null) => void) | { current: DialogRootActions | null } | undefined; /** * Handle to connect with detached `` * components or to control the dialog imperatively. */ handle?: DialogHandle | undefined; /** Controlled active trigger id (multi-trigger setups). */ triggerId?: string | null | undefined; /** * Children — plain JSX or `({ payload }) => JSX.Element` render * function. */ children?: JSX.Element | DialogPayloadChildRenderFunction; } function DialogRootComponent( props: DialogRootProps, ): JSX.Element { // Controlled / uncontrolled open state (Base UI `open` / `defaultOpen` // contract): the `open` prop wins while its resolved value is defined; // otherwise an internal signal (seeded once from `defaultOpen`) backs // the state and the root's `setOpen` writes it. const [resolvedOpen, setUncontrolledOpen] = createControlledState({ controlled: () => { const controlled = readOption(props.open); return controlled === undefined ? undefined : Boolean(controlled); }, default: () => readOption(props.defaultOpen), fallback: false, name: "", state: "open", }); // Compose the headless Dialog primitive — owns `open`, `modal`, and // `disablePointerDismissal`. The wrapper layers floating-UI, focus // trap, scroll lock, portal, and animation on top. `onOpenChange` is // NOT wired into the primitive — the wrapper's own `setOpen` is the // sole writer so it can thread reason/event details through the full // floating-UI pipeline. const dialogPrimitive = createDialog({ open: resolvedOpen, modal: () => readOption(props.modal) ?? true, disablePointerDismissal: () => readOption(props.disablePointerDismissal) ?? false, }); return useDialogRootInternal(props, { role: "dialog", api: dialogPrimitive, setUncontrolledOpen, }); } /** * Groups all parts of the dialog. Doesn't render its own HTML element. */ export function DialogRoot( props: DialogRootProps, ): JSX.Element { if (useDialogRootContext(true) != null || useFloatingParentNodeId() != null) { return ; } return ( ); }