diff --git a/packages/varlet-ui/src/otp-input/OtpInput.vue b/packages/varlet-ui/src/otp-input/OtpInput.vue
new file mode 100644
index 00000000000..16b43720652
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/OtpInput.vue
@@ -0,0 +1,235 @@
+
+
+
+ handleFocus(event, i)"
+ @blur="handleBlur"
+ @click="handleClick(i)"
+ @keydown="handleKeydown"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/varlet-ui/src/otp-input/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-ui/src/otp-input/__tests__/__snapshots__/index.spec.js.snap
new file mode 100644
index 00000000000..1922cd08433
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/__tests__/__snapshots__/index.spec.js.snap
@@ -0,0 +1,1985 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`test otp-input > enters value and moves to next input 1`] = `
+"
"
+`;
+
+exports[`test otp-input > test otp-input 1`] = `
+""
+`;
+
+exports[`test otp-input 1`] = `
+""
+`;
+
+exports[`test otp-input size 1`] = `
+""
+`;
+
+exports[`test otp-input size 2`] = `
+""
+`;
+
+exports[`test otp-input variant 1`] = `
+""
+`;
+
+exports[`test otp-input variant 2`] = `
+""
+`;
+
+exports[`test otpInput size 1`] = `
+""
+`;
+
+exports[`test otpInput variant 1`] = `
+""
+`;
+
+exports[`test otpInput variant 2`] = `
+""
+`;
diff --git a/packages/varlet-ui/src/otp-input/__tests__/index.spec.js b/packages/varlet-ui/src/otp-input/__tests__/index.spec.js
new file mode 100644
index 00000000000..bace246f0f8
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/__tests__/index.spec.js
@@ -0,0 +1,98 @@
+import OtpInput from '..'
+import VarOtpInput from '../OtpInput'
+import { mount } from '@vue/test-utils'
+import { createApp } from 'vue'
+// import { trigger, triggerKeyboard } from '../../utils/test'
+import { expect, vi, describe, test } from 'vitest'
+
+test('test otp-input plugin', () => {
+ const app = createApp({}).use(OtpInput)
+ expect(app.component(OtpInput.name)).toBeTruthy()
+})
+
+describe('test otp-input', () => {
+ test('enters value and moves to next input', async () => {
+ const onUpdateModelValue = vi.fn()
+ const onFocus = vi.fn()
+
+ const wrapper = mount(VarOtpInput, {
+ props: {
+ modelValue: '',
+ 'onUpdate:modelValue': onUpdateModelValue,
+ onFocus,
+ },
+ })
+
+ const children = wrapper.findAllComponents({ name: 'var-input' })
+
+ let val = ''
+ for (let i = 0; i < children.length; ++i) {
+ const input = children[i].find('.var-input__input')
+ if (i === 0) {
+ await input.trigger('click')
+ }
+ await input.setValue(String(i))
+ expect(onUpdateModelValue).lastCalledWith((val += String(i)))
+ if (i !== 0 && i < children.length - 1) {
+ expect(onFocus).lastCalledWith(i + 1)
+ }
+ }
+
+ wrapper.unmount()
+ })
+
+ test('enters value and moves to next input when focused index is not next', async () => {
+ const onUpdateModelValue = vi.fn()
+ const onFocus = vi.fn()
+
+ const wrapper = mount(VarOtpInput, {
+ props: {
+ modelValue: '',
+ 'onUpdate:modelValue': onUpdateModelValue,
+ onFocus,
+ },
+ })
+
+ const children = wrapper.findAllComponents({ name: 'var-input' })
+ const input = children[3].find('.var-input__input')
+ await input.trigger('click')
+ await input.setValue(String(0))
+
+ expect(onUpdateModelValue).lastCalledWith('0')
+ expect(onFocus).lastCalledWith(1)
+
+ wrapper.unmount()
+ })
+
+ // test('removes value and goes back when using backspace', async () => {
+ // const onUpdateModelValue = vi.fn()
+ // const onFocus = vi.fn()
+ // const onChange = vi.fn()
+ // const onKeydown = vi.fn()
+
+ // const wrapper = mount(VarOtpInput, {
+ // props: {
+ // modelValue: '123',
+ // 'onUpdate:modelValue': onUpdateModelValue,
+ // onFocus: onFocus,
+ // onChange: onChange,
+ // onKeydown: onKeydown
+ // }
+ // })
+
+ // const children = wrapper.findAllComponents({ name: 'var-input' })
+ // const input = children[2].find('.var-input__input')
+ // trigger(input, 'click')
+
+ // await triggerKeyboard(wrapper, 'keydown', { key: 'Backspace' })
+ // await triggerKeyboard(wrapper, 'keydown', { key: 'Backspace' })
+ // await triggerKeyboard(wrapper, 'keydown', { key: 'Backspace' })
+ // await triggerKeyboard(wrapper, 'keydown', { key: 'Backspace' })
+ // await triggerKeyboard(wrapper, 'keydown', { key: 'Backspace' })
+
+ // expect(onKeydown).toHaveBeenCalledTimes(5)
+ // expect(onUpdateModelValue).lastCalledWith('')
+
+ // wrapper.unmount()
+ // })
+})
diff --git a/packages/varlet-ui/src/otp-input/docs/en-US.md b/packages/varlet-ui/src/otp-input/docs/en-US.md
new file mode 100644
index 00000000000..b9810178785
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/docs/en-US.md
@@ -0,0 +1,136 @@
+# Input
+
+### Intro
+
+The OTP input is used to authenticate users with a one-time password.
+
+### Standard Usage
+
+```html
+
+
+
+
+
+```
+
+### Readonly
+
+```html
+
+
+
+
+
+```
+
+### Disabled
+
+```html
+
+
+
+
+
+```
+
+### Validate
+
+```html
+
+
+
+
+
+```
+
+### Variant Appearance
+
+```html
+
+
+
+
+
+```
+
+### Smaller Size Variant Appearance
+
+```html
+
+
+
+
+
+```
+
+## API
+
+### Props
+
+| Prop | Description | Type | Default |
+| --- |----------------------------------------------------------------------------------------------------------------------------------------| --- | --- |
+| `v-model` | The value of the binding | _string\|number_ | `-` |
+| `size` | Input size, The optional value is `normal` `small` | _string_ | `normal` |
+| `variant` | Input variants, The optional value is `standard` `outlined` | _string_ | `standard` |
+| `text-color` | Text color | _string_ | `-` |
+| `focus-color` | The primary color in focus | _string_ | `-` |
+| `blur-color` | The primary color in blur | _string_ | `-` |
+| `readonly` | Whether the readonly | _boolean_ | `false` |
+| `disabled` | Whether the disabled | _boolean_ | `false` |
+| `autofocus` | Whether the autofocus the first input component | _boolean_ | `false` |
+| `validate-trigger` | Timing to trigger validation, The optional value is `onFocus` `onBlur` `onChange` `onClick` `onInput` | _ValidateTriggers[]_ | `['onInput']` |
+| `rules` | The validation rules, return `true` to indicate that the validation passed,The remaining values are converted to text as user prompts | _Array<(v: string) => any>_ | `-` |
+
+### Methods
+
+| Method | Description | Arguments | Return |
+| --- | --- | --- | --- |
+| `focus` | Focus | `-` | `-` |
+| `blur` | Blur | `-` | `-` |
+| `validate` | Trigger validate | `-` | `valid: Promise` |
+| `resetValidation` | Clear validate messages | `-` | `-` |
+| `reset` | Clear the value of the binding and validate messages | `-` | `-` |
+
+### Events
+
+| Event | Description | Arguments |
+| --- | --- | --- |
+| `focus` | Triggered while focusing | `event: Event` |
+| `blur` | Triggered when out of focus | `event: Event` |
+| `click` | Triggered on Click | `index: number` |
+| `input` | Triggered on input | `value: string` |
+| `change` | Triggered on change | `value: string` |
+
+### Style Variables
+
+Here are the CSS variables used by the component. Styles can be customized using [StyleProvider](#/en-US/style-provider).
+
+| Variable | Default |
+| --- | --- |
+| `--var-otp-input-text-align` | `center` |
+| `--var-otp-input-margin-right` | `8px` |
diff --git a/packages/varlet-ui/src/otp-input/docs/zh-CN.md b/packages/varlet-ui/src/otp-input/docs/zh-CN.md
new file mode 100644
index 00000000000..a30e7987b1f
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/docs/zh-CN.md
@@ -0,0 +1,136 @@
+# 验证码输入框
+
+### 介绍
+
+验证码输入框,用于通过一次性密码对用户进行认证。
+
+### 基本使用
+
+```html
+
+
+
+
+
+```
+
+### 只读状态
+
+```html
+
+
+
+
+
+```
+
+### 禁用状态
+
+```html
+
+
+
+
+
+```
+
+### 字段校验
+
+```html
+
+
+
+
+
+```
+
+### 变体外观
+
+```html
+
+
+
+
+
+```
+
+### 小尺寸变体外观
+
+```html
+
+
+
+
+
+```
+
+## API
+
+### 属性
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- |--------------------------------------------------------------------------| --- | --- |
+| `v-model` | 绑定的值 | _string\|number_ | `-` |
+| `size` | 输入框尺寸,可选值 `normal` `small` | _string_ | `normal` |
+| `variant` | 输入框风格, 可选值为 `standard` `outlined` | _string_ | `standard` |
+| `text-color` | 文字颜色 | _string_ | `-` |
+| `focus-color` | 聚焦时的主要颜色 | _string_ | `-` |
+| `blur-color` | 失焦时的主要颜色 | _string_ | `-` |
+| `readonly` | 是否只读 | _boolean_ | `false` |
+| `disabled` | 是否禁用 | _boolean_ | `false` |
+| `autofocus` | 是否自动聚焦第一个输入框 | _boolean_ | `false` |
+| `validate-trigger` | 触发验证的时机,可选值为 `onFocus` `onBlur` `onChange` `onClick` `onInput` | _ValidateTriggers[]_ | `['onInput']` |
+| `rules` | 验证规则,返回 `true` 表示验证通过,其余的值则转换为文本作为用户提示 | _Array<(v: string) => any>_ | `-` |
+
+### 方法
+
+| 方法名 | 说明 | 参数 | 返回值 |
+| --- | --- | --- | --- |
+| `focus` | 聚焦 | `-` | `-` |
+| `blur` | 失焦 | `-` | `-` |
+| `validate` | 触发校验 | `-` | `valid: Promise` |
+| `resetValidation` | 清空校验信息 | `-` | `-` |
+| `reset` | 清空绑定的值和校验信息 | `-` | `-` |
+
+### 事件
+
+| 事件名 | 说明 | 参数 |
+| --- | --- | --- |
+| `focus` | 聚焦时触发 | `event: Event` |
+| `blur` | 失焦时触发 | `event: Event` |
+| `click` | 点击时触发 | `index: number` |
+| `input` | 输入时触发 | `value: string` |
+| `change` | 更新时触发 | `value: string` |
+
+### 样式变量
+
+以下为组件使用的 css 变量,可以使用 [StyleProvider 组件](#/zh-CN/style-provider) 进行样式定制。
+
+| 变量名 | 默认值 |
+| --- | --- |
+| `--var-otp-input-text-align` | `center` |
+| `--var-otp-input-margin-right` | `8px` |
diff --git a/packages/varlet-ui/src/otp-input/example/index.vue b/packages/varlet-ui/src/otp-input/example/index.vue
new file mode 100644
index 00000000000..227ab23bce9
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/example/index.vue
@@ -0,0 +1,37 @@
+
+
+
+ {{ t('basicUsage') }}
+
+
+ {{ t('readonly') }}
+
+
+ {{ t('disabled') }}
+
+
+ {{ t('validate') }}
+
+
+ {{ t('variant') }}
+
+
+ {{ t('smallSize') }}
+
+
+
+
diff --git a/packages/varlet-ui/src/otp-input/example/locale/en-US.ts b/packages/varlet-ui/src/otp-input/example/locale/en-US.ts
new file mode 100644
index 00000000000..96f4bb6c0e0
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/example/locale/en-US.ts
@@ -0,0 +1,10 @@
+export default {
+ basicUsage: 'Basic Usage',
+ errorMessage: 'Must enter a 6-digit verification code.',
+ variant: 'Variant Appearance',
+ standard: 'Standard Variant',
+ smallSize: 'Small Size Variant Appearance',
+ disabled: 'Disabled',
+ readonly: 'Readonly',
+ validate: 'Validate',
+}
diff --git a/packages/varlet-ui/src/otp-input/example/locale/index.ts b/packages/varlet-ui/src/otp-input/example/locale/index.ts
new file mode 100644
index 00000000000..8855457aa5c
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/example/locale/index.ts
@@ -0,0 +1,17 @@
+import { Locale } from '@varlet/ui'
+import zhCN from './zh-CN'
+import enUS from './en-US'
+
+const { add, use: exampleUse, t, merge } = Locale.useLocale()
+
+const use = (lang: string) => {
+ Locale.use(lang)
+ exampleUse(lang)
+}
+
+Locale.add('zh-CN', Locale.zhCN)
+Locale.add('en-US', Locale.enUS)
+add('zh-CN', zhCN)
+add('en-US', enUS)
+
+export { add, t, merge, use }
diff --git a/packages/varlet-ui/src/otp-input/example/locale/zh-CN.ts b/packages/varlet-ui/src/otp-input/example/locale/zh-CN.ts
new file mode 100644
index 00000000000..0b02ec37846
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/example/locale/zh-CN.ts
@@ -0,0 +1,10 @@
+export default {
+ basicUsage: '基本使用',
+ errorMessage: '必须输入6位验证码',
+ variant: '变体外观',
+ standard: '标准外观',
+ smallSize: '小尺寸变体外观',
+ disabled: '禁用',
+ readonly: '只读',
+ validate: '字段校验',
+}
diff --git a/packages/varlet-ui/src/otp-input/index.ts b/packages/varlet-ui/src/otp-input/index.ts
new file mode 100644
index 00000000000..63a4dcf8604
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/index.ts
@@ -0,0 +1,12 @@
+import OtpInput from './OtpInput.vue'
+import { withInstall, withPropsDefaultsSetter } from '../utils/components'
+import { props as otpInputProps } from './props'
+
+withInstall(OtpInput)
+withPropsDefaultsSetter(OtpInput, otpInputProps)
+
+export { otpInputProps }
+
+export const _OtpInputComponent = OtpInput
+
+export default OtpInput
diff --git a/packages/varlet-ui/src/otp-input/otpInput.less b/packages/varlet-ui/src/otp-input/otpInput.less
new file mode 100644
index 00000000000..98727f345a3
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/otpInput.less
@@ -0,0 +1,24 @@
+:root {
+ --var-otp-input-text-align: center;
+ --var-otp-input-margin-right: 8px;
+}
+
+.var-otp-input {
+ &__container {
+ display: flex;
+
+ .var-input[var-otp-input-cover] {
+ .var-form-details {
+ display: none;
+ }
+
+ .var-input__input {
+ text-align: var(--var-otp-input-text-align);
+ }
+
+ &:not(:last-child) {
+ margin-right: var(--var-otp-input-margin-right);
+ }
+ }
+ }
+}
diff --git a/packages/varlet-ui/src/otp-input/props.ts b/packages/varlet-ui/src/otp-input/props.ts
new file mode 100644
index 00000000000..1c4adb787df
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/props.ts
@@ -0,0 +1,38 @@
+import { type PropType } from 'vue'
+import { defineListenerProp, pickProps } from '../utils/components'
+import { inputProps } from '../input'
+
+export type OptInputValidateTrigger = 'onFocus' | 'onBlur' | 'onChange' | 'onClick' | 'onInput'
+
+export const props = {
+ modelValue: {
+ type: [Number, String],
+ default: undefined,
+ },
+ length: {
+ type: Number,
+ default: 6,
+ },
+ validateTrigger: {
+ type: Array as PropType,
+ default: () => ['onInput'],
+ },
+ rules: Array as PropType any>>,
+ onFocus: defineListenerProp<(event: Event) => void>(),
+ onBlur: defineListenerProp<(event: Event) => void>(),
+ onClick: defineListenerProp<(i: number) => void>(),
+ onInput: defineListenerProp<(value: string) => void>(),
+ onChange: defineListenerProp<(value: string) => void>(),
+ 'onUpdate:modelValue': defineListenerProp<(value: string) => void>(),
+ ...pickProps(inputProps, [
+ 'variant',
+ 'size',
+ 'readonly',
+ 'autofocus',
+ 'disabled',
+ 'readonly',
+ 'textColor',
+ 'focusColor',
+ 'blurColor',
+ ]),
+}
diff --git a/packages/varlet-ui/src/otp-input/provide.ts b/packages/varlet-ui/src/otp-input/provide.ts
new file mode 100644
index 00000000000..7b1472f08df
--- /dev/null
+++ b/packages/varlet-ui/src/otp-input/provide.ts
@@ -0,0 +1,3 @@
+import { Validation } from '../form/provide'
+
+export type OtpInputProvider = Validation
diff --git a/packages/varlet-ui/types/index.d.ts b/packages/varlet-ui/types/index.d.ts
index 26a63bcc679..94dc70ae186 100644
--- a/packages/varlet-ui/types/index.d.ts
+++ b/packages/varlet-ui/types/index.d.ts
@@ -54,6 +54,7 @@ export * from './menu'
export * from './menuOption'
export * from './menuSelect'
export * from './option'
+export * from './otpInput'
export * from './overlay'
export * from './pagination'
export * from './paper'
@@ -144,6 +145,7 @@ declare module 'vue' {
VarMenuOption: typeof import('@varlet/ui')['_MenuOptionComponent']
VarMenuSelect: typeof import('@varlet/ui')['_MenuSelectComponent']
VarOption: typeof import('@varlet/ui')['_OptionComponent']
+ VarOtpInput: typeof import('@varlet/ui')['_OtpInputComponent']
VarOverlay: typeof import('@varlet/ui')['_OverlayComponent']
VarPagination: typeof import('@varlet/ui')['_PaginationComponent']
VarPaper: typeof import('@varlet/ui')['_PaperComponent']
diff --git a/packages/varlet-ui/types/otpInput.d.ts b/packages/varlet-ui/types/otpInput.d.ts
new file mode 100644
index 00000000000..9ada3f10334
--- /dev/null
+++ b/packages/varlet-ui/types/otpInput.d.ts
@@ -0,0 +1,45 @@
+import { VarComponent, BasicAttributes, ListenerProp, SetPropsDefaults } from './varComponent'
+import { InputSize } from './input'
+
+export declare const inputProps: Record
+
+export type OptInputValidateTrigger = 'onFocus' | 'onBlur' | 'onChange' | 'onClick' | 'onInput'
+
+export interface OtpInputProps extends BasicAttributes {
+ modelValue?: string | number
+ size?: InputSize
+ length?: number
+ variant?: boolean
+ textColor?: string
+ focusColor?: string
+ blurColor?: string
+ disabled?: boolean
+ readonly?: boolean
+ autofocus?: boolean
+ validateTrigger?: OptInputValidateTrigger[]
+ rules?: Array<(v: string) => any>
+ onFocus?: ListenerProp<(e: Event) => void>
+ onBlur?: ListenerProp<(e: Event) => void>
+ onClick?: ListenerProp<(e: Event) => void>
+ onInput?: ListenerProp<(value: string, e: Event) => void>
+ onChange?: ListenerProp<(value: string, e: Event) => void>
+ 'onUpdate:modelValue'?: ListenerProp<(value: string) => void>
+}
+
+export class OtpInput extends VarComponent {
+ static setPropsDefaults: SetPropsDefaults
+
+ $props: OtpInputProps
+
+ focus(): void
+
+ blur(): void
+
+ validate(): Promise
+
+ resetValidation(): void
+
+ reset(): void
+}
+
+export class _OtpInputComponent extends OtpInput {}
diff --git a/packages/varlet-ui/types/styleVars.d.ts b/packages/varlet-ui/types/styleVars.d.ts
index 661b4a69e0d..1fab890ab97 100644
--- a/packages/varlet-ui/types/styleVars.d.ts
+++ b/packages/varlet-ui/types/styleVars.d.ts
@@ -837,6 +837,10 @@ export interface StyleVars {
menuOptionTextColor?: string
'--menu-option-disabled-color'?: string
menuOptionDisabledColor?: string
+ '--var-otp-input-text-align'?: string
+ varOtpInputTextAlign?: string
+ '--var-otp-input-margin-right'?: string
+ varOtpInputMarginRight?: string
'--overlay-background-color'?: string
overlayBackgroundColor?: string
'--pagination-text-color'?: string
diff --git a/packages/varlet-ui/varlet.config.mjs b/packages/varlet-ui/varlet.config.mjs
index 3799744e687..044f6116e58 100644
--- a/packages/varlet-ui/varlet.config.mjs
+++ b/packages/varlet-ui/varlet.config.mjs
@@ -630,6 +630,14 @@ export default defineConfig({
doc: 'input',
type: 2,
},
+ {
+ text: {
+ 'zh-CN': 'OtpInput 验证码输入框',
+ 'en-US': 'OtpInput',
+ },
+ doc: 'otp-input',
+ type: 2,
+ },
{
text: {
'zh-CN': 'Select 选择框',