import { untrack } from "solid-js"; import type { FocusableElement } from "./tabbable"; interface Options { preventScroll?: boolean | undefined; cancelPrevious?: boolean | undefined; sync?: boolean | undefined; } const NOOP = () => {}; // Module-scoped so consecutive calls with `cancelPrevious: true` share a slot. // Matches the React source — the cancellation is cross-call-site by design. let rafId = 0; /** * Focus `el` on the next animation frame (or synchronously when `sync` * is true). Returns a cancellation function that only cancels the frame * it scheduled, so "stale" cancellations from later calls are no-ops. * * `cancelPrevious` (default `true`) cancels any prior RAF scheduled by * this module, which is the standard "focus the latest element" pattern * when multiple focus requests queue up in the same tick. */ export function enqueueFocus( el: FocusableElement | null, options: Options = {}, ): () => void { const { preventScroll = false, cancelPrevious = true, sync = false, } = options; if (cancelPrevious) { cancelAnimationFrame(rafId); } // `focus()` dispatches focus events synchronously; keep their handlers out // of the caller's reactive scope (e.g. a sync focus from an effect). const exec = () => untrack(() => el?.focus({ preventScroll })); if (sync) { exec(); return NOOP; } const currentRafId = requestAnimationFrame(exec); rafId = currentRafId; return () => { if (rafId === currentRafId) { cancelAnimationFrame(currentRafId); rafId = 0; } }; }