Skip to content

OtpInput: fix click-to-focus and overwrite-on-retype#42524

Open
mdo wants to merge 1 commit into
v6-devfrom
mdo/otp-input-focus-fix
Open

OtpInput: fix click-to-focus and overwrite-on-retype#42524
mdo wants to merge 1 commit into
v6-devfrom
mdo/otp-input-focus-fix

Conversation

@mdo

@mdo mdo commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

The single-input OTP rewrite (#42500) improved accessibility but left two interaction gaps reported after merge:

  1. You couldn't click a slot to focus it. The slots had pointer-events: none and focus always jumped to the end of the value, so the click position was ignored.
  2. Retyping a digit shifted the others along. A native <input> inserts, so editing mid-value pushed the remaining digits down. OTP entry expects overwrite.

Both stem from the same thing: a single, full-width, centered input can't map a click to a slot, and native editing inserts rather than overwrites.

Approach

Keep the single accessible <input> (preserving the one-announced-field, autocomplete="one-time-code", SMS autofill, password-manager, and formatted-paste wins) and make its interaction faithful to the input-otp model:

  • Active slot = a selection range[i, i+1] on a filled slot so the next keystroke overwrites it, [i, i] on an empty one so it appends.
  • beforeinput handler intercepts single-character typing (overwrite + advance) and backspace (clear / step back). Paste, SMS autofill, and IME composition still fall through to the existing bulk input path.
  • Clickable slots — a pointerdown handler focuses the input and positions the caret on the clicked slot, clamped to the first empty slot.
  • Sane focus — Tab/focus() land on the first empty slot instead of the absolute end; a document selectionchange listener keeps the active-slot highlight in sync with the caret.

No public API or event changes.

Testing

  • npm run js-test-karma1097/1097 pass, including 5 new interaction tests (click-to-position, overwrite-not-insert, backspace, Tab → first-empty, disallowed-char swallowed).
  • eslint + stylelint clean.

Built dist/ is intentionally not included (CI builds it, consistent with other PRs on this branch).

Note

Unit tests cover the core logic, but real cross-browser/mobile behavior (iOS Safari focus with pointer-events: none, Android GBoard beforeinput, IME composition) is worth a quick manual pass on the forms/otp-input docs page before merge. The code guards for these (single-char insertText only; composition falls through).

The single-input rewrite left two interaction gaps: clicking a slot
didn't position the caret there (slots had pointer-events: none and
focus always jumped to the end), and retyping inserted instead of
overwriting, so preceding digits shifted along.

Keep the single accessible input but make its interaction faithful to
the input-otp model:

- Represent the active slot as a selection range so the next keystroke
  overwrites a filled slot or appends to an empty one
- Intercept single-char typing and backspace via beforeinput for
  overwrite semantics; paste/autofill/IME still flow through input
- Make slots clickable (pointerdown) to position the caret, clamped to
  the first empty slot
- Land focus on the first empty slot instead of the end; track the caret
  with a document selectionchange listener
@mdo mdo requested review from a team as code owners June 18, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant