import type { PinInputType } from '../utils/pin-chars.js'; import type { PrimitivePropsWithRef } from '../utils/primitive.js'; /** What a rejected character reports: the character, and the box it was aimed at. */ export interface PinInputInvalidDetail { /** The character the `type`/`pattern` filter refused. */ value: string; /** The 0-based index of the box it was typed or pasted into. */ index: number; } /** @internal Shared state distributed from {@link PinInput} to its `PinInputField`s via context. */ export interface PinInputContextValue { /** The joined value, whose identity change re-renders boxes that read {@link charAt} instead. */ value: string; length: number; type: PinInputType; mask: boolean; disabled: boolean; invalid: boolean; readOnly: boolean; /** A single character drawn in every empty box, or `undefined` for none. */ placeholder?: string | undefined; /** Whether box 0 advertises `autocomplete="one-time-code"`. */ otp: boolean; /** Whether a box selects the character it already holds when focus lands in it. */ selectOnFocus: boolean; /** Whether this character passes the root's `type`/`pattern` filter, so a keystroke and a paste are judged alike. */ accepts: (char: string) => boolean; /** Publishes `value-invalid` for a character the filter refused. */ reportInvalid: (char: string, index: number) => void; /** The character in the box at `index`, read from the fixed-length slot array so boxes never shift left. */ charAt: (index: number) => string; /** Writes one box's character (`''` clears it) and publishes the joined value. */ setCharAt: (index: number, char: string) => void; /** Writes consecutive boxes from `index` onward, truncating at the last, returning the last index written. */ fillFrom: (index: number, chars: string) => number; focusIndex: (index: number) => void; registerRef: (index: number, node: HTMLInputElement | null) => void; sanitizePaste: (pasted: string) => string; /** Words a box's `aria-label` from its 1-based position and the pin's length. */ digitLabel: (index: number, length: number) => string; } /** Props for the {@link PinInput} root. */ export interface PinInputProps extends Omit, 'defaultValue' | 'onChange'> { /** * Current value, for controlled usage. Pair with `onValueChange`. * @default undefined — uncontrolled, tracks its own value internally. */ value?: string | undefined; /** * Initial combined value, for uncontrolled usage. * @default '' */ defaultValue?: string | undefined; /** * Fires with the new value whenever any box changes, controlled or not. * @default undefined — no-op if omitted. */ onValueChange?: ((value: string) => void) | undefined; /** * Number of character boxes rendered and the max length of the combined value. * @default 6 * @min 1 */ length?: number | undefined; /** * Disables every `PinInputField` in this pin input. * @default false */ disabled?: boolean | undefined; /** * Which characters a box accepts; `pattern` overrides it entirely. * @default 'text' */ type?: PinInputType | undefined; /** * A regular-expression source tested against **one character at a time**, replacing whatever set `type` names. * @default undefined — `type` decides. */ pattern?: string | undefined; /** * A single character drawn in every empty box, so the boxes read as slots waiting for a code. * @default undefined — empty boxes are blank. */ placeholder?: string | undefined; /** * Advertises `autocomplete="one-time-code"` on the first box only — a second such field suppresses both. * @default true */ otp?: boolean | undefined; /** * Focuses the first box on mount. * @default false */ autoFocus?: boolean | undefined; /** * Blurs the focused box the moment the code completes, running after `onValueComplete`. * @default false */ blurOnComplete?: boolean | undefined; /** * Selects a box's existing character when focus lands in it, on click, Tab and auto-advance alike. * @default false */ selectOnFocus?: boolean | undefined; /** * Fires once per character the `type`/`pattern` filter refused, with the index of the box it was aimed at. * @default undefined — no-op if omitted. */ onValueInvalid?: ((detail: PinInputInvalidDetail) => void) | undefined; /** * Renders filled boxes as `type="password"` (dots) instead of plain text. * @default false */ mask?: boolean | undefined; /** * Marks the field as invalid: `aria-invalid` on the `role="group"` root, * `data-invalid` on the root and every field. * @default false */ invalid?: boolean | undefined; /** * Makes every `PinInputField` read-only (native `readOnly`, plus * `aria-readonly`/`data-readonly` on the root and each field). * @default false */ readOnly?: boolean | undefined; /** * Fires with the full value once per completion, not on every keystroke that keeps it complete. * @default undefined — no-op if omitted. */ onValueComplete?: ((value: string) => void) | undefined; /** * Submit the surrounding `
` (via `requestSubmit`, so validation and * the submit event still run) as soon as the value completes. * @default false */ autoSubmit?: boolean | undefined; /** * Cleans pasted text before it is distributed across the boxes, running before the `type` filter. * @default undefined — pasted text is used as-is. */ sanitize?: ((pasted: string) => string) | undefined; /** * Form field name; a hidden input submits the joined value as one entry, matching what `` publishes. * @default undefined — submits nothing. */ name?: string | undefined; /** * Id of the `` this pin submits into, for a pin rendered outside it (a portalled Dialog). * @default undefined — the pin belongs to its nearest ancestor ``, if any. */ form?: string | undefined; /** * Marks the pin required: with `name`, native `valueMissing` until every box * is filled, reported through `checkValidity()`/`reportValidity()`. * @default false */ required?: boolean | undefined; /** * Words each `PinInputField`'s `aria-label` from the box's **1-based** position and the pin's `length`, unless the field sets its own. * @default {@link pinInputDigitLabel} — `` `Digit ${index} of ${length}` `` */ digitLabel?: ((index: number, length: number) => string) | undefined; /** * Spacing rung the skin packs the boxes by, mirrored to `data-density`. * @default undefined — the skin's own default spacing. */ density?: 'default' | 'compact' | 'loose' | (string & {}) | undefined; /** * Corner rung the skin rounds the boxes by, mirrored to `data-shape`. * @default undefined — the skin's own default corner. */ shape?: 'rounded' | 'square' | 'pill' | (string & {}) | undefined; /** * Size rung the skin scales the boxes by, mirrored to `data-size`. * @default undefined — the skin's own default size. */ size?: 'md' | 'sm' | 'lg' | (string & {}) | undefined; } /** What {@link PinInput}'s forwarded ref carries: the root `
` with the constraint-validation methods, `clear()` and `focus()` assigned onto it. */ export interface PinInputHandle extends HTMLDivElement { /** Returns whether the current code satisfies its constraints, firing `invalid` if not. */ checkValidity(): boolean; /** Like {@link PinInputHandle.checkValidity}, but also shows the browser's validation message. */ reportValidity(): boolean; /** Pushes an externally-computed error in, exactly like `HTMLInputElement.setCustomValidity()`; `''` clears it. */ setCustomValidity(message: string): void; /** Empties every box and publishes the empty value — the only imperative writer, since `value` is already controllable. */ clear(): void; /** Moves DOM focus into the first box, shadowing the root `
`'s own no-op `focus()`. */ focus(): void; } /** Props for a single {@link PinInputField} character box. */ export interface PinInputFieldProps extends Omit, 'value' | 'defaultValue' | 'type'> { /** This box's 0-indexed position within the pin's value. */ index: number; }