import { type Accessor, createEffect } from "solid-js"; import { createStableCallback } from "../stable-callback"; import { useAnimationsFinished } from "../use-animations-finished"; /** * Plain `{ current }` ref shape that mirrors React's `RefObject`. Kept structurally identical to the other `MutableRef` * declarations in this codebase (see `use-latest-ref.ts`) so consumers can * pass the same object interchangeably. */ export interface MutableRef { current: T; } export interface UseOpenChangeCompleteParameters { /** * Whether the hook is enabled. May be a plain boolean or a reactive * accessor. Defaults to `true`. */ enabled?: boolean | Accessor | undefined; /** * Whether the element is open. Must be a reactive accessor — Solid has no * re-render cycle, so changes arrive via signal reads. */ open?: Accessor; /** * Ref to the element being closed. Its `.current` is read at * invocation time, so a ref that is `null` during setup but populated * before the first `open` change still works correctly. */ ref: MutableRef; /** * Function to call when the animation completes (or there is no * animation). Stabilized internally via `createStableCallback` so * consumers may swap the function freely — the latest closure wins at * firing time. */ onComplete: () => void; } /** * Calls the provided function when the CSS open/close animation or * transition on the referenced element completes. * * Port of Base UI React's * `packages/react/src/internals/useOpenChangeComplete.tsx`. The React * version re-runs its effect every time `open`, `enabled`, or the * stabilized `onComplete` changes — because the effect's dependency * array includes those inputs. The Solid equivalent uses a * `createEffect` split-form that tracks the same reactive inputs: each * change re-fires the apply, which aborts any prior in-flight wait and * schedules a fresh `runOnceAnimationsFinish` cycle. * * The `ref` is intentionally not reactive — matching React, which reads * `ref.current` lazily inside `useAnimationsFinished`'s closure. If the * ref populates after setup, the next `open` change will pick it up. * * @example * ```ts * const [open, setOpen] = createSignal(false); * let el!: HTMLDivElement; * useOpenChangeComplete({ * open, * ref: { get current() { return el; }, set current(v) { el = v!; } }, * onComplete: () => setMounted(false), * }); * ``` */ export function useOpenChangeComplete( parameters: UseOpenChangeCompleteParameters, ): void { const { enabled = true, open, ref, onComplete: onCompleteParam } = parameters; const isEnabled = (): boolean => typeof enabled === "function" ? enabled() : enabled; // Stabilize the consumer-provided callback. `createStableCallback` expects // a getter so the trampoline can forward to the latest reference at call // time; we bind to `onCompleteParam` captured in this closure — in Solid // the hook runs once per component, so this getter doesn't need to // re-resolve across renders (there are no renders), it just keeps the // identity stable for downstream captures. const onComplete = createStableCallback<() => void>(() => onCompleteParam); // Resolve the element via the existing ref on each invocation. // `useAnimationsFinished` accepts an `ElementSource` which can be an // accessor that returns an element — a ref-reader accessor fits exactly. const runOnceAnimationsFinish = useAnimationsFinished(() => ref.current); // Mirror React's `useEffect` with deps [enabled, open, onComplete, // runOnceAnimationsFinish]. The stable-callback trampolines don't // change, so the live deps are effectively `isEnabled()` and `open()`. // On each apply, abort any outstanding wait cycle (to cancel a stale // close wait when a rapid re-open arrives) and schedule a fresh one. createEffect( () => ({ enabled: isEnabled(), openValue: open?.(), }), (deps) => { if (!deps.enabled) { return undefined; } const abortController = new AbortController(); runOnceAnimationsFinish(onComplete, abortController.signal); return () => { abortController.abort(); }; }, ); }