vuetify
vuetify copied to clipboard
[Bug Report][3.4.0] Autofill on v-otp-input
Environment
Vuetify Version: 3.4.0 Vue Version: 3.0.0 Browsers: Chrome 119.0.0.0 OS: Android, iOS
Steps to reproduce
I use v-otp-input to get otp from user
Expected Behavior
On mobile device when receive sms otp keyboard on mobile device auto showing and when click this auto fill to otp input
Actual Behavior
Fill first input like picture i attached
Reproduction Link
https://play.vuetifyjs.com/#eNptUstOwzAQ/JXFl4CEEwQcEEorceMPOFAOrrMBI8e27E3Uquq/s054JBGnxLPj2fHOvp5Eirp6CqEcehSPoibsglWE250DqAepQhh/x4P2jpRxGL8hBhszgLYqpc1OaN+gNC70lKCRrcUDfPaJTHuUGh1hhG4vb3fi9/Yomj4QCbS3PrJG6mOrNC5II81TmLTnOIDqyWvPnpGQr3uHkkyHbLVhkSW39bpP2PwDSmXtEh5UNMpRdsTO1kp0DLmb67s9xnVxdNlx/4mB0eg1ZZC5bpnQpfdlcVtXs8eup0B4INkatM1aA6r5WFljnOtfUBUn9ZMkV5dRZmRKuq5mG8DHpKMJBAmpnxbBdMFHghNEbOEMbfQdFLw8RaYD5+gSATuCTWZcFs9orYcXH21zUVzlBpMki4nztbgr78oHkb/35Y14+wKmZcSf
You can see that via v-model and can pass value. or use model-value to pass value to component
Vuetify can't handle your logic. this is just component and you have to implement it correctly.
https://play.vuetifyjs.com/#eNptUstOwzAQ/JXFl4CEEwQcEEorceMPOFAOrrMBI8e27E3Uquq/s054JBGnxLPj2fHOvp5Eirp6CqEcehSPoibsglWE250DqAepQhh/x4P2jpRxGL8hBhszgLYqpc1OaN+gNC70lKCRrcUDfPaJTHuUGh1hhG4vb3fi9/Yomj4QCbS3PrJG6mOrNC5II81TmLTnOIDqyWvPnpGQr3uHkkyHbLVhkSW39bpP2PwDSmXtEh5UNMpRdsTO1kp0DLmb67s9xnVxdNlx/4mB0eg1ZZC5bpnQpfdlcVtXs8eup0B4INkatM1aA6r5WFljnOtfUBUn9ZMkV5dRZmRKuq5mG8DHpKMJBAmpnxbBdMFHghNEbOEMbfQdFLw8RaYD5+gSATuCTWZcFs9orYcXH21zUVzlBpMki4nztbgr78oHkb/35Y14+wKmZcSf
You can see that via v-model and can pass value. or use model-value to pass value to component
Vuetify can't handle your logic. this is just component and you have to implement it correctly.
i really know that! what i want is somthing like autocomplete="one-time-code"
I'm experiencing this as well on iOS.
Reproduction will require getting a mobile device to receive an OTP text message which the operating system then displays as an autofill shortcut on the keyboard.
Say I received a text with a 6-digit OTP that my phone presented to me on my device's keyboard.
Expected Behavior
Tapping that autofill shortcut should put a digit in each text field
Actual Behavior
Only the first text field is modified with only the first digit (or maybe all 6, it's hard to tell)
I see. right now this makes sense and more clear. thanks
@websitevirtuoso I'm experiencing this as well, is it resolved?
It seems that this is related to ios not calling on paste:
https://stackoverflow.com/questions/73704634/autoread-otp-in-web-and-copy-paste-otp-from-clipboard-in-web-not-working-for-and/73757650#73757650
It looks like this was fixed at some point in the past, but the fix was lost. Between a repo rename and converting to v3 there are too many changes to track down when exactly this broke.
OG commit to support this feature: https://github.com/vuetifyjs/vuetify/commit/8c67ed8cf96334a86c6f087b7abfa845992098a2
The issue is the maxlength="1" prop on each of the <input> elements. iOS fires a textInput event with data: '12345' unlike a human typing who types a single character a time. I am working on a complete fix with correct focus handling, but in the meantime I used patch-package with the following patch:
patches/vuetify+3.5.9.patch:
diff --git a/node_modules/vuetify/dist/vuetify.esm.js b/node_modules/vuetify/dist/vuetify.esm.js
index 4e635a0..1e20d68 100644
--- a/node_modules/vuetify/dist/vuetify.esm.js
+++ b/node_modules/vuetify/dist/vuetify.esm.js
@@ -23167,7 +23167,6 @@ const VOtpInput = genericComponent()({
"disabled": props.disabled,
"inputmode": props.type === 'number' ? 'numeric' : 'text',
"min": props.type === 'number' ? 0 : undefined,
- "maxlength": "1",
"placeholder": props.placeholder,
"type": props.type === 'number' ? 'text' : props.type,
"value": model.value[i],
diff --git a/node_modules/vuetify/dist/vuetify.js b/node_modules/vuetify/dist/vuetify.js
index 0164850..67da61c 100644
--- a/node_modules/vuetify/dist/vuetify.js
+++ b/node_modules/vuetify/dist/vuetify.js
@@ -23171,7 +23171,6 @@
"disabled": props.disabled,
"inputmode": props.type === 'number' ? 'numeric' : 'text',
"min": props.type === 'number' ? 0 : undefined,
- "maxlength": "1",
"placeholder": props.placeholder,
"type": props.type === 'number' ? 'text' : props.type,
"value": model.value[i],
diff --git a/node_modules/vuetify/dist/vuetify.min.js b/node_modules/vuetify/dist/vuetify.min.js
index 04f4bce..8ff2b04 100644
--- a/node_modules/vuetify/dist/vuetify.min.js
+++ b/node_modules/vuetify/dist/vuetify.min.js
@@ -1796,7 +1796,7 @@ t[f.value]=a
let l=null
f.value>c.value.length?l=c.value.length+1:f.value+1!==v.value&&(l="next"),c.value=t,l&&ee(m.value,l)}function b(e){const t=c.value.slice(),a=f.value
let l=null;["ArrowLeft","ArrowRight","Backspace","Delete"].includes(e.key)&&(e.preventDefault(),"ArrowLeft"===e.key?l="prev":"ArrowRight"===e.key?l="next":["Backspace","Delete"].includes(e.key)&&(t[f.value]="",c.value=t,f.value>0&&"Backspace"===e.key?l="prev":requestAnimationFrame((()=>{g.value[a]?.select()}))),requestAnimationFrame((()=>{null!=l&&ee(m.value,l)})))}function V(){u(),f.value=-1}return vt({VField:{color:t.computed((()=>e.color)),bgColor:t.computed((()=>e.color)),baseColor:t.computed((()=>e.baseColor)),disabled:t.computed((()=>e.disabled)),error:t.computed((()=>e.error)),variant:t.computed((()=>e.variant))}},{scoped:!0}),t.watch(c,(e=>{e.length===v.value&&o("finish",e.join(""))}),{deep:!0}),t.watch(f,(e=>{e<0||t.nextTick((()=>{g.value[e]?.select()}))})),At((()=>{const[a,o]=R(l)
-return t.createVNode("div",t.mergeProps({class:["v-otp-input",{"v-otp-input--divided":!!e.divider},e.class],style:[e.style]},a),[t.createVNode("div",{ref:m,class:"v-otp-input__content",style:[r.value]},[p.value.map(((a,l)=>t.createVNode(t.Fragment,null,[e.divider&&0!==l&&t.createVNode("span",{class:"v-otp-input__divider"},[e.divider]),t.createVNode(ti,{focused:i.value&&e.focusAll||f.value===l,key:l},{...n,loader:void 0,default:()=>t.createVNode("input",{ref:e=>g.value[l]=e,"aria-label":d(e.label,l+1),autofocus:0===l&&e.autofocus,autocomplete:"one-time-code",class:["v-otp-input__field"],disabled:e.disabled,inputmode:"number"===e.type?"numeric":"text",min:"number"===e.type?0:void 0,maxlength:"1",placeholder:e.placeholder,type:"number"===e.type?"text":e.type,value:c.value[l],onInput:y,onFocus:e=>{return t=l,s(),void(f.value=t)
+return t.createVNode("div",t.mergeProps({class:["v-otp-input",{"v-otp-input--divided":!!e.divider},e.class],style:[e.style]},a),[t.createVNode("div",{ref:m,class:"v-otp-input__content",style:[r.value]},[p.value.map(((a,l)=>t.createVNode(t.Fragment,null,[e.divider&&0!==l&&t.createVNode("span",{class:"v-otp-input__divider"},[e.divider]),t.createVNode(ti,{focused:i.value&&e.focusAll||f.value===l,key:l},{...n,loader:void 0,default:()=>t.createVNode("input",{ref:e=>g.value[l]=e,"aria-label":d(e.label,l+1),autofocus:0===l&&e.autofocus,autocomplete:"one-time-code",class:["v-otp-input__field"],disabled:e.disabled,inputmode:"number"===e.type?"numeric":"text",min:"number"===e.type?0:void 0,placeholder:e.placeholder,type:"number"===e.type?"text":e.type,value:c.value[l],onInput:y,onFocus:e=>{return t=l,s(),void(f.value=t)
var t},onBlur:V,onKeydown:b,onPaste:e=>{return t=l,(a=e).preventDefault(),a.stopPropagation(),c.value=(a?.clipboardData?.getData("Text")??"").split(""),void g.value?.[t].blur()
var t,a}},null)})]))),t.createVNode("input",t.mergeProps({class:"v-otp-input-input",type:"hidden"},o,{value:c.value.join("")}),null),t.createVNode(Hr,{contained:!0,"content-class":"v-otp-input__loader","model-value":!!e.loading,persistent:!0},{default:()=>[n.loader?.()??t.createVNode(Hl,{color:"boolean"==typeof e.loading?void 0:e.loading,indeterminate:!0,size:"24",width:"2"},null)]}),n.default?.()])])})),{blur:()=>{g.value?.some((e=>e.blur()))},focus:()=>{g.value?.[0].focus()},reset:function(){c.value=[]},isFocused:i}}})
const rv=a({scale:{type:[Number,String],default:.5},...l()},"VParallax"),iv=gt()({name:"VParallax",props:rv(),setup(e,a){let{slots:l}=a
diff --git a/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs b/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
index ce0a7d0..f29f80f 100644
--- a/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
+++ b/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
@@ -188,7 +188,6 @@ export const VOtpInput = genericComponent()({
"disabled": props.disabled,
"inputmode": props.type === 'number' ? 'numeric' : 'text',
"min": props.type === 'number' ? 0 : undefined,
- "maxlength": "1",
"placeholder": props.placeholder,
"type": props.type === 'number' ? 'text' : props.type,
"value": model.value[i],
Has this problem been fixed in any updates ?
Any update? We're quite dependant on the OTP component, and this renders it quite useless. Confirmed still an issue with 3.6.7
I can confirm removing maxlength="1" fixes this, but I have struggled to create a Cypress test to prove the fix. Cypress's type command fires 1 input event per character as if a human were typing, and creating an event with the full OTP as the .data attribute and using Cypress's trigger also does not accurately mimic how Mobile Safari works.
I am reluctant to put up a pull request without a test because the fix could inadvertently be removed again in the future without a test.
This appears to be an issue for us on v3.7.2, and we do have users complain about it from time to time.
Is there a plan to address it? Or would you recommend we look elsewhere for a different OTP input component?
Is this going to be fixed?
This issue makes v-otp input completely unusable for me.