Skip to content

Commit

Permalink
fix(VOtpInput): support paste and autofill on mobile
Browse files Browse the repository at this point in the history
fixes #14801
  • Loading branch information
KaelWD committed Jun 15, 2022
1 parent e60e5a9 commit 8c67ed8
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 70 deletions.
66 changes: 23 additions & 43 deletions packages/vuetify/src/components/VOtpInput/VOtpInput.ts
Expand Up @@ -49,7 +49,6 @@ export default baseMixins.extend<options>().extend({
},

data: () => ({
badInput: false,
initialValue: null,
isBooted: false,
otp: [] as string[],
Expand All @@ -66,9 +65,6 @@ export default baseMixins.extend<options>().extend({
'v-otp-input--plain': this.plain,
}
},
isDirty (): boolean {
return VInput.options.computed.isDirty.call(this) || this.badInput
},
},

watch: {
Expand Down Expand Up @@ -159,18 +155,17 @@ export default baseMixins.extend<options>().extend({
},
attrs: {
...this.attrs$,
autocomplete: 'one-time-code',
disabled: this.isDisabled,
readonly: this.isReadonly,
type: this.type,
id: `${this.computedId}--${otpIdx}`,
class: `otp-field-box--${otpIdx}`,
maxlength: 1,
},
on: Object.assign(listeners, {
blur: this.onBlur,
input: (e: Event) => this.onInput(e, otpIdx),
focus: (e: Event) => this.onFocus(e, otpIdx),
paste: (e: ClipboardEvent) => this.onPaste(e, otpIdx),
keydown: this.onKeyDown,
keyup: (e: KeyboardEvent) => this.onKeyUp(e, otpIdx),
}),
Expand Down Expand Up @@ -212,22 +207,31 @@ export default baseMixins.extend<options>().extend({
e && this.$emit('focus', e)
}
},
onInput (e: Event, otpIdx: number) {
onInput (e: Event, index: number) {
const maxCursor = +this.length - 1

const target = e.target as HTMLInputElement
const value = target.value
this.applyValue(otpIdx, target.value, () => {
this.internalValue = this.otp.join('')
})
this.badInput = target.validity && target.validity.badInput
const inputDataArray = value?.split('') || []

const newOtp: string[] = [...this.otp]
for (let i = 0; i < inputDataArray.length; i++) {
const appIdx = index + i
if (appIdx > maxCursor) break
newOtp[appIdx] = inputDataArray[i].toString()
}
if (!inputDataArray.length) {
newOtp.splice(index, 1)
}

this.otp = newOtp
this.internalValue = this.otp.join('')

const nextIndex = otpIdx + 1
if (value) {
if (nextIndex < +this.length) {
this.changeFocus(nextIndex)
} else {
this.clearFocus(otpIdx)
this.onCompleted()
}
if (index + inputDataArray.length >= +this.length) {
this.onCompleted()
this.clearFocus(index)
} else if (inputDataArray.length) {
this.changeFocus(index + inputDataArray.length)
}
},
clearFocus (index: number) {
Expand Down Expand Up @@ -255,30 +259,6 @@ export default baseMixins.extend<options>().extend({

VInput.options.methods.onMouseUp.call(this, e)
},
onPaste (event: ClipboardEvent, index: number) {
const maxCursor = +this.length - 1
const inputVal = event?.clipboardData?.getData('Text')
const inputDataArray = inputVal?.split('') || []
event.preventDefault()
const newOtp: string[] = [...this.otp]
for (let i = 0; i < inputDataArray.length; i++) {
const appIdx = index + i
if (appIdx > maxCursor) break
newOtp[appIdx] = inputDataArray[i].toString()
}
this.otp = newOtp
this.internalValue = this.otp.join('')
const targetFocus = Math.min(index + inputDataArray.length, maxCursor)
this.changeFocus(targetFocus)

if (newOtp.length === +this.length) { this.onCompleted(); this.clearFocus(targetFocus) }
},
applyValue (index: number, inputVal: string, next: Function) {
const newOtp: string[] = [...this.otp]
newOtp[index] = inputVal
this.otp = newOtp
next()
},
changeFocus (index: number) {
this.onFocus(undefined, index || 0)
},
Expand Down
Expand Up @@ -173,14 +173,7 @@ describe('VOtpInput.ts', () => {
expect(change).toHaveBeenCalledTimes(2)
})

it('should process input when paste event', async () => {
const getData = jest.fn(() => '1337078')
const event = {
clipboardData: {
getData,
},
}

it('should process input on paste', async () => {
const wrapper = mountFunction({})

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

input.trigger('paste', event)
element.value = '1337078'
input.trigger('input')
await wrapper.vm.$nextTick()

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

it('should process input when paste event with empty event', async () => {
const event = {}

const wrapper = mountFunction({})

const input = wrapper.findAll('input').at(0)

const element = input.element as HTMLInputElement
input.trigger('focus')
await wrapper.vm.$nextTick()
expect(document.activeElement === element).toBe(true)

input.trigger('paste', event)
await wrapper.vm.$nextTick()

expect(wrapper.vm.otp).toStrictEqual(''.split(''))
})

it('should clear cursor when input typing is done', async () => {
const onFinish = jest.fn()
const clearFocus = jest.fn()
Expand Down

0 comments on commit 8c67ed8

Please sign in to comment.