feat(useDelay): add new composable#224
Merged
Merged
Conversation
`duration` is now read fresh on every `start()` via `toValue`, so consumers can pass a ref or getter and the timer picks up the new value without recreating the instance. Plain `number` continues to work — the change is a strict superset. Enables `useDelay` to compose `useTimer` directly with a reactive duration that switches between `openDelay` / `closeDelay` based on direction.
Schedules deferred open and close transitions with configurable
delays. Composes `useTimer` for the underlying lifecycle and adds
direction tracking plus promise-based resolution.
API:
- `start(isOpening, { minDelay? })` — schedule a transition; restarts
if already running and resolves any prior promise with the new
direction
- `stop` / `pause` / `resume` — forwarded from the underlying timer
- `isActive` / `isPaused` / `remaining` / `isOpening` — reactive state
Covers tooltip, popover, and overlay use cases that previously
required ad-hoc timer logic. Zero-delay paths fire synchronously to
preserve the behavior of pre-existing callers (e.g. instant focus
opens).
The documented import-section order didn't match what `perfectionist/sort-imports` actually enforces. Update the canonical example, the section list, and the checklist to reflect the real order: Components, Composables, Adapters, Utilities (Vue lives here), Transformers, Types, Globals (#v0/constants/globals — sorted last as the `internal` group, with an author-added section comment for consistency). Run `pnpm lint:fix`; never hand-author the order.
Addresses bugs and pattern violations surfaced by /inspect --thorough: - Resolve `pendingResolve` on `stop()` and `onScopeDispose` so awaiters never hang - Resolve the promise before invoking `onChange` so a thrown callback doesn't strand the resolution - Settle `previousResolve` outside the new Promise executor to remove a re-entrancy window in the synchronous zero-delay path - Wrap forwarded `isActive` / `isPaused` / `remaining` with `shallowReadonly()` so the runtime matches the declared `Readonly<Ref<>>` contract - Narrow `openDelay` / `closeDelay` to `MaybeRefOrGetter<number>`; drop the `Number(raw) || 0` coercion that masked NaN inputs - Extract `UseDelayStartOptions` interface and rename the `start()` options arg to `options` per project convention - Demote internal `minDelay` from a `shallowRef` to a plain `let` - Add `@example` JSDoc on `UseDelayReturn` showing destructure usage Docs: - Simplify the Mermaid lifecycle diagram (remove dangling `Pending` node) - Add a `## FAQ` section per the composable page structure - Expand the Examples block with multi-paragraph prose and a File/Role table; clarify the scope-disposal resolution semantic - Reorder sections so FAQ precedes `<DocsApi />` - Reactivity table references the new `UseDelayStartOptions` interface Example: - Drop the dead `target.value === 0` ternary
|
commit: |
- pendingResolve → resolver
- resolveDuration → duration (enables { duration } shorthand to useTimer)
- previousResolve → previous
Aligns useDelay with the established callback-position precedent across
8 sibling composables (useTimer, useRaf, useEventListener,
useMutationObserver, useIntersectionObserver, useResizeObserver,
useClickOutside, useHotkey). Burying onChange inside the options bag
made useDelay the only outlier; the new shape matches Vuetify's
useDelay and the broader v0 muscle memory.
Before:
useDelay({ openDelay: 300, closeDelay: 200, onChange: v => ... })
After:
useDelay(v => ..., { openDelay: 300, closeDelay: 200 })
Codifies the rule in .claude/rules/composables.md as
\`Callback Argument Position\`.
Progress.Root is form-stateful (v-model + ARIA progressbar) and was crashing here with a getter-set-on-readonly error against the docs build. The countdown bar is decorative — no form value, no a11y progressbar role needed — so a plain div is the right fit. Button.Root remains since native button → v0 Button is the correct form-control swap. Also updates the Examples prose from "800 ms / 600 ms" to "2000 ms / 1500 ms" to match the actual example.
The ::: faq container parses each ??? question as a separate item, but only when a blank line separates the question from its answer. Without the blank line, the answer collapses into the question text and the dropdown renders empty.
31 tests across 11 categories: - basic lifecycle (open/close fire, promise resolution, no-callback path, default zero-delay) - reactive delays (number, ref, getter; mid-flight changes don't affect in-flight) - minDelay floor (greater-than, less-than, reset between calls) - re-entrancy (previous promise resolves with new direction; in-flight timer cancels on restart) - stop (cancels delay, resolves pending with current isOpening, no callback fired, no-op when idle) - pause / resume (preserves remaining, resumes from pause point, isOpening unchanged through cycle) - synchronous fire (zero delay, negative delay, cancels in-flight on back-to-back start) - isOpening tracking (direction reflects start arg, readonly contract) - callback throw safety (promise still resolves) - auto-cleanup (timer cleared on scope dispose, pending promise settled) - remaining tracking (decreases over time, 0 after fire) Zero stderr output. Vue fake-timers + effectScope for lifecycle tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
useDelay— schedules deferred open / close transitions with configurable delays. ComposesuseTimerfor the underlying lifecycle and adds direction tracking plus promise-based resolution.useTimer.durationto acceptMaybeRefOrGetter<number>so the timer can be driven by a reactive source. Strict superset; existing plain-number consumers are unaffected.Import Section Orderrule in.claude/rules/composables.mdwith whatperfectionist/sort-importsactually enforces.API
Notes
ms <= 0) fires synchronously; preserves behavior for instant-focus / skip-window callerspreview,since: null(set on first release)useTimerconsumers verified unaffected by the duration widening