import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, createMemo, useContext } from "solid-js"; import { createPopupCore } from "../internals/create-popup-core"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; /** * Why the drawer's open state changed. * * - `"trigger-press"` — trigger was clicked or keyboard-activated. * - `"swipe"` — user swipe-dismissed the drawer. * - `"escape-key"` — Escape dismissed the drawer. * - `"outside-press"` — pointer pressed outside. * - `"close-button"` — `` was pressed. * - `"close-watcher"` — Android CloseWatcher back-gesture. * - `"imperative"` — `dr.setOpen()` called programmatically. */ export type DrawerChangeReason = | "trigger-press" | "swipe" | "escape-key" | "outside-press" | "close-button" | "close-watcher" | "imperative"; import type { DrawerSnapPoint, DrawerSwipeDirection, } from "./root/drawer-root-context"; export type { DrawerSnapPoint, DrawerSwipeDirection }; /** * Options for {@link createDrawer}. */ export interface CreateDrawerOptions { /** * Open state. Consumer-owned signal. */ open: MaybeAccessor; /** * Whether interaction is suppressed. * @default false */ disabled?: MaybeAccessor; /** * Modal surface kind (drawer is modal by default). * @default true */ modal?: MaybeAccessor; /** * When true, outside-press dismissal is disabled. * @default false */ disablePointerDismissal?: MaybeAccessor; /** * Direction the drawer slides from. * @default "down" */ swipeDirection?: MaybeAccessor; /** * Snap points list. When provided the drawer can be dragged to each point. */ snapPoints?: MaybeAccessor; /** * Active snap point. Consumer-owned — pair with `onSnapPointChange`. */ snapPoint?: MaybeAccessor; /** * Pre-change callback fired via `runWithOwner(null, ...)`. */ onOpenChange?: ( open: boolean, event: Event, reason: DrawerChangeReason, ) => void; /** * Fires on every snap-point change request. */ onSnapPointChange?: (snapPoint: DrawerSnapPoint | null) => void; } /** * Public API returned by {@link createDrawer}. */ export interface DrawerApi { /** Reactive accessor for the current open state. */ open: Accessor; /** Whether interaction is suppressed. */ disabled: Accessor; /** Modal surface kind. */ modal: Accessor; /** Whether outside-press dismissal is disabled. */ disablePointerDismissal: Accessor; /** Direction the drawer slides from. */ swipeDirection: Accessor; /** Active snap point accessor. */ snapPoint: Accessor; /** Snap points list. */ snapPoints: Accessor; /** Imperative open setter. */ setOpen: (next: boolean, event?: Event, reason?: DrawerChangeReason) => void; /** Flip open (no-op when disabled). */ toggle: (event?: Event, reason?: DrawerChangeReason) => void; /** Imperative snap-point setter. Fires `onSnapPointChange`. */ setSnapPoint: (snapPoint: DrawerSnapPoint | null) => void; /** Context provider. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const DrawerContext = createContext(null); /** * Read the nearest {@link DrawerApi}. Throws unless `optional` is true. */ export function useDrawer(): DrawerApi; export function useDrawer(optional: true): DrawerApi | null; export function useDrawer(optional?: boolean): DrawerApi | null { const ctx = useContext(DrawerContext); if (ctx == null && !optional) { throw new Error( "Base UI: useDrawer() must be called inside a .", ); } return ctx; } /** * Headless primitive for a drawer compound. Modal slide-in panel with swipe * dismissal and snap-point control. DOM machinery lives in ``. */ export function createDrawer(options: CreateDrawerOptions): DrawerApi { const core = createPopupCore({ open: options.open, disabled: options.disabled, onOpenChange: options.onOpenChange, }); const modal = createMemo( () => readOption(options.modal) ?? true, { name: "drawer.modal" }, ); const disablePointerDismissal = createMemo( () => Boolean(readOption(options.disablePointerDismissal)), { name: "drawer.disablePointerDismissal" }, ); const swipeDirection = createMemo( () => (readOption(options.swipeDirection) ?? "down") as DrawerSwipeDirection, { name: "drawer.swipeDirection" }, ); const snapPoint = createMemo( () => readOption(options.snapPoint), { name: "drawer.snapPoint" }, ); const snapPoints = createMemo( () => readOption(options.snapPoints), { name: "drawer.snapPoints" }, ); const setSnapPoint = (next: DrawerSnapPoint | null): void => { options.onSnapPointChange?.(next); }; const api: DrawerApi = { open: core.open, disabled: core.disabled, modal, disablePointerDismissal, swipeDirection, snapPoint, snapPoints, setOpen: core.setOpen, toggle: core.toggle, setSnapPoint, Provider: (props) => ( {props.children} ), }; (api as DrawerApi & Snapshottable)[SNAPSHOT_KEYS] = DRAWER_SNAPSHOT_KEYS; return api; } const DRAWER_SNAPSHOT_KEYS = snapshotKeys( "open", "disabled", "modal", "disablePointerDismissal", "swipeDirection", "snapPoint", );