Skip to content

feat(Otp): add Otp compound component over createOtp - #787

Open
sridhar-3009 wants to merge 6 commits into
vuetifyjs:devfrom
sridhar-3009:feat/otp-component-519
Open

feat(Otp): add Otp compound component over createOtp#787
sridhar-3009 wants to merge 6 commits into
vuetifyjs:devfrom
sridhar-3009:feat/otp-component-519

Conversation

@sridhar-3009

Copy link
Copy Markdown
Contributor

Closes #519.

createOtp has shipped a complete, tested headless primitive for one-time-password / verification-code values since 1.0.0-alpha.5 (pattern-gated entry, completion detection, decisional async onComplete), but there was no Vue component wrapping it — consumers had to hand-roll rendering, focus management, and keyboard/paste wiring themselves.

What's added

Otp.Root, Otp.Item, Otp.HiddenInput — same Root/Item/HiddenInput shape as Rating and Slider:

  • Otp.Root creates the createOtp context, bridges v-model, renders a role="group" container with a locale-driven default aria-label ("Verification code"), and tracks each Item's element so focus can move between boxes.
  • Otp.Item renders one character box (<input maxlength="1" autocomplete="one-time-code"> by default — autocomplete="one-time-code" is the documented convention that lets mobile OSes offer SMS-code autofill). Handles:
    • auto-advance to the next box when a pattern-accepted character is typed
    • Backspace on an empty box moves focus back and clears the previous box (native Backspace on a filled box just clears it in place, standard OTP UX)
    • ArrowLeft/ArrowRight move focus directly between boxes
    • paste distributes the clipboard text across boxes starting at the pasted-into box (via createOtp's distribute), since maxlength would otherwise silently truncate a multi-character paste to 1 char
    • each box gets its own aria-label ("Digit N of length")
  • Otp.HiddenInput mirrors Rating.HiddenInput — auto-rendered when name is set, for native form submission.

Registered in the advisory a11y sweep (packages/0/src/components/fixtures/Otp.vue) — clean, no axe violations. Added the two new locale keys (Otp.label, Otp.itemLabel) to the en catalog per the static locale-coverage check. Bumped Otp's maturity.json entry from draft to preview.

Tests

23 tests in Otp/index.test.ts covering rendering, v-model (including auto-advance, backspace-back, arrow-key nav, paste distribution, and the beforeinput pattern gate), disabled/readonly no-ops, the name/hidden-input path, and onComplete (both the completion signal and async rejection). 94%/98.5% statement/line coverage on the new files, consistent with other components in this codebase.

Full suite: 4819 non-browser tests pass, 54 a11y-sweep browser tests pass, typecheck/knip/sherif clean.

@johnleider
johnleider changed the base branch from master to dev August 20, 2026 14:18
@johnleider johnleider added this to the v1.3.0 milestone Aug 20, 2026
…ct paste focus

- Wrap length/pattern as getters into createOtp and expose pattern as a
  Ref on the context — bare destructured props froze them at mount
  (Rating precedent), leaving inputmode and accepts() stale after a
  pattern prop change.
- Re-sync the box DOM when a keystroke is rejected by the pattern or
  arrives while disabled/readonly/validating — the model doesn't change,
  so Vue skips the patch and the stale character lingered in the input.
- Focus the next empty box after paste from distribute's start + written
  count — index + written overshot when the paste landed before index on
  a shorter value.
- Route the Atom template ref through AtomExpose + toElement, track item
  elements as Element, and cast to HTMLElement only at the focus site.
- aria-disabled on the group is a concrete boolean per the house ARIA
  contract; maturity since starts null until a release ships it.
…ked-input sync

Adds regression coverage for the branches codecov flagged: paste
filtering/truncation/next-empty-box focus and the disabled guard, arrow
navigation clamped at both ends, backspace no-op on the first box,
alphanumeric/custom-RegExp/reactive pattern handling, DOM re-sync of
rejected and validation-locked keystrokes, single-fire and refire of
onComplete, hidden input value/disabled syncing, and the exposed
focusItem clamp.
@johnleider johnleider added the enhancement New feature or request label Aug 20, 2026
Covers the exact lines codecov flagged: the readonly side of the
disabled/readonly guards in beforeinput, keydown, and paste; backspace
on a filled box deferring to the native input; unrelated keys falling
through the keydown handler; paste without clipboard data; and the
renderless slot path driving a consumer-rendered input through slot
attrs. OtpItem now measures 100% statements/branches/lines locally.
- Declare the explicit 'update:model-value' emit alongside defineModel
  (vue-devtools event tracking; Input/Slider/Switch precedent).
- Type the Root's imperative surface as an exported OtpRootExpose
  interface (Splitter precedent) and re-export it from the barrel.
- Single-word locals per PHILOSOPHY 3.3: charValue -> char, with the
  typed character in onInput renamed to entered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Otp component (over createOtp)

2 participants