Skip to content

Commit e516518

Browse files
J-Michalekzernonia
andauthored
feat(PinInput)!: add support for numeric type (#1878)
* feat(pin-input): support number[] as possible value * feat(pin-input): use generic type for the type prop * feat(pin-input): use generic type for the type prop BREAKING: Require number[] for number type and convert the value to number. * test(pin-input): expect numbers to be emitted for number type * fix: set default type as text --------- Co-authored-by: zernonia <zernonia@gmail.com>
1 parent 570c587 commit e516518

File tree

4 files changed

+31
-26
lines changed

4 files changed

+31
-26
lines changed

packages/core/src/PinInput/PinInput.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ describe('give PinInput type=number', async () => {
215215
})
216216

217217
it('should emit \'complete\' with the result', () => {
218-
expect(wrapper.emitted('complete')?.[0]?.[0]).toStrictEqual(['1', '2', '3', '4', '5'])
218+
expect(wrapper.emitted('complete')?.[0]?.[0]).toStrictEqual([1, 2, 3, 4, 5])
219219
})
220220
})
221221
})

packages/core/src/PinInput/PinInputInput.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { PrimitiveProps } from '@/Primitive'
33
import { Primitive, usePrimitiveElement } from '@/Primitive'
44
import { getActiveElement, useArrowNavigation } from '@/shared'
5-
import { injectPinInputRootContext } from './PinInputRoot.vue'
5+
import { type PinInputValue, injectPinInputRootContext } from './PinInputRoot.vue'
66
77
export interface PinInputInputProps extends PrimitiveProps {
88
/** Position of the value this input binds to. */
@@ -115,7 +115,7 @@ function handlePaste(event: ClipboardEvent) {
115115
}
116116
117117
function handleMultipleCharacter(values: string) {
118-
const tempModelValue = [...context.currentModelValue.value]
118+
const tempModelValue = [...context.currentModelValue.value] as PinInputValue<typeof context.type.value>
119119
const initialIndex = values.length >= inputElements.value.length ? 0 : props.index
120120
const lastIndex = Math.min(initialIndex + values.length, inputElements.value.length)
121121
for (let i = initialIndex; i < lastIndex; i++) {
@@ -131,7 +131,7 @@ function handleMultipleCharacter(values: string) {
131131
inputElements.value[lastIndex]?.focus()
132132
}
133133
134-
function removeTrailingEmptyStrings(input: string[]) {
134+
function removeTrailingEmptyStrings(input: PinInputValue<typeof context.type.value>) {
135135
let i = input.length - 1
136136
137137
while (i >= 0 && input[i] === '') {
@@ -143,8 +143,8 @@ function removeTrailingEmptyStrings(input: string[]) {
143143
}
144144
145145
function updateModelValueAt(index: number, value: string) {
146-
const tempModelValue = [...context.currentModelValue.value]
147-
tempModelValue[index] = value
146+
const tempModelValue = [...context.currentModelValue.value] as PinInputValue<typeof context.type.value>
147+
tempModelValue[index] = isNumericMode.value ? +value : value
148148
context.modelValue.value = removeTrailingEmptyStrings(tempModelValue)
149149
}
150150

packages/core/src/PinInput/PinInputRoot.vue

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,29 @@ import { createContext, useDirection, useForwardExpose } from '@/shared'
66
import VisuallyHiddenInput from '@/VisuallyHidden/VisuallyHiddenInput.vue'
77
import { computed, ref, toRefs, watch } from 'vue'
88
9-
export type PinInputRootEmits = {
10-
'update:modelValue': [value: string[]]
11-
'complete': [value: string[]]
9+
export type PinInputType = 'text' | 'number'
10+
11+
// Using this type to avoid mixed arrays (string | number)[].
12+
export type PinInputValue<Type extends PinInputType = 'text'> = Type extends 'number' ? number[] : string[]
13+
14+
export type PinInputRootEmits<Type extends PinInputType = 'text'> = {
15+
'update:modelValue': [value: PinInputValue<Type>]
16+
'complete': [value: PinInputValue<Type>]
1217
}
1318
14-
export interface PinInputRootProps extends PrimitiveProps, FormFieldProps {
19+
export interface PinInputRootProps<Type extends PinInputType = 'text'> extends PrimitiveProps, FormFieldProps {
1520
/** The controlled checked state of the pin input. Can be binded as `v-model`. */
16-
modelValue?: string[] | null
21+
modelValue?: PinInputValue<Type> | null
1722
/** The default value of the pin inputs when it is initially rendered. Use when you do not need to control its checked state. */
18-
defaultValue?: string[]
23+
defaultValue?: PinInputValue<Type>[]
1924
/** The placeholder character to use for empty pin-inputs. */
2025
placeholder?: string
2126
/** When `true`, pin inputs will be treated as password. */
2227
mask?: boolean
2328
/** When `true`, mobile devices will autodetect the OTP from messages or clipboard, and enable the autocomplete field. */
2429
otp?: boolean
2530
/** Input type for the inputs. */
26-
type?: 'text' | 'number'
31+
type?: Type
2732
/** The reading direction of the combobox when applicable. <br> If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. */
2833
dir?: Direction
2934
/** When `true`, prevents the user from interacting with the pin input */
@@ -32,13 +37,13 @@ export interface PinInputRootProps extends PrimitiveProps, FormFieldProps {
3237
id?: string
3338
}
3439
35-
export interface PinInputRootContext {
36-
modelValue: Ref<string[]>
37-
currentModelValue: ComputedRef<string[]>
40+
export interface PinInputRootContext<Type extends PinInputType = 'text'> {
41+
modelValue: Ref<PinInputValue<Type>>
42+
currentModelValue: ComputedRef<PinInputValue<Type>>
3843
mask: Ref<boolean>
3944
otp: Ref<boolean>
4045
placeholder: Ref<string>
41-
type: Ref<PinInputRootProps['type']>
46+
type: Ref<PinInputType>
4247
dir: Ref<Direction>
4348
disabled: Ref<boolean>
4449
isCompleted: ComputedRef<boolean>
@@ -47,22 +52,22 @@ export interface PinInputRootContext {
4752
}
4853
4954
export const [injectPinInputRootContext, providePinInputRootContext]
50-
= createContext<PinInputRootContext>('PinInputRoot')
55+
= createContext<PinInputRootContext<PinInputType>>('PinInputRoot')
5156
</script>
5257

53-
<script setup lang="ts">
58+
<script setup lang="ts" generic="Type extends PinInputType = 'text'">
5459
import { Primitive } from '@/Primitive'
5560
import { useVModel } from '@vueuse/core'
5661
5762
defineOptions({
5863
inheritAttrs: false,
5964
})
6065
61-
const props = withDefaults(defineProps<PinInputRootProps>(), {
66+
const props = withDefaults(defineProps<PinInputRootProps<Type>>(), {
6267
placeholder: '',
63-
type: 'text',
68+
type: 'text' as any,
6469
})
65-
const emits = defineEmits<PinInputRootEmits>()
70+
const emits = defineEmits<PinInputRootEmits<Type>>()
6671
6772
defineSlots<{
6873
default: (props: {
@@ -76,9 +81,9 @@ const { forwardRef } = useForwardExpose()
7681
const dir = useDirection(propDir)
7782
7883
const modelValue = useVModel(props, 'modelValue', emits, {
79-
defaultValue: props.defaultValue ?? [],
84+
defaultValue: props.defaultValue ?? [] as any,
8085
passive: (props.modelValue === undefined) as false,
81-
}) as Ref<string[]>
86+
}) as Ref<PinInputValue<Type>>
8287
8388
const currentModelValue = computed(() => Array.isArray(modelValue.value) ? [...modelValue.value] : [])
8489
@@ -99,7 +104,7 @@ watch(modelValue, () => {
99104
100105
providePinInputRootContext({
101106
modelValue,
102-
currentModelValue,
107+
currentModelValue: currentModelValue as ComputedRef<PinInputValue<Type>>,
103108
mask,
104109
otp,
105110
placeholder,

packages/core/src/PinInput/story/PinInputNumeric.story.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { ref } from 'vue'
33
import { PinInputInput, PinInputRoot } from '..'
44
5-
const value = ref<string[]>([])
5+
const value = ref<number[]>([])
66
</script>
77

88
<template>

0 commit comments

Comments
 (0)