feat(VOtpInput): groups, a11y improvements, emoji#22803
Conversation
|
I have a question about the local translation, since one aria label has been touch did i need to clear all the translation ? |
|
yes to both
additionally
btw. I am not sure if it counts as breaking or not.. would be a shame to push it to v5 |
|
Ok thank i will fix that tomorrow, i can do the French translation since im a native and for the doc i will see if i could updated it properly, i will probably need some feedback about it. |
|
I am curious if we could make it all backward compatible. This would mean we need to preserve the original "otp" key in translations and add a new one "otpInput" or sth. |
d7d3ac3 to
5351f5c
Compare
I’m confused about what the use case would be for keeping the old key. isn't just dead code ? the only part of the component that needs an aria-label is the hidden input, we can add is an aria-description, but the old pattern with index no longer makes much sense here. |
|
Also, I figured out that once the input is selected, it intercepts the hover CSS of VField. The only workaround I found to this is to add an hover prop to VField and add |
Replace the N separate <input> elements with a single hidden <input> overlaying all visual slots, inspired by the input-otp library. - Single real input improves a11y, native autofill (autocomplete="one-time-code"), and mobile keyboard support - Forced 1-char range selection ([i, i+1]) drives the active-slot highlight via onSelectionChange; mirrors stored in renderSelectionStart/End refs - Visual slots are now <div> + <span> inside VField (no more input elements) - Fake caret rendered on active empty slot with CSS blink animation - deleteContentForward (Delete key) intercepted in beforeinput to keep selection at slot i instead of drifting backward - Modifier + Backspace/Delete intercepted in keydown (before browser converts to deleteContentBackward due to forced range selection) - Android IME fallback: word/line delete inputTypes handled in beforeinput - renderSelectionStart/End updated directly after setSelectionRange to avoid missed selectionchange on empty inputs in Chromium - Tests adapted: single input selector, active slot via .v-field--focused, clickSlot helper for selection-based slot targeting
- Add VOtpField, VOtpGroup, VOtpSeparator sub-components for custom layouts - Add merged prop to VOtpInput and VOtpGroup for merged-border style - Propagate merged/divider from root via provide/inject context - Auto-layout uses sub-components internally - VOtpGroup uses display:contents (non-merged) for equal field sizing - VOtpGroup uses flex:N (merged, N=field count) for proportional sizing - Merge mode collapses adjacent borders via margin-inline-start:-1px - Restore focused field border via z-index and border-inline-start-width
Add focusAt(index) to context for programmatic slot selection. VOtpField emits data-otp-index + onClick for pre-focus slot clicks. Input uses onMousedown + elementsFromPoint for post-focus detection. Pointer-events toggle (none/all via :focus-within) enables native hover before focus and right-click paste after focus. Fix hover borders in merged groups with z-index layering. Clamp focusAt index to input.value.length for empty slot clicks.
Remove individual field box-shadows in merged groups to prevent overlapping artifacts. Re-apply elevation on the group container via :has() for solo variants. Add overlay on focused solo/plain fields. Adapt tests to pointer-events toggle and add coverage for click-to-select, empty slot redirect, merged layout and custom sub-component layouts.
5351f5c to
27c8eec
Compare
- old class name for separator - new slot name, so #default appends below the fields - restored IME support (e.g. autocomplete when typing symbols - korean/chinese/etc.)
|
This is a very welcome set of changes, but in order to ship it in v4.1.0, we have to eliminate breaking changes. I covered the obvious: I would call the IME support "under review". Probably won't have time to test it thoroughly, so here's notes:
If it can be tested quickly (today), it would increase the chances for PR to be merged and shipped in v4.1.0 |
- discriminate real IME composition (CJK/Bopomofo) from predictive-text keyboards (Samsung, Chrome OTP autofill) and Latin dead keys - render in-progress IME characters as an italic overlay on the active slot via a separate compositionText ref, without advancing model.value - freeze the active slot and guard the `finish` emit while composing - add `unicode-alpha` and `unicode-alphanumeric` pattern presets for non-Latin scripts
mention `unicode-alpha` and `unicode-alphanumeric` in the API description and on the pattern section of the component page.
abc80fb to
e7d340a
Compare
|
Normally the IME support works well. There were a few issues specific to the Samsung keyboard, which always triggers for CJK users, an overlay shows what they're typing, the OTP wont register intermediate characters as real input, lets them temporarily type past the max length, and avoids triggering the only keyboard that doesn't follow this flow is Microsoft SwiftKey (maybe others), since it never emits a composition event, I haven't found a clean way to solve that case. |
|
Thanks for handling it so quickly. I will only check for regression against that closed #21918 (later today) and we should be ready to merge |
|
I was not happy with input being "overlay", so it is hidden and unclickable. Afaik, it only exists in Emoji support is not crucial - just fun, marketable and gives us a slight edge. Before changes, Functionally 99% done. Pending some code cleanup:
|
Shift+ArrowLeft from a single-slot selection used to drop the anchor slot because the native selection collapsed to a caret and the single-slot synthesis in syncSelection slid the range by one. Track anchor/focus explicitly in grapheme space and extend the selection ourselves so the originally-selected slot stays included.
I understand your point, but the main reason was to bring back right-click → copy / paste / select all. A lot of users (especially older people) rely on the context menu rather than keyboard shortcuts. I tried alternatives like a custom context menu, but couldn't match the native UX, the overlay was the cleanest solution: yes it requires some JS to keep VField's hover working and handle selection, but after that the browser does the rest for us. Anyway, the Shift+Arrow bug should be fixed now, and I renamed some variables. I'll let you handle the docs and remaining comment cleanups, let me know if you need something else :) |
|
We could have <template #input="{ props }">
<input v-bind="{ props }" class="opacity-0 absolute inset-0" />
</template>Might be good to experiment a bit in a follow-up PR. This one packs a ton of improvements and we went through a lot to make it backward compatible as well. I barely have any HP to spare 😆. Some OTP inputs in real-life use plain text field - GitHub, GitLab, Plausible, Netlify, which does not look that bad actually is the most cost-effective way to achieve fully native experience. I suppose those platforms had their fair share of support tickets and decided that fancy split view is not worth it. |
|
I share the vibe i never thought a otp would so picky to make, an opt-in would be good for the v4.x but maybe we can make that the default on v5.x, anyway lets close this PR and see what people think of it |
resolves #22659
resolves #18427
Full refactoring of the VOtpInput
This component can be manipulated to other used case then an OTP such as a PIN code, Bank codes (BSB, IBAN, ...) and maybe other but it may need changes on the pattern side to check the whole input and not each character
New Sub Components
index(Number) - Index of slotmerged(Boolean | null) default =null- Merge childVOtpField, fallback to root componentmergeddefault- to create custom separator (replace text)Root Component
pattern(’numeric’ | ‘alpha’ | ‘alphanumeric’ | RegExp) default =undefined- filter accepted character, fallback totype=numberif it’s the case then pattern ='numeric'for backward-compatibility reasonmerged(Boolean) default =false- define VOtpGroup default behavior, when merge no divider can be displayedfields- to custom slots layout with sub components:VOtpField,VOtpGroup,VOtpSeparatordivider- Slot for custom divider content, Receives{ index }with the divider positionMarkup: