Skip to content

Commit 8c67ed8

Browse files
committed
fix(VOtpInput): support paste and autofill on mobile
fixes #14801
1 parent e60e5a9 commit 8c67ed8

File tree

2 files changed

+26
-70
lines changed

2 files changed

+26
-70
lines changed

Diff for: packages/vuetify/src/components/VOtpInput/VOtpInput.ts

+23-43
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export default baseMixins.extend<options>().extend({
4949
},
5050

5151
data: () => ({
52-
badInput: false,
5352
initialValue: null,
5453
isBooted: false,
5554
otp: [] as string[],
@@ -66,9 +65,6 @@ export default baseMixins.extend<options>().extend({
6665
'v-otp-input--plain': this.plain,
6766
}
6867
},
69-
isDirty (): boolean {
70-
return VInput.options.computed.isDirty.call(this) || this.badInput
71-
},
7268
},
7369

7470
watch: {
@@ -159,18 +155,17 @@ export default baseMixins.extend<options>().extend({
159155
},
160156
attrs: {
161157
...this.attrs$,
158+
autocomplete: 'one-time-code',
162159
disabled: this.isDisabled,
163160
readonly: this.isReadonly,
164161
type: this.type,
165162
id: `${this.computedId}--${otpIdx}`,
166163
class: `otp-field-box--${otpIdx}`,
167-
maxlength: 1,
168164
},
169165
on: Object.assign(listeners, {
170166
blur: this.onBlur,
171167
input: (e: Event) => this.onInput(e, otpIdx),
172168
focus: (e: Event) => this.onFocus(e, otpIdx),
173-
paste: (e: ClipboardEvent) => this.onPaste(e, otpIdx),
174169
keydown: this.onKeyDown,
175170
keyup: (e: KeyboardEvent) => this.onKeyUp(e, otpIdx),
176171
}),
@@ -212,22 +207,31 @@ export default baseMixins.extend<options>().extend({
212207
e && this.$emit('focus', e)
213208
}
214209
},
215-
onInput (e: Event, otpIdx: number) {
210+
onInput (e: Event, index: number) {
211+
const maxCursor = +this.length - 1
212+
216213
const target = e.target as HTMLInputElement
217214
const value = target.value
218-
this.applyValue(otpIdx, target.value, () => {
219-
this.internalValue = this.otp.join('')
220-
})
221-
this.badInput = target.validity && target.validity.badInput
215+
const inputDataArray = value?.split('') || []
216+
217+
const newOtp: string[] = [...this.otp]
218+
for (let i = 0; i < inputDataArray.length; i++) {
219+
const appIdx = index + i
220+
if (appIdx > maxCursor) break
221+
newOtp[appIdx] = inputDataArray[i].toString()
222+
}
223+
if (!inputDataArray.length) {
224+
newOtp.splice(index, 1)
225+
}
226+
227+
this.otp = newOtp
228+
this.internalValue = this.otp.join('')
222229

223-
const nextIndex = otpIdx + 1
224-
if (value) {
225-
if (nextIndex < +this.length) {
226-
this.changeFocus(nextIndex)
227-
} else {
228-
this.clearFocus(otpIdx)
229-
this.onCompleted()
230-
}
230+
if (index + inputDataArray.length >= +this.length) {
231+
this.onCompleted()
232+
this.clearFocus(index)
233+
} else if (inputDataArray.length) {
234+
this.changeFocus(index + inputDataArray.length)
231235
}
232236
},
233237
clearFocus (index: number) {
@@ -255,30 +259,6 @@ export default baseMixins.extend<options>().extend({
255259

256260
VInput.options.methods.onMouseUp.call(this, e)
257261
},
258-
onPaste (event: ClipboardEvent, index: number) {
259-
const maxCursor = +this.length - 1
260-
const inputVal = event?.clipboardData?.getData('Text')
261-
const inputDataArray = inputVal?.split('') || []
262-
event.preventDefault()
263-
const newOtp: string[] = [...this.otp]
264-
for (let i = 0; i < inputDataArray.length; i++) {
265-
const appIdx = index + i
266-
if (appIdx > maxCursor) break
267-
newOtp[appIdx] = inputDataArray[i].toString()
268-
}
269-
this.otp = newOtp
270-
this.internalValue = this.otp.join('')
271-
const targetFocus = Math.min(index + inputDataArray.length, maxCursor)
272-
this.changeFocus(targetFocus)
273-
274-
if (newOtp.length === +this.length) { this.onCompleted(); this.clearFocus(targetFocus) }
275-
},
276-
applyValue (index: number, inputVal: string, next: Function) {
277-
const newOtp: string[] = [...this.otp]
278-
newOtp[index] = inputVal
279-
this.otp = newOtp
280-
next()
281-
},
282262
changeFocus (index: number) {
283263
this.onFocus(undefined, index || 0)
284264
},

Diff for: packages/vuetify/src/components/VOtpInput/__tests__/VOtpInput.spec.ts

+3-27
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,7 @@ describe('VOtpInput.ts', () => {
173173
expect(change).toHaveBeenCalledTimes(2)
174174
})
175175

176-
it('should process input when paste event', async () => {
177-
const getData = jest.fn(() => '1337078')
178-
const event = {
179-
clipboardData: {
180-
getData,
181-
},
182-
}
183-
176+
it('should process input on paste', async () => {
184177
const wrapper = mountFunction({})
185178

186179
const input = wrapper.findAll('input').at(0)
@@ -190,30 +183,13 @@ describe('VOtpInput.ts', () => {
190183
await wrapper.vm.$nextTick()
191184
expect(document.activeElement === element).toBe(true)
192185

193-
input.trigger('paste', event)
186+
element.value = '1337078'
187+
input.trigger('input')
194188
await wrapper.vm.$nextTick()
195189

196190
expect(wrapper.vm.otp).toStrictEqual('133707'.split(''))
197191
})
198192

199-
it('should process input when paste event with empty event', async () => {
200-
const event = {}
201-
202-
const wrapper = mountFunction({})
203-
204-
const input = wrapper.findAll('input').at(0)
205-
206-
const element = input.element as HTMLInputElement
207-
input.trigger('focus')
208-
await wrapper.vm.$nextTick()
209-
expect(document.activeElement === element).toBe(true)
210-
211-
input.trigger('paste', event)
212-
await wrapper.vm.$nextTick()
213-
214-
expect(wrapper.vm.otp).toStrictEqual(''.split(''))
215-
})
216-
217193
it('should clear cursor when input typing is done', async () => {
218194
const onFinish = jest.fn()
219195
const clearFocus = jest.fn()

0 commit comments

Comments
 (0)