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
49 changes: 49 additions & 0 deletions docs/content/docs/2.components/pin-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,61 @@ props:
---
::

### Delimiter :badge{label="Soon" class="align-text-top"}

Use the `delimiterPositions` prop to insert delimiters between specific input fields. Pass an array of numbers indicating after which position to insert a delimiter.

::component-code
---
ignore:
- placeholder
props:
length: 6
placeholder: 'β—‹'
delimiterPositions: [3]
---
::

You can also insert multiple delimiters by passing an array with multiple positions:

::component-code
---
ignore:
- placeholder
props:
length: 9
placeholder: 'β—‹'
delimiterPositions: [3, 6]
---
::

Use the `delimiter` slot to customize the delimiter appearance:

::component-code
---
ignore:
- placeholder
props:
length: 6
placeholder: 'β—‹'
delimiterPositions: [3]
slots:
delimiter: |

<span class="text-primary font-bold">-</span>
---
::

## API

### Props

:component-props

### Slots

:component-slots

### Emits

:component-emits
Expand Down
1 change: 1 addition & 0 deletions playgrounds/nuxt/app/pages/components/pin-input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ const onComplete = (e: string[]) => {
<UPinInput placeholder="β—‹" highlight v-bind="props" />
<UPinInput placeholder="β—‹" disabled v-bind="props" />
<UPinInput placeholder="β—‹" required v-bind="props" />
<UPinInput placeholder="β—‹" :length="6" :delimiter-positions="[3]" v-bind="props" />
</Matrix>
</template>
40 changes: 29 additions & 11 deletions src/runtime/components/PinInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<Pin
highlight?: boolean
/** Keep the mobile text size on all breakpoints. */
fixed?: boolean
/**
* Position(s) after which to insert a delimiter.
* @example [3, 6] // Insert delimiters after 3rd and 6th inputs
*/
delimiterPositions?: number[]
Comment on lines +42 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Consider paste/autofill sanitization to fully address issue #6389.

The linked issue calls out robust paste/autofill handling β€” e.g., pasting "123-456" or "123 456" into a PIN that displays delimiters at position 3 should strip the delimiter characters and fill ["1","2","3","4","5","6"]. The current implementation is display-only (delimiter is a sibling <span>, so it never enters the inputs), which means:

  • With type="number", reka-ui already filters non-numeric input on paste β€” fine.
  • With type="text", a paste of "123-456" will distribute - into the 4th slot rather than being treated as a delimiter and skipped.

If that's intentionally out of scope for this PR, consider calling it out as a known limitation in the docs; otherwise, an @paste/update:model-value normalization step that strips characters appearing at delimiterPositions boundaries would close the gap.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/PinInput.vue` around lines 42 - 46, The component's
paste/autofill isn't sanitizing delimiter characters so pasted text like
"123-456" can populate delimiter slots; add normalization that strips any
characters at delimiterPositions boundaries before distributing into inputs:
implement an onPaste handler on the root/input elements that reads clipboard
data, removes characters at indices corresponding to delimiterPositions (or
strips known delimiter chars), and then emits the cleaned value via
update:model-value (or calls the existing value-distribution logic); also apply
the same normalization to any external updates handled by the update:model-value
path so programmatic Autofill/paste-like updates are normalized; reference
PinInput.vue props/methods: delimiterPositions, update:model-value event, and
the input distribution logic to locate where to integrate the sanitization.

class?: any
ui?: PinInput['slots']
}
Expand Down Expand Up @@ -110,6 +115,10 @@ function autoFocus() {
}
}

function shouldInsertDelimiter(index: number) {
return props.delimiterPositions?.includes((index as number) + 1) && (index as number) + 1 < looseToNumber(props.length)
}

onMounted(() => {
setTimeout(() => {
autoFocus()
Expand All @@ -134,16 +143,25 @@ defineExpose({
@update:model-value="emitFormInput()"
@complete="onComplete"
>
<PinInputInput
v-for="(ids, index) in looseToNumber(props.length)"
:key="ids"
:ref="el => setInputRef(index as number, el)"
:index="(index as number)"
data-slot="base"
:class="ui.base({ class: uiProp?.base })"
:disabled="disabled"
@blur="onBlur"
@focus="emitFormFocus"
/>
<template v-for="(ids, index) in looseToNumber(props.length)" :key="ids">
<PinInputInput
:ref="el => setInputRef(index as number, el)"
:index="(index as number)"
data-slot="base"
:class="ui.base({ class: uiProp?.base })"
:disabled="disabled"
@blur="onBlur"
@focus="emitFormFocus"
/>
<span
v-if="shouldInsertDelimiter(index as number)"
data-slot="delimiter"
aria-hidden="true"
>
<slot name="delimiter">
<span class="text-dimmed">β€’</span>
</slot>
</span>
</template>
</PinInputRoot>
</template>
Loading
Loading