feat(createOtp): add headless OTP / verification-code composable#238
Open
johnleider wants to merge 23 commits into
Open
feat(createOtp): add headless OTP / verification-code composable#238johnleider wants to merge 23 commits into
johnleider wants to merge 23 commits into
Conversation
Adds the createOtp directory, barrel export, and maturity entry. The composable returns the documented context shape but the mutation helpers are placeholders that subsequent commits flesh out per the locked design spec.
Satisfies the "Per-symbol @example (100% enforced)" rule from .claude/rules/composables.md — the interfaces render in <DocsApi /> and require their own example blocks.
Compiles string presets ('numeric', 'alphanumeric', 'alphabetic')
lazily and applies arbitrary RegExp patterns per character. Reactive
through MaybeRefOrGetter. A dev-mode useLogger warn fires once per
RegExp that matches multi-character input.
The WeakSet<RegExp> cache ensures the multi-char warn fires exactly once per pattern instance. Assert toHaveBeenCalledTimes(1) so the no-spam guarantee is locked by the test, not just the implementation.
Replaces typeof resolved === 'string' with isString(resolved) per the project type-guard rule, and tightens the multi-character warn test to assert the message content alongside the call count.
setAt overwrites a single position, truncates-from-here when char is empty, and takes the first character of multi-char input (consumers use paste for distribution). paste filters through accepts, splices at the index, and clips to length, returning the consumed count. fill replaces the joined value through the same filter+clip path.
isComplete derives through toRef: true when value length equals the configured length and every character matches the active pattern. Mutation helpers short-circuit when disabled or readonly is true.
A watcher on value fires onComplete when isComplete is true on each distinct completed value. Sync false / async resolved false / thrown / rejected promise paths clear the value and set input.error with the v0.otp.rejected message. Async pending state locks mutation helpers until the promise settles. clearRejection() is called synchronously in each mutation helper so error state clears on the next user input.
Resets the lastCompleted edge tracker inside reject() so a user re-entering the same rejected code triggers onComplete on the next cycle. Also routes the watcher's lock check through isLocked() for consistency, and adds a comment marking the input.isValidating follow-up.
Adds spy-based assertions for the two logger.warn paths in the onComplete edge watcher (sync throw, async promise rejection). Also moves the lastCompleted declaration above reject() to remove a latent TDZ forward reference.
Public docs page with the standard composable structure (Usage, Architecture, Patterns, Behavior, Examples, FAQ, DocsApi). The basic example wires six numeric inputs to createOtp, demonstrating setAt with focus advance, Backspace handling, and paste distribution.
Dedents meta list items to column 0 per docs.md, moves DocsPageFeatures between the H1 and intro per the 10-section composable page structure, adds the missing Reactivity section, and expands the Examples prose to multi-paragraph depth covering when to reach for the helper, tradeoffs, and related APIs.
Adds createOtp to the docs Forms table, both package and root READMEs, the vuetify0 SKILL.md decision table, and the REFERENCE.md form handling section.
Implementation, tests (35 passing), docs page, and basic example all ship in this branch — meets the preview promotion criteria from the new-feature-checklist. `since` stays null; the maintainer cutting the next release flips it.
|
commit: |
Single-word verb matches the neighboring put/paste/clear/fill surface
and reads naturally ("put '4' at position 0"). setAt was an escape-
hatch multi-word name; put fits PHILOSOPHY §3.3 cleanly without the
ambiguity carve-out. Safe to flip — createOtp is still preview with
no released consumers.
paste reads as a DOM clipboard-event handler; distribute is a plain string operation with no UI association. The example keeps its onPaste DOM handler (consumer wires the @paste event), now calling otp.distribute(text, index) inside — clarifies the line between component-layer event handlers and composable-layer state ops.
- Use isThenable over instanceof Promise so onComplete handles cross-realm or library-typed promises correctly. - Safe error message extraction so Promise.reject(null) or throw null doesn't crash the rejection handler. - Reset lastCompleted whenever the value drops below isComplete, not just on reject — same-value re-entry after acceptance now refires the completion edge per spec. Also reset synchronously in clear() so back-to-back clear()+fill() without an await tick still refires. - Hoist PRESETS to module scope with /* @__PURE__ */ — per-instance allocation eliminated. - Tighten the public type: Omit error/errorMessages from OtpOptions (factory owns them) and drop the redundant disabled/readonly re-declarations that shadow InputOptions. - Move clearRejection() calls after validation in put/distribute/fill so out-of-range or pattern-rejected mutations don't silently clear the error indicator without making a change. - Test additions covering same-value re-entry, null rejection paths, non-native thenables, reactive length, multi-char accepts, distribute boundary indices, rules passthrough, and input.reset.
The thenable-not-native-Promise test deliberately creates an object with a then method to exercise the isThenable code path. Lint rule flags it as a footgun, which is correct in general but intended here.
input.reset() previously cleared the value but left the rejection error refs (errorRef, errorMessagesRef) set — users would see stale 'v0.otp.rejected' in input.errors after a reset. Now the wrapped reset calls clearRejection first. Test additions: thenable resolving false, mutations unblocked after async accept, message-content assertions on the non-Error throw and reject paths. Sweep await Promise.resolve() to await nextTick() to match the convention used in sibling test files.
…t edges
- OtpContext.value typed as Readonly<Ref<string>>. The mutation helpers
(put/distribute/fill/clear) are the sole supported write path; direct
ref writes via the context surface bypass pattern, length, and lock
invariants. Type-only change — internal mutation paths unchanged.
- fill() now only clears the rejection state when input was empty
(user intent to clear) or at least one character was accepted. An
all-rejected fill (e.g. fill('----') under numeric) preserves the
rejection signal, matching distribute()'s semantic.
- inputContext.reset() now resets lastCompleted alongside the rejection
clear and the underlying input.reset(). Closes the same-tick reset +
refill edge where the value watcher coalesces and skips onComplete
for an identical re-entry.
- warned WeakSet hoisted to module scope with /* @__PURE__ */ so
multi-char-RegExp warns dedup across createOtp instances sharing a
pattern object.
- lastCompleted declaration moved to top of factory body (removes the
forward-reference smell from clear() and the new reset wrapper).
- Tests: pattern change invalidates complete value, clear() no-ops
while pending, fill all-rejected preserves error, fill('') clears
error, input.reset() allows same-value re-fire, length decrease
does NOT fire onComplete (contract documentation), async resolving
undefined leaves value intact.
Round 4 review caught an inconsistency: OtpContext.value was typed Readonly<Ref<string>> but OtpContext.input.value was still writable via InputContext<string>, and the JSDoc explicitly condoned writes through either alias. Type-narrow input to also expose value as Readonly<Ref<string>>, and tighten the JSDoc to remove the "may write through either" carve-out. Internal mutation paths unchanged — the local value ref inside the factory remains Ref<string> for write access through the helpers.
Round 5 inspection caught that the standalone-line /* @__PURE__ */ form was non-functional — Rollup and esbuild only honor the annotation when it's inline before the call expression. Verified the project convention via utilities/helpers.ts:311, instance.ts:15, constants/htmlElements.ts:22, etc. — every other use is inline on a constructor or call. Dropped the annotation from PRESETS entirely (plain object literals are inherently pure to bundlers; no annotation needed). Kept it inline on the new WeakSet<RegExp>() constructor where it actually helps tree-shaking.
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
createOtp, a Layer 2 forms composable wrappingcreateInput<string>with fixed-length OTP/verification-code state.'numeric' | 'alphanumeric' | 'alphabetic' | RegExp, length-based completion detection, and a decisional sync/asynconCompletehook that clears the value and surfacesv0.otp.rejectedon rejection.setAt,paste,clear,fill) no-op whendisabled,readonly, or an asynconCompleteis pending.pastereturns the count consumed so the consumer can advance focus.What's included
packages/0/src/composables/createOtp/— implementation + 35 colocated testspreview/forms/since: null<DocsApi />whitelistcomposables/forms/create-otpwith Architecture, Reactivity, Patterns, Behavior, Examples, and FAQ sectionsSKILL.mddecision-table row andREFERENCE.mdForm Handling subsectionKnown follow-up
input.isValidatingis not yet wired to the internalisPendingref — documented inline in source. Will land alongside anisValidatingoption oncreateInput.