-
Notifications
You must be signed in to change notification settings - Fork 611
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(PinInput): implement component (#2570)
Co-authored-by: Max Steinwand <[email protected]> Co-authored-by: Benjamin Canac <[email protected]> Co-authored-by: Romain Hamel <[email protected]>
- Loading branch information
1 parent
f516d7b
commit 95aa6f6
Showing
32 changed files
with
1,580 additions
and
654 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,10 +29,11 @@ const schema = z.object({ | |
radioGroup: z.string().refine(value => value === 'option-2', { | ||
message: 'Select Option 2' | ||
}), | ||
slider: z.number().max(20, { message: 'Must be less than 20' }) | ||
slider: z.number().max(20, { message: 'Must be less than 20' }), | ||
pin: z.string().regex(/^\d$/).array().length(5) | ||
}) | ||
type Schema = z.output<typeof schema> | ||
type Schema = z.input<typeof schema> | ||
const state = reactive<Partial<Schema>>({}) | ||
|
@@ -52,7 +53,7 @@ async function onSubmit(event: FormSubmitEvent<any>) { | |
</script> | ||
|
||
<template> | ||
<UForm ref="form" :state="state" :schema="schema" @submit="onSubmit"> | ||
<UForm ref="form" :state="state" :schema="schema" class="w-full" @submit="onSubmit"> | ||
<div class="grid grid-cols-3 gap-4"> | ||
<UFormField label="Input" name="input"> | ||
<UInput v-model="state.input" placeholder="[email protected]" class="w-40" /> | ||
|
@@ -101,6 +102,12 @@ async function onSubmit(event: FormSubmitEvent<any>) { | |
<UFormField name="radioGroup"> | ||
<URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" /> | ||
</UFormField> | ||
|
||
<span /> | ||
|
||
<UFormField name="pin" label="Pin Input" :error-pattern="/(pin)\..*/"> | ||
<UPinInput v-model="state.pin" /> | ||
</UFormField> | ||
</div> | ||
|
||
<div class="flex gap-2 mt-8"> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
--- | ||
title: PinInput | ||
description: An input element to enter a pin. | ||
links: | ||
- label: GitHub | ||
icon: i-simple-icons-github | ||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/PinInput.vue | ||
--- | ||
|
||
## Usage | ||
|
||
Use the `v-model` directive to control the value of the PinInput. | ||
|
||
::component-code | ||
--- | ||
prettier: true | ||
ignore: | ||
- modelValue | ||
external: | ||
- modelValue | ||
props: | ||
modelValue: [] | ||
--- | ||
:: | ||
|
||
Use the `default-value` prop to set the initial value when you do not need to control its state. | ||
|
||
::component-code | ||
--- | ||
prettier: true | ||
ignore: | ||
- defaultValue | ||
props: | ||
defaultValue: ['1','2','3'] | ||
--- | ||
:: | ||
|
||
### Type | ||
|
||
Use the `type` prop to change the input type. Defaults to `text`. | ||
|
||
::component-code | ||
--- | ||
items: | ||
type: | ||
- text | ||
- number | ||
props: | ||
type: 'number' | ||
--- | ||
:: | ||
|
||
::note | ||
When `type` is set to `number`, it will only accept numeric characters. | ||
:: | ||
|
||
### Mask | ||
|
||
Use the `mask` prop to treat the input like a password. | ||
|
||
::component-code | ||
--- | ||
prettier: true | ||
ignore: | ||
- placeholder | ||
- defaultValue | ||
props: | ||
mask: true | ||
defaultValue: ['1','2','3','4','5'] | ||
--- | ||
:: | ||
|
||
### OTP | ||
|
||
Use the `otp` prop to enable One-Time Password functionality. When enabled, mobile devices can automatically detect and fill OTP codes from SMS messages or clipboard content, with autocomplete support. | ||
|
||
::component-code | ||
--- | ||
props: | ||
otp: true | ||
--- | ||
:: | ||
|
||
### Length | ||
|
||
Use the `length` prop to change the amount of inputs. | ||
|
||
::component-code | ||
--- | ||
props: | ||
length: 6 | ||
--- | ||
:: | ||
|
||
### Placeholder | ||
|
||
Use the `placeholder` prop to set a placeholder text. | ||
|
||
::component-code | ||
--- | ||
props: | ||
placeholder: '○' | ||
--- | ||
:: | ||
|
||
### Color | ||
|
||
Use the `color` prop to change the ring color when the PinInput is focused. | ||
|
||
::component-code | ||
--- | ||
ignore: | ||
- placeholder | ||
props: | ||
color: neutral | ||
highlight: true | ||
placeholder: '○' | ||
--- | ||
:: | ||
|
||
::note | ||
The `highlight` prop is used here to show the focus state. It's used internally when a validation error occurs. | ||
:: | ||
|
||
### Variant | ||
|
||
Use the `variant` prop to change the variant of the PinInput. | ||
|
||
::component-code | ||
--- | ||
ignore: | ||
- placeholder | ||
props: | ||
color: neutral | ||
variant: subtle | ||
highlight: false | ||
placeholder: '○' | ||
--- | ||
:: | ||
|
||
### Size | ||
|
||
Use the `size` prop to change the size of the PinInput. | ||
|
||
::component-code | ||
--- | ||
ignore: | ||
- placeholder | ||
props: | ||
size: xl | ||
placeholder: '○' | ||
--- | ||
:: | ||
|
||
### Disabled | ||
|
||
Use the `disabled` prop to disable the PinInput. | ||
|
||
::component-code | ||
--- | ||
ignore: | ||
- placeholder | ||
props: | ||
disabled: true | ||
placeholder: '○' | ||
--- | ||
:: | ||
|
||
## API | ||
|
||
### Props | ||
|
||
:component-props | ||
|
||
### Emits | ||
|
||
:component-emits | ||
|
||
## Theme | ||
|
||
:component-theme |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<script setup lang="ts"> | ||
import theme from '#build/ui/pin-input' | ||
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size> | ||
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant> | ||
const onComplete = (e: string[]) => { | ||
alert(e.join('')) | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="flex flex-col items-center gap-4"> | ||
<div class="flex gap-4"> | ||
<UPinInput placeholder="○" autofocus @complete="onComplete" /> | ||
</div> | ||
<div class="flex items-center gap-4"> | ||
<UPinInput v-for="variant in variants" :key="variant" placeholder="○" :variant="variant" /> | ||
</div> | ||
<div class="flex items-center gap-4"> | ||
<UPinInput | ||
v-for="variant in variants" | ||
:key="variant" | ||
placeholder="○" | ||
:variant="variant" | ||
color="neutral" | ||
/> | ||
</div> | ||
<div class="flex items-center gap-4"> | ||
<UPinInput | ||
v-for="variant in variants" | ||
:key="variant" | ||
placeholder="○" | ||
:variant="variant" | ||
color="error" | ||
highlight | ||
/> | ||
</div> | ||
<div class="flex flex-col gap-4"> | ||
<UPinInput placeholder="○" disabled /> | ||
<UPinInput placeholder="○" required /> | ||
</div> | ||
<div class="flex items-center gap-4"> | ||
<UPinInput | ||
v-for="size in sizes" | ||
:key="size" | ||
placeholder="○" | ||
:size="size" | ||
/> | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<script lang="ts"> | ||
import _appConfig from '#build/app.config' | ||
import theme from '#build/ui/pin-input' | ||
import type { AppConfig } from '@nuxt/schema' | ||
import type { PinInputRootEmits, PinInputRootProps } from 'radix-vue' | ||
import { tv, type VariantProps } from 'tailwind-variants' | ||
import type { PartialString } from '../types/utils' | ||
const appConfig = _appConfig as AppConfig & { ui: { pinInput: Partial<typeof theme> } } | ||
const pinInput = tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) }) | ||
type PinInputVariants = VariantProps<typeof pinInput> | ||
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> { | ||
/** | ||
* The element or component this component should render as. | ||
* @defaultValue 'div' | ||
*/ | ||
as?: any | ||
color?: PinInputVariants['color'] | ||
variant?: PinInputVariants['variant'] | ||
size?: PinInputVariants['size'] | ||
length?: number | string | ||
highlight?: boolean | ||
class?: any | ||
ui?: PartialString<typeof pinInput.slots> | ||
} | ||
export type PinInputEmits = PinInputRootEmits & { | ||
change: [payload: Event] | ||
blur: [payload: Event] | ||
} | ||
</script> | ||
<script setup lang="ts"> | ||
import { ref, computed } from 'vue' | ||
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'radix-vue' | ||
import { reactivePick } from '@vueuse/core' | ||
import { looseToNumber } from '../utils' | ||
defineOptions({ inheritAttrs: false }) | ||
const props = withDefaults(defineProps<PinInputProps>(), { | ||
type: 'text', | ||
length: 5 | ||
}) | ||
const emits = defineEmits<PinInputEmits>() | ||
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits) | ||
const { emitFormInput, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled } = useFormField<PinInputProps>(props) | ||
const ui = computed(() => pinInput({ | ||
color: color.value, | ||
variant: props.variant, | ||
size: size.value, | ||
highlight: highlight.value | ||
})) | ||
const completed = ref(false) | ||
function onComplete(value: string[]) { | ||
// @ts-expect-error - 'target' does not exist in type 'EventInit' | ||
const event = new Event('change', { target: { value } }) | ||
emits('change', event) | ||
emitFormChange() | ||
} | ||
function onBlur(event: FocusEvent) { | ||
if (!event.relatedTarget || completed.value) { | ||
emits('blur', event) | ||
emitFormBlur() | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<PinInputRoot | ||
v-bind="rootProps" | ||
:id="id" | ||
:name="name" | ||
:class="ui.root({ class: [props.class, props.ui?.root] })" | ||
@update:model-value="emitFormInput()" | ||
@complete="onComplete" | ||
> | ||
<PinInputInput | ||
v-for="(ids, index) in looseToNumber(props.length)" | ||
:key="ids" | ||
:index="index" | ||
:class="ui.base({ class: props.ui?.base })" | ||
v-bind="$attrs" | ||
:disabled="disabled" | ||
@blur="onBlur" | ||
/> | ||
</PinInputRoot> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.