GrOtpInput

  • ядро
  • Формы
  • API · 18 props
  • ⌨ клавиши

Берут, когда код приходит извне.

import { GrOtpInput } from '@feugene/granularity/components/GrOtpInput'

Обзор

Когда брать

  • код приходит извне — из SMS, письма или приложения: длина известна заранее, и поле её показывает;
  • проверка идёт сразу по заполнению — complete наступает без кнопки «Отправить»;
  • PIN на экране блокировки — masked прячет символы, поведение остаётся тем же;
  • код читают вслух или списывают глазами — groups разбивает его на части, как номер карты.

Когда взять другое

НужноБерите
Строка произвольной длиныGrInput
Пароль, который печатают, а не получаютGrInput с type="password"
Число со ступенями и границамиGrNumberInput
Пользователь придумывает значения самGrInputTag

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Код входа из SMS

    groups разбивает код так, как он напечатан в сообщении, а complete проверяет его без кнопки «Отправить». one-time-code даёт телефону подставить код из сообщения. Отклонённый код очищается через clear(), фокус остаётся, и новая попытка начинается с первой ячейки.

    Messages · nowNorthwind: 482 913 is your sign-in code. Do not share it.
    Check your phoneWe sent a code to •• 4821.
    The code works for 10 minutes.
  • PIN новой карты

    masked показывает точки там, где вероятен чужой взгляд, а модель хранит цифры. invalid отмечает весь повтор PIN — одну попытку, а не четыре неверные ячейки.

    Set a PIN for your new cardVisa Business •••• 7720
    New PIN
    Repeat the PIN
  • Код привязки телевизора

    Код, который списывают с экрана, обходит похожие знаки: pattern описывает один символ, поэтому O, 0, I и 1 в модель не попадают, а вставленное «k7pm-w2qx» оставляет только подходящее. Слот cell показывает буквы заглавными, как на телевизоре, а separator ставит точку между группами.

    Connect a TVType the code shown on the TV screen, e.g. K7PM·W2QX.

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
disabledboolean | undefinedfalseВыключить поле: код не вводится и не отправляется с формой. Складывается с `GrFormField`.
lengthnumber | undefinedundefinedЧисло ячеек.
type"text" | "numeric" | "alphanumeric" | undefined"numeric"Допустимый алфавит и раскладка мобильной клавиатуры.
modelValuev-modelstring | undefined""Код целиком, без разделителей.
readonlyboolean | undefinedfalseТолько для чтения: код видно, но не изменить.
invalidboolean | undefinedfalseОшибка — например, сервер отверг код: рамка ячеек и `aria-invalid`. Складывается с `GrFormField`.
requiredboolean | undefinedfalseОбязательное поле (`aria-required`). Складывается с `required` у `GrFormField`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер ячеек и кегль символов. Не задан — из `GrConfigProvider`, иначе `md`.
placeholderstring | undefined""Знак в пустой ячейке.
ariaLabelstring | undefinedundefinedДоступное имя вне `GrFormField`: ячейки скрыты от скринридера, читается одно поле поверх них.
namestring | undefinedundefinedИмя поля для нативной отправки формы.
formstring | undefinedundefined`id` формы, если поле лежит вне неё.
patternstring | undefinedundefinedСвой алфавит регуляркой на **один символ**. Перекрывает `type`.
groupsnumber[] | undefinedundefinedРазбивка на группы: `[3, 3]` рисует `123 – 456`. Сумма обязана равняться `length`.

Слоты

SlotTypeОписание
cell
{ char: string; index: number; active: boolean; filled: bool{ char: string; index: number; active: boolean; filled: boolean; masked: boolean; }
Своё оформление ячейки. Размер и рамку по-прежнему держит компонент.
separator{ index: number; }Знак между группами. По умолчанию — тире.

События

EventСигнатураОписание
update:modelValue(event: "update:modelValue", value: string): void—
change(event: "change", value: string): void—
focus(event: "focus", event: FocusEvent): void—
blur(event: "blur", event: FocusEvent): void—
complete(event: "complete", value: string): void—

Методы

MethodsСигнатураОписание
focus() => voidФокус в поле кода.
blur() => voidСнять фокус с поля кода.
clear() => voidСтереть код и вернуть фокус в первую ячейку — когда сервер отверг код и ждёт новый.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrOtpInputConfigurableProps, GrOtpInputProps, GrOtpInputEmits } from '@feugene/granularity/components/GrOtpInput'

Файлdist/types/components/GrOtpInput/config.d.ts

grOtpInputConfigconst
const grOtpInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrOtpInput">;

Файлdist/types/components/GrOtpInput/defaults.d.ts

GrOtpInputConfigurablePropsinterface

Пропы GrOtpInput, настраиваемые глобально через componentDefaults. Только оформление и поведение по умолчанию — см. GrButton/defaults.ts.

interface GrOtpInputConfigurableProps {
    size: GrComponentSize;
    length: number;
    masked: boolean;
}

Файлdist/types/components/GrOtpInput/safelist.d.ts

grOtpInputSafelistconst
const grOtpInputSafelist: string[];

Файлdist/types/components/GrOtpInput/GrOtpInput.vue.d.ts

GrOtpInputPropsinterface
interface GrOtpInputProps {
    /** Код целиком, без разделителей. */
    modelValue?: string;
    /** Число ячеек. */
    length?: number;
    /** Допустимый алфавит и раскладка мобильной клавиатуры. */
    type?: GrOtpInputType;
    /** Свой алфавит регуляркой на **один символ**. Перекрывает `type`. */
    pattern?: string;
    /** Разбивка на группы: `[3, 3]` рисует `123 – 456`. Сумма обязана равняться `length`. */
    groups?: number[];
    /** Точки вместо символов — сценарий PIN. На `modelValue` не влияет. */
    masked?: boolean;
    /** Знак в пустой ячейке. */
    placeholder?: string;
    /** Размер ячеек и кегль символов. Не задан — из `GrConfigProvider`, иначе `md`. */
    size?: GrOtpInputSize;
    /** Фокус в поле сразу после монтирования — для экрана, где ввод кода и есть всё действие. */
    autofocus?: boolean;
    /** `autocomplete="one-time-code"`: код из SMS подставляется на iOS и macOS. */
    oneTimeCode?: boolean;
    /**
     * Приём кода из SMS на Android через WebOTP. Выключен: требует HTTPS и
     * особого хвоста в тексте сообщения, который добавляет бэкенд.
     */
    webOtp?: boolean;
    /** Имя поля для нативной отправки формы. */
    name?: string;
    /** `id` формы, если поле лежит вне неё. */
    form?: string;
    /** Выключить поле: код не вводится и не отправляется с формой. Складывается с `GrFormField`. */
    disabled?: boolean;
    /** Только для чтения: код видно, но не изменить. */
    readonly?: boolean;
    /** Ошибка — например, сервер отверг код: рамка ячеек и `aria-invalid`. Складывается с `GrFormField`. */
    invalid?: boolean;
    /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */
    required?: boolean;
    /** Доступное имя вне `GrFormField`: ячейки скрыты от скринридера, читается одно поле поверх них. */
    ariaLabel?: string;
}
GrOtpInputEmitsinterface
interface GrOtpInputEmits {
    /** Код изменился (`v-model`) — уже очищенный от недопустимых символов. */
    (e: 'update:modelValue', value: string): void;
    /** Код изменился — то же значение и в тот же момент, что у `update:modelValue`. */
    (e: 'change', value: string): void;
    /** Фокус пришёл в поле. */
    (e: 'focus', event: FocusEvent): void;
    /** Фокус ушёл из поля. */
    (e: 'blur', event: FocusEvent): void;
    /** Код набран целиком. Точка автосабмита. */
    (e: 'complete', value: string): void;
}

Файлdist/types/components/GrOtpInput/grOtpInputStyles.d.ts

GrOtpInputSizetype
type GrOtpInputSize = GrComponentSize;

Файлdist/types/components/GrOtpInput/otpValue.d.ts

GrOtpInputTypetype
type GrOtpInputType = typeof GR_OTP_INPUT_TYPES[number];

Файлdist/types/components/GrOtpInput/index.d.ts

GrOtpInputInstancetype
type GrOtpInputInstance = ComponentExposed<typeof GrOtpInputComponent>;

Доступность

Паттерн APG

—

КлавишаДействие
—клавиатура нативная: поле одно, каретка настоящая. Печать **заменяет** символ под кареткой, а не сдвигает хвост; Backspace и Delete стирают; ←/→ ходят по ячейкам; выделение и отмена — как в обычном поле. К краям строки — тем сочетанием, которое даёт платформа (Home/End на macOS каретку в поле не двигают). Всё поле — **одна** остановка Tab по построению, а не через roving tabindex

Использование из прозы библиотеки

Ячеек много, поле одно

Компонент рисует ряд ячеек, но в DOM это один <input>, растянутый поверх ряда и невидимый: текст, каретка и подсветка выделения прозрачны. Ячейки — декорация с aria-hidden.

Это несущее решение, и почти всё поведение поля следует из него:

здесьшесть отдельных полей
Вставка коданативнаяраспределять по ячейкам руками
Код из SMS на iOSautocomplete="one-time-code" работаетподставляется только в первую ячейку
Выделение, Ctrl+A, отменанативныенет
Стрелки, Home/Endнативная кареткаписать руками
Таб-порядокодна остановка по построениюroving tabindex руками
Диктор«поле ввода, 123456»шесть безымянных полей подряд

Требование пакета «составной виджет — одна остановка Tab» (../keyboard.md) выполняется здесь устройством, а не поверх него.

Цена — одна: активная ячейка не хранится, а выводится из положения каретки настоящего поля.

Печать заменяет, а не сдвигает

Единственное место, где поведение расходится с обычным полем. Длина фиксирована, поэтому символ, напечатанный в занятую ячейку, заменяет её содержимое, а хвост кода остаётся на месте.

Нормализация висит на событии input, а не на beforeinput, и это не деталь реализации: мобильные клавиатуры и автоподстановка присылают целую строку одним событием. Обработчик обязан одинаково принимать и один символ с клавиатуры, и шесть из сообщения — различить их по событию нельзя.

Символы вне алфавита отбрасываются молча, а не блокируют ввод: 123-456, скопированное из мессенджера, даёт 123456. Это главный сценарий вставки, и он не должен требовать от пользователя чистить строку руками.

Клик ставит каретку в первую пустую ячейку

А не туда, куда пришёлся курсор. Ячейки — декорация, попадание мышью по невидимому тексту ничего осмысленного не значит, а «продолжить с того места, где остановился» — единственное, чего от кода ждут. Редактирование середины остаётся за стрелками и Backspace.

Код из сообщения подставляется сам

Два канала, и вместе они закрывают обе мобильные платформы:

ПропПлатформаСостояние
oneTimeCodeiOS, macOSвключён: autocomplete="one-time-code" ничего не стоит и работает сам
webOtpAndroidвыключен

webOtp выключен не из осторожности: WebOTP требует HTTPS и особого хвоста в тексте сообщения (@example.com #123456), который добавляет бэкенд. Включённый по умолчанию, он молча не работал бы у всех, кто об этом не знает.

Запрос живёт в onMounted и снимается при заполнении кода и при размонтировании: navigator в теле setup роняет серверный рендер (../ssr.md), а незакрытый запрос переживает уход со страницы. Отсутствие API, отказ пользователя и отмена — не ошибки: поле просто ждёт ручного ввода.

Алфавит
typeЧто принимаетinputmode
numericцифрыnumeric
alphanumericцифры и латиницу, приводя к верхнему региструtext
textлюбой непробельный символtext

pattern задаёт свой алфавит регуляркой на один символ и перекрывает type целиком — вместе с приведением регистра, потому что своя регулярка сама решает, какой регистр допустим. Сломанная регулярка не роняет рендер: компонент предупреждает в dev и возвращается к типу.

Маска — визуальная

masked рисует точки, но значение поля остаётся настоящим кодом. Так и должно быть: type="password" сломал бы one-time-code, а прятать значение не от кого — текст настоящего поля и так прозрачен.

Оформление

size — xs…lg; сторона ячейки повторяет высоту GrInput на той же ступени, чтобы код садился в одну линию с полями формы. Точки настройки — хуки --gr-otp-input-cell-size, --gr-otp-input-active-brd и --gr-otp-input-caret (../tokens.md).

Каретка рисуется своя, потому что настоящая скрыта вместе с текстом. Мигание — CSS-анимация, поэтому подчиняется глобальному клампу движения пакета (../motion.md): под prefers-reduced-motion каретка останавливается видимой, то есть ровно тем, чем и должна быть в поле под фокусом.

Слоты cell и separator меняют оформление, не трогая поведение: размер, рамку и каретку по-прежнему держит компонент.

Границы
  • таймер повторной отправки — состояние экрана, а не поля;
  • проверка кода — приложение слушает complete и решает само;
  • формат SMS под WebOTP — хвост @domain #code добавляет бэкенд;
  • маска ввода общего назначения — телефон и дата это другая задача.