Skip to content

Commit f85f4ed

Browse files
committed
feat: new component input-otp
1 parent 165c58a commit f85f4ed

File tree

19 files changed

+1016
-0
lines changed

19 files changed

+1016
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<template>
2+
<div :class="classes(n())">
3+
<div :class="n('container')">
4+
<var-input
5+
ref="inputRef"
6+
type="number"
7+
var-otp-input-cover
8+
:model-value="modelValue"
9+
:variant="variant"
10+
:readonly="readonly"
11+
:disabled="disabled"
12+
:size="size"
13+
:text-color="textColor"
14+
:focus-color="focusColor"
15+
:blur-color="blurColor"
16+
:autofocus="index === 0 && autofocus"
17+
@update:model-value="handleInput"
18+
@focus="handleFocus"
19+
@blur="onItemBlur(index)"
20+
@click="handleClick(index)"
21+
@keydown="handleKeydown"
22+
/>
23+
</div>
24+
</div>
25+
</template>
26+
27+
<script lang="ts">
28+
import { computed, defineComponent, ref, watch } from 'vue'
29+
import { preventDefault } from '@varlet/shared'
30+
import VarInput from '../input'
31+
import { createNamespace } from '../utils/components'
32+
import { useInputOtp, type InputOtpItemProvider } from './provide'
33+
34+
const { name, n, classes } = createNamespace('otp-input')
35+
36+
type VarInputInstance = InstanceType<typeof VarInput>
37+
38+
export default defineComponent({
39+
name,
40+
components: {
41+
VarInput,
42+
},
43+
emits: ['update:modelValue'],
44+
setup() {
45+
const inputRef = ref<VarInputInstance>()
46+
const { index, inputOtp, bindInputOtp } = useInputOtp()
47+
const {
48+
activeInput,
49+
parentModel,
50+
disabled,
51+
readonly,
52+
variant,
53+
size,
54+
textColor,
55+
focusColor,
56+
blurColor,
57+
autofocus,
58+
onItemChange,
59+
onItemFocus,
60+
onItemBlur,
61+
} = inputOtp
62+
63+
const modelValue = computed(() => {
64+
return parentModel.value.slice(index.value, index.value + 1)
65+
})
66+
67+
const inputOtpItemProvider: InputOtpItemProvider = {
68+
index,
69+
}
70+
71+
watch(
72+
() => activeInput.value,
73+
(value) => {
74+
if (value === index.value) {
75+
inputRef.value?.focus()
76+
}
77+
},
78+
)
79+
80+
bindInputOtp(inputOtpItemProvider)
81+
82+
function handleInput(value: string) {
83+
if (!value) {
84+
return
85+
}
86+
87+
inputRef.value?.blur()
88+
89+
onItemChange(index.value, value.slice(value.length - 1, value.length))
90+
}
91+
92+
function handleFocus() {
93+
const input = inputRef.value?.$el.querySelector('input')
94+
if (input) {
95+
input.select()
96+
}
97+
onItemFocus(index.value)
98+
}
99+
100+
function handleClick(index: number) {
101+
onItemChange(index)
102+
}
103+
104+
function handleKeydown(event: KeyboardEvent) {
105+
if (disabled.value || readonly.value || !['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete'].includes(event.key)) {
106+
return
107+
}
108+
109+
preventDefault(event)
110+
111+
if (event.key === 'ArrowLeft') {
112+
onItemChange(index.value - 1)
113+
} else if (event.key === 'ArrowRight') {
114+
onItemChange(index.value + 1)
115+
} else if (['Backspace', 'Delete'].includes(event.key)) {
116+
onItemChange(index.value, '')
117+
return
118+
}
119+
}
120+
121+
return {
122+
index,
123+
modelValue,
124+
inputRef,
125+
disabled,
126+
readonly,
127+
variant,
128+
size,
129+
textColor,
130+
focusColor,
131+
blurColor,
132+
autofocus,
133+
n,
134+
classes,
135+
handleInput,
136+
handleClick,
137+
handleKeydown,
138+
handleFocus,
139+
onItemBlur,
140+
}
141+
},
142+
})
143+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createApp } from 'vue'
2+
import { expect, test } from 'vitest'
3+
import InputOtpItem from '..'
4+
5+
test('input-otp-item plugin', () => {
6+
const app = createApp({}).use(InputOtpItem)
7+
expect(app.component(InputOtpItem.name)).toBeTruthy()
8+
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { withInstall, withPropsDefaultsSetter } from '../utils/components'
2+
import InputOtpItem from './InputOtpItem.vue'
3+
import { props as inputOtpItemProps } from './props'
4+
5+
withInstall(InputOtpItem)
6+
withPropsDefaultsSetter(InputOtpItem, inputOtpItemProps)
7+
8+
export { inputOtpItemProps }
9+
10+
export const _InputOtpItemComponent = InputOtpItem
11+
12+
export default InputOtpItem
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { BasicAttributes, SetPropsDefaults, VarComponent } from '../../types/varComponent'
2+
3+
export declare const inputOtpItemProps: Record<keyof InputOtpItemProps, any>
4+
5+
export interface InputOtpItemProps extends BasicAttributes {}
6+
7+
export class InputOtpItem extends VarComponent {
8+
static setPropsDefaults: SetPropsDefaults<InputOtpItemProps>
9+
10+
$props: InputOtpItemProps
11+
}
12+
13+
export class _InputOtpItemComponent extends InputOtpItem {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { inputProps } from '../input'
2+
import { pickProps } from '../utils/components'
3+
4+
export const props = {
5+
...pickProps(inputProps, ['variant', 'size', 'autofocus', 'textColor', 'focusColor', 'blurColor']),
6+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { type ComputedRef } from 'vue'
2+
import { assert } from '@varlet/shared'
3+
import { useParent } from '@varlet/use'
4+
import { INPUT_OTP_BIND_INPUT_OTP_ITEM_KEY, type InputOtpProvider } from '../input-otp/provide'
5+
6+
export interface InputOtpItemProvider {
7+
index: ComputedRef<number>
8+
}
9+
10+
export function useInputOtp() {
11+
const { parentProvider, index, bindParent } = useParent<InputOtpProvider, InputOtpItemProvider>(
12+
INPUT_OTP_BIND_INPUT_OTP_ITEM_KEY,
13+
)
14+
15+
assert(!!bindParent, 'InputOtpItem', '<var-input-otp-item/> must in <var-input-otp/>')
16+
17+
return {
18+
index,
19+
inputOtp: parentProvider,
20+
bindInputOtp: bindParent,
21+
}
22+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<template>
2+
<div :class="n()">
3+
<div :class="n('container')">
4+
<slot />
5+
</div>
6+
7+
<var-form-details :error-message="errorMessage" @mousedown.stop>
8+
<template v-if="$slots['extra-message']" #extra-message>
9+
<slot name="extra-message" />
10+
</template>
11+
</var-form-details>
12+
</div>
13+
</template>
14+
15+
<script lang="ts">
16+
import { computed, defineComponent, nextTick, ref } from 'vue'
17+
import { call } from '@varlet/shared'
18+
import { useForm } from '../form/provide'
19+
import { createNamespace, useValidation } from '../utils/components'
20+
import { props, type OptInputValidateTrigger } from './props'
21+
import { useInputOtpItems, type InputOtpProvider } from './provide'
22+
23+
const { name, n } = createNamespace('input-otp')
24+
25+
export default defineComponent({
26+
name,
27+
props,
28+
setup(props, { emit }) {
29+
const { length, inputOtpItems, bindInputOtpItem } = useInputOtpItems()
30+
const activeInput = ref()
31+
32+
const model = computed({
33+
get: () => props.modelValue,
34+
set: (value) => {
35+
call(props.onChange, value)
36+
call(props['onUpdate:modelValue'], value)
37+
validateWithTrigger('onChange')
38+
},
39+
})
40+
41+
const { errorMessage, validateWithTrigger: vt, validate: v, resetValidation } = useValidation()
42+
43+
const inputOtpProvider: InputOtpProvider = {
44+
parentModel: model,
45+
activeInput,
46+
length,
47+
disabled: ref(props.disabled),
48+
readonly: ref(props.readonly),
49+
variant: ref(props.variant),
50+
size: ref(props.size),
51+
textColor: ref(props.textColor),
52+
focusColor: ref(props.focusColor),
53+
blurColor: ref(props.blurColor),
54+
autofocus: ref(props.autofocus),
55+
onItemChange,
56+
onItemFocus,
57+
onItemBlur,
58+
reset,
59+
validate,
60+
resetValidation,
61+
}
62+
63+
bindInputOtpItem(inputOtpProvider)
64+
65+
const { bindForm } = useForm()
66+
call(bindForm, inputOtpProvider)
67+
68+
function validateWithTrigger(trigger: OptInputValidateTrigger) {
69+
nextTick(() => {
70+
const { validateTrigger, rules, modelValue } = props
71+
vt(validateTrigger, trigger, rules, modelValue)
72+
})
73+
}
74+
75+
function onItemChange(index: number, value?: string) {
76+
if (value == null) {
77+
activeInput.value = index
78+
} else {
79+
const currentValue = model.value || ''
80+
if (index < length.value) {
81+
activeInput.value = value ? index + 1 : index - 1
82+
emit('update:modelValue', currentValue.slice(0, index) + value + currentValue.slice(index + 1))
83+
} else {
84+
emit('update:modelValue', currentValue + value)
85+
}
86+
}
87+
}
88+
89+
function onItemFocus(index: number) {
90+
call(props.onFocus, index)
91+
}
92+
93+
function onItemBlur(index: number) {
94+
call(props.onBlur, index)
95+
}
96+
97+
// expose
98+
function reset() {
99+
call(props['onUpdate:modelValue'], '')
100+
resetValidation()
101+
}
102+
103+
// expose
104+
function validate() {
105+
return v(props.rules, props.modelValue)
106+
}
107+
108+
return {
109+
length,
110+
inputOtpItems,
111+
errorMessage,
112+
n,
113+
activeInput,
114+
reset,
115+
validate,
116+
}
117+
},
118+
})
119+
</script>
120+
121+
<style lang="less">
122+
@import '../styles/common';
123+
@import '../form-details/formDetails';
124+
@import './inputOtp';
125+
</style>

0 commit comments

Comments
 (0)