GrOtpInput
Берут, когда код приходит извне.
Обзор
Когда брать
- код приходит извне — из SMS, письма или приложения: длина известна заранее, и поле её показывает;
- проверка идёт сразу по заполнению —
completeнаступает без кнопки «Отправить»; - PIN на экране блокировки —
maskedпрячет символы, поведение остаётся тем же; - код читают вслух или списывают глазами —
groupsразбивает его на части, как номер карты.
Когда взять другое
| Нужно | Берите |
|---|---|
| Строка произвольной длины | GrInput |
| Пароль, который печатают, а не получают | GrInput с type="password" |
| Число со ступенями и границами | GrNumberInput |
| Пользователь придумывает значения сам | GrInputTag |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrOtpInput v-model="value" />Примеры 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 PINRepeat 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
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Выключить поле: код не вводится и не отправляется с формой. Складывается с `GrFormField`. |
length | number | undefined | undefined | Число ячеек. |
type | "text" | "numeric" | "alphanumeric" | undefined | "numeric" | Допустимый алфавит и раскладка мобильной клавиатуры. |
modelValuev-model | string | undefined | "" | Код целиком, без разделителей. |
readonly | boolean | undefined | false | Только для чтения: код видно, но не изменить. |
invalid | boolean | undefined | false | Ошибка — например, сервер отверг код: рамка ячеек и `aria-invalid`. Складывается с `GrFormField`. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер ячеек и кегль символов. Не задан — из `GrConfigProvider`, иначе `md`. |
placeholder | string | undefined | "" | Знак в пустой ячейке. |
ariaLabel | string | undefined | undefined | Доступное имя вне `GrFormField`: ячейки скрыты от скринридера, читается одно поле поверх них. |
name | string | undefined | undefined | Имя поля для нативной отправки формы. |
form | string | undefined | undefined | `id` формы, если поле лежит вне неё. |
pattern | string | undefined | undefined | Свой алфавит регуляркой на **один символ**. Перекрывает `type`. |
groups | number[] | undefined | undefined | Разбивка на группы: `[3, 3]` рисует `123 – 456`. Сумма обязана равняться `length`. |
masked | boolean | undefined | undefined | Точки вместо символов — сценарий PIN. На `modelValue` не влияет. |
autofocus | boolean | undefined | false | Фокус в поле сразу после монтирования — для экрана, где ввод кода и есть всё действие. |
oneTimeCode | boolean | undefined | true | `autocomplete="one-time-code"`: код из SMS подставляется на iOS и macOS. |
webOtp | boolean | undefined | false | Приём кода из SMS на Android через WebOTP. Выключен: требует HTTPS и особого хвоста в тексте сообщения, который добавляет бэкенд. |
Слоты
| Slot | Type | Описание |
|---|---|---|
cell |
| Своё оформление ячейки. Размер и рамку по-прежнему держит компонент. |
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
const grOtpInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrOtpInput">;Файлdist/types/components/GrOtpInput/defaults.d.ts
Пропы GrOtpInput, настраиваемые глобально через componentDefaults.
Только оформление и поведение по умолчанию — см. GrButton/defaults.ts.
interface GrOtpInputConfigurableProps {
size: GrComponentSize;
length: number;
masked: boolean;
}Файлdist/types/components/GrOtpInput/safelist.d.ts
const grOtpInputSafelist: string[];Файлdist/types/components/GrOtpInput/GrOtpInput.vue.d.ts
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;
}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
type GrOtpInputSize = GrComponentSize;Файлdist/types/components/GrOtpInput/otpValue.d.ts
type GrOtpInputType = typeof GR_OTP_INPUT_TYPES[number];Файлdist/types/components/GrOtpInput/index.d.ts
type GrOtpInputInstance = ComponentExposed<typeof GrOtpInputComponent>;Доступность
Паттерн APG
—
| Клавиша | Действие |
|---|---|
| — | клавиатура нативная: поле одно, каретка настоящая. Печать **заменяет** символ под кареткой, а не сдвигает хвост; Backspace и Delete стирают; ←/→ ходят по ячейкам; выделение и отмена — как в обычном поле. К краям строки — тем сочетанием, которое даёт платформа (Home/End на macOS каретку в поле не двигают). Всё поле — **одна** остановка Tab по построению, а не через roving tabindex |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Ячеек много, поле одно
Компонент рисует ряд ячеек, но в DOM это один <input>, растянутый поверх
ряда и невидимый: текст, каретка и подсветка выделения прозрачны. Ячейки —
декорация с aria-hidden.
Это несущее решение, и почти всё поведение поля следует из него:
| здесь | шесть отдельных полей | |
|---|---|---|
| Вставка кода | нативная | распределять по ячейкам руками |
| Код из SMS на iOS | autocomplete="one-time-code" работает | подставляется только в первую ячейку |
Выделение, Ctrl+A, отмена | нативные | нет |
Стрелки, Home/End | нативная каретка | писать руками |
| Таб-порядок | одна остановка по построению | roving tabindex руками |
| Диктор | «поле ввода, 123456» | шесть безымянных полей подряд |
Требование пакета «составной виджет — одна остановка Tab»
(../keyboard.md) выполняется здесь устройством, а не
поверх него.
Цена — одна: активная ячейка не хранится, а выводится из положения каретки настоящего поля.
Печать заменяет, а не сдвигает
Единственное место, где поведение расходится с обычным полем. Длина фиксирована, поэтому символ, напечатанный в занятую ячейку, заменяет её содержимое, а хвост кода остаётся на месте.
Нормализация висит на событии input, а не на beforeinput, и это не деталь
реализации: мобильные клавиатуры и автоподстановка присылают целую строку одним
событием. Обработчик обязан одинаково принимать и один символ с клавиатуры, и
шесть из сообщения — различить их по событию нельзя.
Символы вне алфавита отбрасываются молча, а не блокируют ввод: 123-456,
скопированное из мессенджера, даёт 123456. Это главный сценарий вставки, и
он не должен требовать от пользователя чистить строку руками.
Клик ставит каретку в первую пустую ячейку
А не туда, куда пришёлся курсор. Ячейки — декорация, попадание мышью по
невидимому тексту ничего осмысленного не значит, а «продолжить с того места, где
остановился» — единственное, чего от кода ждут. Редактирование середины остаётся
за стрелками и Backspace.
Код из сообщения подставляется сам
Два канала, и вместе они закрывают обе мобильные платформы:
| Проп | Платформа | Состояние |
|---|---|---|
oneTimeCode | iOS, macOS | включён: autocomplete="one-time-code" ничего не стоит и работает сам |
webOtp | Android | выключен |
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добавляет бэкенд; - маска ввода общего назначения — телефон и дата это другая задача.
Пропы этого примера