Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/varlet-ui/src/field-decorator/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ export const props = {
type: Boolean,
default: true,
},
textColor: String,
focusColor: String,
blurColor: String,
textColor: {
type: String,
default: '',
},
focusColor: {
type: String,
default: '',
},
blurColor: {
type: String,
default: '',
},
isError: Boolean,
formDisabled: Boolean,
disabled: Boolean,
Expand Down
143 changes: 143 additions & 0 deletions packages/varlet-ui/src/input-otp-item/InputOtpItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div :class="classes(n())">
<div :class="n('container')">
<var-input
ref="inputRef"
type="number"
var-otp-input-cover
:model-value="modelValue"
:variant="variant"
:readonly="readonly"
:disabled="disabled"
:size="size"
:text-color="textColor"
:focus-color="focusColor"
:blur-color="blurColor"
:autofocus="index === 0 && autofocus"
@update:model-value="handleInput"
@focus="handleFocus"
@blur="onItemBlur(index)"
@click="handleClick(index)"
@keydown="handleKeydown"
/>
</div>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
import { preventDefault } from '@varlet/shared'
import VarInput from '../input'
import { createNamespace } from '../utils/components'
import { useInputOtp, type InputOtpItemProvider } from './provide'

const { name, n, classes } = createNamespace('otp-input')

type VarInputInstance = InstanceType<typeof VarInput>

export default defineComponent({
name,
components: {
VarInput,
},
emits: ['update:modelValue'],
setup() {
const inputRef = ref<VarInputInstance>()
const { index, inputOtp, bindInputOtp } = useInputOtp()
const {
activeInput,
parentModel,
disabled,
readonly,
variant,
size,
textColor,
focusColor,
blurColor,
autofocus,
onItemChange,
onItemFocus,
onItemBlur,
} = inputOtp

const modelValue = computed(() => {
return parentModel.value.slice(index.value, index.value + 1)
})

const inputOtpItemProvider: InputOtpItemProvider = {
index,
}

watch(
() => activeInput.value,
(value) => {
if (value === index.value) {
inputRef.value?.focus()
}
},
)

bindInputOtp(inputOtpItemProvider)

function handleInput(value: string) {
if (!value) {
return
}

inputRef.value?.blur()

onItemChange(index.value, value.slice(value.length - 1, value.length))
}

function handleFocus() {
const input = inputRef.value?.$el.querySelector('input')
if (input) {
input.select()
}
onItemFocus(index.value)
}

function handleClick(index: number) {
onItemChange(index)
}

function handleKeydown(event: KeyboardEvent) {
if (disabled.value || readonly.value || !['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete'].includes(event.key)) {
return
}

preventDefault(event)

if (event.key === 'ArrowLeft') {
onItemChange(index.value - 1)
} else if (event.key === 'ArrowRight') {
onItemChange(index.value + 1)
} else if (['Backspace', 'Delete'].includes(event.key)) {
onItemChange(index.value, '')
return
}
}

return {
index,
modelValue,
inputRef,
disabled,
readonly,
variant,
size,
textColor,
focusColor,
blurColor,
autofocus,
n,
classes,
handleInput,
handleClick,
handleKeydown,
handleFocus,
onItemBlur,
}
},
})
</script>
8 changes: 8 additions & 0 deletions packages/varlet-ui/src/input-otp-item/__tests__/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import { expect, test } from 'vitest'
import InputOtpItem from '..'

test('input-otp-item plugin', () => {
const app = createApp({}).use(InputOtpItem)
expect(app.component(InputOtpItem.name)).toBeTruthy()
})
12 changes: 12 additions & 0 deletions packages/varlet-ui/src/input-otp-item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { withInstall, withPropsDefaultsSetter } from '../utils/components'
import InputOtpItem from './InputOtpItem.vue'
import { props as inputOtpItemProps } from './props'

withInstall(InputOtpItem)
withPropsDefaultsSetter(InputOtpItem, inputOtpItemProps)

export { inputOtpItemProps }

export const _InputOtpItemComponent = InputOtpItem

export default InputOtpItem
13 changes: 13 additions & 0 deletions packages/varlet-ui/src/input-otp-item/inputOtpItem.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BasicAttributes, SetPropsDefaults, VarComponent } from '../../types/varComponent'

export declare const inputOtpItemProps: Record<keyof InputOtpItemProps, any>

export interface InputOtpItemProps extends BasicAttributes {}

export class InputOtpItem extends VarComponent {
static setPropsDefaults: SetPropsDefaults<InputOtpItemProps>

$props: InputOtpItemProps
}

export class _InputOtpItemComponent extends InputOtpItem {}
6 changes: 6 additions & 0 deletions packages/varlet-ui/src/input-otp-item/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { inputProps } from '../input'
import { pickProps } from '../utils/components'

export const props = {
...pickProps(inputProps, ['variant', 'size', 'autofocus', 'textColor', 'focusColor', 'blurColor']),
}
22 changes: 22 additions & 0 deletions packages/varlet-ui/src/input-otp-item/provide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type ComputedRef } from 'vue'
import { assert } from '@varlet/shared'
import { useParent } from '@varlet/use'
import { INPUT_OTP_BIND_INPUT_OTP_ITEM_KEY, type InputOtpProvider } from '../input-otp/provide'

export interface InputOtpItemProvider {
index: ComputedRef<number>
}

export function useInputOtp() {
const { parentProvider, index, bindParent } = useParent<InputOtpProvider, InputOtpItemProvider>(
INPUT_OTP_BIND_INPUT_OTP_ITEM_KEY,
)

assert(!!bindParent, 'InputOtpItem', '<var-input-otp-item/> must in <var-input-otp/>')

return {
index,
inputOtp: parentProvider,
bindInputOtp: bindParent,
}
}
125 changes: 125 additions & 0 deletions packages/varlet-ui/src/input-otp/InputOtp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<div :class="n()">
<div :class="n('container')">
<slot />
</div>

<var-form-details :error-message="errorMessage" @mousedown.stop>
<template v-if="$slots['extra-message']" #extra-message>
<slot name="extra-message" />
</template>
</var-form-details>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, nextTick, ref } from 'vue'
import { call } from '@varlet/shared'
import { useForm } from '../form/provide'
import { createNamespace, useValidation } from '../utils/components'
import { props, type OptInputValidateTrigger } from './props'
import { useInputOtpItems, type InputOtpProvider } from './provide'

const { name, n } = createNamespace('input-otp')

export default defineComponent({
name,
props,
setup(props, { emit }) {
const { length, bindInputOtpItem } = useInputOtpItems()
const activeInput = ref<number>(-1)

const model = computed({
get: () => props.modelValue,
set: (value: string) => {
call(props.onChange, value)
call(props['onUpdate:modelValue'], value)
validateWithTrigger('onChange')
},
})

const { errorMessage, validateWithTrigger: vt, validate: v, resetValidation } = useValidation()

const inputOtpProvider: InputOtpProvider = {
parentModel: model,
activeInput,
length,
disabled: computed(() => props.disabled),
readonly: computed(() => props.readonly),
variant: computed(() => props.variant),
size: computed(() => props.size),
textColor: computed(() => props.textColor),
focusColor: computed(() => props.focusColor),
blurColor: computed(() => props.blurColor),
autofocus: computed(() => props.autofocus),
onItemChange,
onItemFocus,
onItemBlur,
reset,
validate,
resetValidation,
}

bindInputOtpItem(inputOtpProvider)

const { bindForm } = useForm()
call(bindForm, inputOtpProvider)

function validateWithTrigger(trigger: OptInputValidateTrigger) {
nextTick(() => {
const { validateTrigger, rules, modelValue } = props
vt(validateTrigger, trigger, rules, modelValue)
})
}

function onItemChange(index: number, value?: string) {
if (value == null) {
activeInput.value = index
return
}

const currentValue = model.value || ''
if (index < length.value) {
activeInput.value = value ? index + 1 : index - 1
emit('update:modelValue', currentValue.slice(0, index) + value + currentValue.slice(index + 1))
} else {
emit('update:modelValue', currentValue + value)
}
}

function onItemFocus(index: number) {
call(props.onFocus, index)
}

function onItemBlur(index: number) {
call(props.onBlur, index)
}

// expose
function reset() {
call(props['onUpdate:modelValue'], '')
resetValidation()
}

// expose
function validate() {
return v(props.rules, props.modelValue)
}

return {
length,
activeInput,
errorMessage,
n,
reset,
validate,
}
},
})
</script>

<style lang="less">
@import '../styles/common';
@import '../form-details/formDetails';
@import './inputOtp';
</style>
Loading