跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件一次性验证码输入框

一次性验证码输入框

接收发送给用户的简短验证码——用于登录或核实身份——它是一个文本值,画成一个个独立的格子,让人看得出验证码有多长、还缺多少,平台也能一步把它填好。

content_copy以 Markdown 格式查看

进阶

大多数产品直接使用包含此组件的One-time-code field。

类别
文本输入
令牌
comp.one-time-code-input
层级
基础

概览

Web
tanrei-one-time-code-input
schedule
SwiftUI
TanreiOneTimeCodeInput
radio_button_unchecked
Jetpack Compose
TanreiOneTimeCodeInput
radio_button_unchecked
Flutter
TanreiOneTimeCodeInput
radio_button_unchecked
React Native
OneTimeCodeInput
radio_button_unchecked

生成自 docs/status/status.json, specs/one-time-code-input.spec.json

字段里的一次性验证码输入框,以及一个验证码被产品拒绝的输入框

一次性验证码输入框用于产品发送给用户、并要求用户回填一次的简短验证码:登录、确认某个电子邮件地址或电话号码、批准一笔付款。格子显示出验证码有多长,以及还缺多少。

它是容纳整个验证码的一个输入框,而不是每个字符一个输入框。它只有一个文本值,对 Tab 键来说只有一个停靠点,对辅助技术来说只是一个文本框;格子只是把这个值画出来。所以平台自己的验证码自动填充能一步把它填好,粘贴的内容会完整落入,屏幕阅读器读到的是一个有名称的文本框。

它是一个高级组件:一个不带任何附加功能的控件,供自己动手组合的产品使用。它自己没有标签、帮助文本和错误文本。大多数产品需要的是一次性验证码字段,也就是放在字段里的这个输入框,可以直接使用。

使用它的组件是一次性验证码字段。要自己组合,就把它放进字段,由字段为它命名、描述它、把它标记为必填,并让它进入错误状态——本页的示例都是这样写的,比一次性验证码字段低一层。如果可见的标签在别处——例如对话框的标题——就单独使用它,并给出 label;label 绝不会画出来。

在 Web 上它是 <tanrei-one-time-code-input>。其他平台尚未实现。

用法

value 是到目前为止已输入的验证码:最多 length 个字符,由数字组成,alphanumeric 时由数字和大写字母组成;无论画出怎样的分组,其中都没有分隔符。

<form>
  <tanrei-field label="验证码"
    supporting-text="请输入发送到手机的 6 位验证码。"
    error-text="验证码不正确。请核对短信后重试。"
    required required-text="必填">
    <tanrei-one-time-code-input name="code" group-size="3"></tanrei-one-time-code-input>
  </tanrei-field>
</form>

<script type="module">
  import '@tanrei/web/components/field';
  import '@tanrei/web/components/one-time-code-input';

  const field = document.querySelector('tanrei-field');
  const code = document.querySelector('tanrei-one-time-code-input');
  // 用户改变了值。这时它已经是新的值。
  code.addEventListener('input', () => (field.error = false));
  // 最后一个格子填上了,或者粘贴、自动填充了完整的验证码。除此之外,不会自动发生任何事。
  code.addEventListener('complete', async (e) => {
    code.readOnly = true;
    const right = await verify(e.detail.value);
    code.readOnly = false;
    field.error = !right;
  });
</script>
// 进阶组件(用于构成其他组件的部件)从 advanced 入口导入。
import { Field } from '@tanrei/react';
import { OneTimeCodeInput } from '@tanrei/react/advanced';

<Field label="验证码" error={wrong}
  supportingText="请输入发送到手机的 6 位验证码。"
  errorText="验证码不正确。请核对短信后重试。">
  <OneTimeCodeInput name="code" groupSize={3} value={code}
    onInput={(event) => setCode(event.detail.value)}
    onComplete={(event) => verify(event.detail.value)} />
</Field>

规格

4 个格子、分成两组的 8 个格子、字母加数字、遮蔽的验证码、只读的和禁用的

格子

部件 它是什么
格子 验证码的每个字符一个方框,要么是空的,要么放着一个字符。空格子和填了字符的格子是同一个方框
字符 已填格子里的字符,使用文本字段的输入文本样式和等宽数字。设置 mask 时,由密码框绘制的那种遮蔽符号代替它
活动格子 输入框获得焦点期间,下一个字符要填入的格子:第一个空格子;全部填满时是最后一个格子。它在边缘以内画出聚焦时的那条线——这条线既是输入框的焦点指示,也是它的位置标记
插入点 位于空的活动格子里。已填的活动格子不画插入点:下一个字符会替换它里面的字符
分组标记 设置 groupSize 时,每隔这么多个格子画一条短线。它只是画出来的:不属于值,不在复制的内容里,也不随表单提交

格子和 medium 尺寸的文本字段一样高,所以同一个表单里的一次性验证码输入框和文本字段高度相同。它只有一种尺寸、一种外观;设计系统在自己的层里设置格子的高度、圆角和填充。

这一行从起始边开始,宽度就是格子所需的宽度:在默认系统中,分成两组的 6 个格子宽 284,放得进宽 320 的屏幕。空间不够时,格子平分宽度并变窄,最窄到一个字符加上两侧各一小段内边距;再窄,这一行就换行——有分组时先在两组之间换行,断开处不画分组标记;没有分组时在格子之间换行。格子绝不会被截断,验证码也绝不会为了放得下而画得更小。

键入和读取

何时 发生什么
键入一个允许的字符 它填入活动格子,替换格子里原有的字符,下一个格子成为活动格子。在最后一个格子上,活动格子不再移动,所以再键入的字符会替换最后一个字符
键入其他任何字符 什么都不发生。格子里只放验证码能包含的字符
Backspace 已填的活动格子失去它的字符;空的活动格子则让前一个格子失去字符。后面的字符向前补位:值里没有空缺
左方向键和右方向键、Home、End 在已填的格子和第一个空格子之间移动活动格子。上方向键和下方向键什么都不做
全选后键入一个字符 替换整个验证码。无法只选中其中几个格子
粘贴、拖放、自动填充、听写 无论活动格子在哪里,整段文本都读作一个验证码

键入或粘贴的内容会被宽松地解读。其他文字系统的数字,以及全角的数字和字母,读作它们的 ASCII 形式,小写字母读作大写;值始终是 ASCII,可以原样与发出的验证码比较。

粘贴的文本会去掉其中的空格、连字符和破折号:123 456 和 123-456 都是 123456。对于数字验证码,如果更长的文本里有且只有一段长度恰为 length 的连续数字,就取这一段,所以可以把整条消息(“你的验证码是 123456。”)粘贴进来。任何有歧义的内容都不会改变值。

输入法尚未确认的文本会画在格子里,带下划线,在确认之前不改变任何值。

事件

事件 何时
input 用户做出的每一次更改。它带有新的值
complete 用户的一次更改让值的长度达到 length 个字符。在这次更改的 input 之后触发一次
change 焦点离开了输入框,或者按下了 Enter,并且值与上一次确认的值不同
action Enter。在 Web 上的表单里,随后会像在任何文本输入控件里那样提交表单

产品设置 value 时,它们都不触发。验证码填完时,也不会发生别的事:输入框不提交、不移动焦点、不禁用自己,也不朗读任何内容。验证码填完之后做什么,由产品决定。

构成

构成

绘制在 Web 包所绘的测试样例 empty 之上。
部件 说明
1 cell One box per character of the code, empty or holding one character. The component's container: it carries the hover and the focus answer, and its tokens are the container's.
2 character The character a filled cell holds, or the platform's obscuring mark in its place while mask is true.
3 caret · 可选 Drawn in the active cell while the field has focus and that cell is empty: where the next character goes.
4 separator · 可选 A short line between two groups of cells. A drawing only: not part of the value and kept from assistive technology.

属性

属性 类型 默认值 说明
value string "" The code entered so far: at most length characters, digits or, for alphanumeric, digits and upper-case letters, with no separator. One string, whatever the cells and groups drawn.
length number 6 How many characters the code has, and so how many cells are drawn. A whole number of 1 or more.
type numeric | alphanumeric "numeric" What a cell takes: numeric is the digits 0 to 9, with a numeric keypad; alphanumeric is digits and the letters A to Z, held in upper case, with a general keyboard.
groupSize number 0 Draws the cells in groups of this many, counted from the first, with a mark between two groups (3 for a six-character code written "123 456"). 0 draws one group. It changes the drawing only: the value has no separator.
mask boolean false Draws the platform's obscuring mark in each filled cell in place of its character, and exposes the field as protected. For a screen other people can see; a code that is used once and expires rarely needs it.
label string "" Names the field for assistive technology where no Field frames it (the visible label is a dialog headline or a step title, in the same words). Never drawn. Inside a Field leave it empty: the Field names it.
error boolean false Draws every cell in the error condition and exposes the field as invalid. A Field sets it from its own error; set it here only where no Field frames the control. What a wrong code means is the product's to say, in words, with the Field's errorText.
disabled boolean false Prevents entry and takes the field out of the focus order. Inside a Field, disable the Field, so that its label follows.
readOnly boolean false Keeps the field focusable and its value readable, and lets nothing change it: for the moment a complete code is being checked, when taking focus away would lose the place of a person who must then correct it.

事件

事件 说明
input The person changed the value; carries the new value. Emitted for every change, not when the product sets value.
change The person committed a changed value (focus left the field, or the action key was pressed); carries the value.
complete A change by the person left the value length characters long; carries the value. Emitted after input, once for that change. The field does nothing else: checking the code is the product's.
action The person pressed the keyboard's action key (Enter, Return, Done).

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
OTC-1 THE one-time-code input SHALL draw one row of length cells, in the order of the code's characters, each a box at least comp.one-time-code-input.container.height high and comp.one-time-code-input.container.width wide (OTC-23), with corners of comp.one-time-code-input.container.shape (never more than half its height) eased by sys.shape.corner-smoothing, comp.one-time-code-input.container.gap from the next, filled with comp.one-time-code-input.standard.container.color. The row SHALL start at the start edge of the width its layout gives it and SHALL be as wide as its cells, gaps and group marks need; the field SHALL be as tall as its rows. THE one-time-code input SHALL draw no label, no help, no error text, no placeholder and no container around the row: a Field frames it (OTC-22).
OTC-2 THE one-time-code input SHALL be one field to the person, the form, the keyboard and assistive technology: it SHALL hold one value, be one stop in the focus order, edit in one native text element of the platform, so that an input method, the platform's autofill, the clipboard and dictation meet one text input, and be exposed as one text box whose value is the whole code. The cells, the caret it draws and the group marks SHALL be a drawing of that value: they SHALL NOT be exposed to assistive technology, SHALL NOT take focus one by one, and SHALL NOT be separate inputs.
OTC-3 THE one-time-code input SHALL draw the character at each place of value in the cell of that place, centred, on one line, in comp.one-time-code-input.input.typescale with tabular figures and in comp.one-time-code-input.standard.input.color, and SHALL leave empty every cell past the end of value. An empty cell and a filled cell SHALL have the same box and the same fill. The value SHALL never have a gap: the filled cells are always the first ones.
OTC-4 THE one-time-code input SHALL hold in value only the characters its type permits — WHILE type is numeric the ASCII digits 0 to 9; WHILE type is alphanumeric those digits and the letters A to Z — and SHALL read what a person types, pastes, dictates or is given by autofill leniently before deciding: a decimal digit of any script and a full-width digit or letter SHALL be read as its ASCII form, and a lower-case letter as its upper-case form. A character that is still not permitted SHALL be dropped: it SHALL NOT enter a cell, move the active cell or emit an event. The value SHALL therefore be ASCII, in upper case, and comparable as it stands with the code the product sent.
OTC-5 WHILE the field has focus THE one-time-code input SHALL have one active cell, the cell the next character goes to: the first empty cell and, WHILE every cell is filled, the last cell — unless the person moved it to a filled cell (OTC-8, OTC-25). WHEN focus enters by the keyboard, by a press on a Field's label or from the product THE active cell SHALL be that first empty or last cell; WHEN focus leaves, the field SHALL have no active cell. The insertion point of the native text element SHALL stand at the active cell — before its character WHILE it is filled, at the end of the value WHILE it is empty — so that assistive technology that reads where the insertion point is, and the character at it, reads the active cell.
OTC-6 WHEN the person types a permitted character (OTC-4) THE one-time-code input SHALL put it in the active cell, in place of the character that cell holds, and SHALL move the active cell to the next one; WHILE the active cell is the last THE active cell SHALL stay there, so that a further character replaces the last one and nothing is typed past the end. Text an input method commits SHALL be read character by character in the same way. WHILE an input method holds text it has not yet committed THE one-time-code input SHALL draw that text's characters, read by OTC-4, in the cells from the active cell on, marked as the platform marks composing text, SHALL NOT change value or emit an event for it, and SHALL remove it from the cells when the input method cancels it; the cells are the only drawing of what is typed, so text that is being composed is never left unseen. THE one-time-code input SHALL NOT move focus out of itself because the code is complete (OTC-14).
OTC-7 WHILE hovered THE one-time-code input SHALL render every cell from the hovered component tokens, and WHILE focused SHALL render its active cell, and that cell alone, from the focused ones (their error parts WHILE error is true), with precedence focused over hovered; unlike a button, focus gained by pointer or touch SHALL select the focused state, because a field shows where typing will go however it was focused. THE one-time-code input SHALL show focus by the active cell's focused tokens and SHALL NOT draw the focus ring: the focused outline color and width are drawn as a line inside that cell's edge, following its corners and taking no layout space, so that no cell moves as the line appears, moves to another cell or goes; WHERE sys.behavior.focus-style is glow it SHALL also draw the glow around the active cell. A hovered cell SHALL show the state layer and draw no edge; a focused cell SHALL draw no state layer. WHILE the active cell is empty and readOnly is false THE one-time-code input SHALL draw a caret centred in it, comp.one-time-code-input.caret.width wide and one line of the input's text style high, in comp.one-time-code-input.standard.caret.color (comp.one-time-code-input.standard.error.caret.color WHILE error is true), blinking as the platform's own caret does and steady WHERE motion is reduced; a filled active cell SHALL draw no caret. WHEN its state, its active cell, error or disabled changes THE one-time-code input SHALL change its colors and line widths over comp.one-time-code-input.motion.state-duration with comp.one-time-code-input.motion.state-easing, and at once WHERE motion is reduced; a character SHALL appear in its cell at once, with no motion of its own.
OTC-8 WHEN the person presses Left Arrow or Right Arrow THE one-time-code input SHALL move the active cell to the cell drawn on that side, in every layout direction (OTC-27), no further than the first cell on one side and the first empty cell — or the last cell WHILE all are filled — on the other; Home SHALL move it to the first cell and End to that furthest one. Up Arrow, Down Arrow, Page Up and Page Down SHALL do nothing in the field and SHALL be left to the page. THE one-time-code input SHALL NOT change the value by any of these keys.
OTC-9 WHEN the person presses Backspace THE one-time-code input SHALL, WHILE the active cell is filled, remove its character and keep the active cell where it is, and WHILE the active cell is empty, remove the character before it and move the active cell there; WHEN the person presses Delete it SHALL remove the active cell's character. The characters after a removed one SHALL each move one cell toward the first, so that the value keeps no gap (OTC-3). On an empty field both keys SHALL do nothing.
OTC-10 WHEN the person selects all with the platform's own command THE one-time-code input SHALL select the whole value and draw every filled cell as the active cell is drawn (OTC-7); a permitted character typed then SHALL replace the whole value and stand in the first cell, and Backspace or Delete SHALL empty the field. Copy SHALL take the whole value, without group marks, whichever cell is active, and cut SHALL take it and empty the field; WHILE mask is true copy and cut SHALL give nothing and cut SHALL change nothing, as the platform's protected field gives nothing and keeps its text. THE one-time-code input SHALL offer no selection of some cells only: a drag and Shift with an arrow key SHALL select nothing.
OTC-11 WHEN text is pasted, dropped or dictated into the field, or given to it by the platform's autofill, THE one-time-code input SHALL read the whole text as one code, wherever the active cell is: it SHALL remove white space and hyphen and dash characters, read the rest by OTC-4, and, IF every remaining character is permitted and there is at least one, replace the whole value with the first length of them. IF that fails WHILE type is numeric and the text holds exactly one run of exactly length digits that no other digit adjoins, it SHALL take that run, so that a whole message pasted with its code in it is read. Otherwise it SHALL change nothing and emit nothing. After a change the active cell SHALL be the first empty cell, or the last WHILE all are filled.
OTC-12 THE one-time-code input SHALL ask the platform for the code it received and for the keyboard of its type: it SHALL mark its text element as a one-time code for the platform's autofill (the web's autofill field name one-time-code; each native platform's own content type for a one-time code), so that the code offered above the keyboard or by a password manager fills the field in one step (OTC-11); WHILE type is numeric it SHALL show a numeric keypad, and WHILE type is alphanumeric a general keyboard that capitalizes every character, each without autocorrection, suggestions or spelling marks. THE one-time-code input SHALL NOT read the person's messages itself and SHALL NOT keep or offer codes entered before.
OTC-13 WHEN the person changes the value — by typing, removing, cutting, pasting or autofill — THE one-time-code input SHALL emit input carrying the new value, once for each change; WHEN focus leaves the field or the action key is pressed and the value differs from the value at the last commit it SHALL emit change carrying the value; WHEN the person presses the keyboard's action key (Enter, Return, Done) it SHALL emit action and, WHERE the platform has forms, leave the key to the form, which submits as it does from any text input: through its default button (BTN-30) — WHILE the form has a submit button, the platform's own or a Button of type submit, the first of them in document order is pressed, so that its click is emitted and its name and value are sent, and WHILE that button is disabled nothing is submitted; only a form with no submit button is asked directly. A key that commits or cancels an input method's composing text SHALL NOT emit action and SHALL NOT reach the form. THE one-time-code input SHALL emit none of these when the product sets value.
OTC-14 WHEN a change by the person leaves the value length characters long — the last empty cell was typed into, a whole code was pasted or filled, or a character of a complete code was replaced — THE one-time-code input SHALL emit complete carrying the value, after the input event of that change and once for it. It SHALL NOT emit complete when the product sets value, and SHALL do nothing else because the code is complete: it SHALL NOT submit a form, move or drop focus, disable itself, or announce anything. What follows a complete code — checking it, saying that it is wrong, moving on — is the product's.
OTC-15 WHILE groupSize is a whole number from 1 to length minus 1 THE one-time-code input SHALL draw, after every groupSize cells counted from the first and before the next cell, a group mark: a horizontal line comp.one-time-code-input.separator.width long and comp.one-time-code-input.separator.thickness thick, with round ends, centred on the height of the cells, comp.one-time-code-input.container.gap from the cell on each side, in comp.one-time-code-input.standard.separator.color; the last group holds the cells that remain. The mark SHALL be a drawing only: it SHALL NOT be part of value, of what is copied or of what a form submits, SHALL take no focus and no press of its own, and SHALL be kept from assistive technology. WHILE groupSize is 0 THE one-time-code input SHALL draw no mark and one even row.
OTC-16 WHILE mask is true THE one-time-code input SHALL draw, in each filled cell and in place of its character, the mark the platform's own protected text field draws for a character, in the character's color and text style, at once and without first showing what was typed; it SHALL expose the field as a protected text entry, so that assistive technology does not read the characters back, and SHALL keep its type's keyboard and the platform's code autofill (OTC-12). The value, the events and what a form submits SHALL be the same as without mask.
OTC-17 WHILE error is true — set by the product, or by the Field that frames it (FLD-8) — THE one-time-code input SHALL render every cell from the error parts of its tokens: at rest a line comp.one-time-code-input.outline.width wide inside each cell's edge in comp.one-time-code-input.standard.error.outline.color, drawn as the focused line is (OTC-7), so that the field itself is marked and not only a text below it; and it SHALL expose the field as invalid to assistive technology. THE one-time-code input SHALL never set error itself and SHALL NOT empty, select or change the value because of it: whether a wrong code is cleared is the product's to decide. The words that say what is wrong are the Field's errorText (FLD-7, FLD-9); the error color is never the only sign.
OTC-18 WHEN the product sets value THE one-time-code input SHALL show it, reading it by OTC-4 and keeping its first length permitted characters; IF anything was dropped it SHALL in debug builds report it. It SHALL emit no event for a value the product sets, and setting value to what it already is SHALL change nothing, the active cell included. WHEN the value is set while the field has focus THE active cell SHALL become the first empty cell, or the last WHILE all are filled.
OTC-19 IF length is not a whole number of 1 or more THE one-time-code input SHALL draw 6 cells, and IF groupSize is not a whole number from 0 to length minus 1 it SHALL draw one group, and in debug builds SHALL report each. WHEN length becomes smaller than the value is long THE one-time-code input SHALL keep the first length characters, without emitting an event. WHEN type names a value it does not know it SHALL behave as numeric and in debug builds report the unknown value.
OTC-20 WHILE disabled is true — set by the product, or by the Field that frames it (FLD-14) — THE one-time-code input SHALL NOT be focusable or editable, SHALL have no active cell and no caret, SHALL NOT emit input, change, complete or action, SHALL expose the disabled state to assistive technology, SHALL be left out of form submission on the web, and SHALL render every cell, character and group mark from the disabled component tokens (of its error parts WHILE error is true).
OTC-21 WHILE readOnly is true and disabled is false THE one-time-code input SHALL stay focusable, with its active cell and its focused line, and copyable (OTC-10); SHALL NOT let the person change the value by typing, removing, cutting, pasting or autofill; SHALL draw no caret; SHALL NOT emit input, change or complete; SHALL expose the read-only state to assistive technology; SHALL be submitted with its form on the web; and SHALL render as an enabled field, including its hovered and focused states.
OTC-22 THE one-time-code input SHALL take its name, its description and its required state from the Field that frames it, as every control a Field frames does (FLD-10, FLD-11): the Field's label is its accessible name, the Field's supporting text with, in error, the error text after it its description. WHILE label is not empty THE one-time-code input SHALL use it as the accessible name instead, drawn nowhere, for a field no Field frames or one whose visible label is elsewhere. WHILE it has either THE one-time-code input SHALL expose that one alone, over a name the product gives it on the element (aria-label or aria-labelledby); WHILE it has neither and the product gives it a name of its own on the element — aria-label, or aria-labelledby naming an element — THE one-time-code input SHALL expose that name as its accessible name; IF it has none of the three THE one-time-code input SHALL still render and work, and in debug builds SHALL report that it has no accessible name. It SHALL expose how many characters the code has only through its value and the product's own words: it SHALL say no word of its own (OTC-30).
OTC-23 THE one-time-code input SHALL never be wider than its layout gives it. WHEN the row needs more width than there is, the cells SHALL share the width equally and become narrower than comp.one-time-code-input.container.width, never narrower than a character of the input's text style with comp.one-time-code-input.container.padding-inline on each side; IF the row still does not fit, it SHALL wrap onto further rows comp.one-time-code-input.container.gap below, each from the start edge: between two groups first, where the group mark at the break SHALL NOT be drawn, and between any two cells where there are no groups or one group alone does not fit. A cell SHALL never be cut, drawn over another or scrolled sideways, and the code SHALL never be drawn at a smaller text size to make it fit.
OTC-24 WHILE the text size is enlarged THE one-time-code input SHALL grow with its characters: each cell grows in height with the character's line, keeping comp.one-time-code-input.container.height as a minimum, becomes as wide as the character and its paddings need where that is more than comp.one-time-code-input.container.width, and all cells keep one size; the row then narrows and wraps by OTC-23. At 200% in a layout 288 wide no character SHALL be lost, cut or laid over another part, and nothing SHALL need scrolling in two directions.
OTC-25 WHEN the person presses a cell, a group mark or the space between cells THE one-time-code input SHALL take focus and make the pressed cell the active cell WHILE it is filled, and otherwise the first empty cell; it SHALL change nothing else and SHALL draw no press answer, because a press on a field is not an action. WHERE a cell is shorter than the minimum target THE one-time-code input SHALL extend the row's hit area vertically to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch.
OTC-26 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE one-time-code input SHALL draw an edge comp.one-time-code-input.outline.width wide in the platform's control text color around every cell, that edge in the platform's highlight color at the focused width around the active cell, its characters, masks and group marks in the platform's text color and its caret in that color, and a disabled field in the platform's disabled text color; it SHALL rely on no color of its own to show an error, which is carried by the Field's words and the invalid state (OTC-17).
OTC-27 WHERE the layout direction is right-to-left THE one-time-code input SHALL keep the cells a left-to-right run: the first character of the code in the leftmost cell of its row, the group marks between the same cells, Right Arrow moving to the cell on the right and Left Arrow to the cell on the left, because a code is a sequence of digits and Latin letters that is written, sent and read aloud left to right in every script and digits are never reversed. Only the row's place SHALL follow the layout: the row, and every row of a wrapped code, SHALL start at the right edge of the width its layout gives it, where the Field's label and help start. A wrapped code SHALL read from the top row down in both directions.
OTC-28 THE one-time-code input SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the field is shown THE one-time-code input SHALL draw itself for the new direction at once, without a motion of its own, keeping its value, its active cell and its focus.
OTC-29 WHERE the platform has forms (the web) THE one-time-code input SHALL take part in them as a native text input does: it takes name, submits its value — the code alone, without group marks — or an empty string WHILE it holds none, restores it on navigation, resets to the value it was first given, and reports to the form's constraint validation a missing value WHILE its Field says it is required and it is empty. It SHALL report nothing for a code that is shorter than length: an unfinished code is the product's to refuse.
OTC-30 THE one-time-code input SHALL have no words of its own: it SHALL draw no word in any language and expose none. Its name is the Field's label or its own label, each the product's (OTC-22); what a required, a wrong or an expired code is called is the Field's requiredText, supportingText and errorText; the group mark and the mask are marks, not words. It SHALL read no key of the product's words.
OTC-31 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE one-time-code input SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the one-time-code input is invalid — except those the one-time-code input owns (aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE one-time-code input SHALL take it off that element. WHEN the product sets an aria- attribute the one-time-code input owns THE one-time-code input SHALL report it in debug builds.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-one-time-code-input TanreiOneTimeCodeInput TanreiOneTimeCodeInput TanreiOneTimeCodeInput OneTimeCodeInput
value · 属性 value code value controller value
length · 属性 length length length length length
type · 属性 type type type type type
groupSize · 属性 groupSize groupSize groupSize groupSize groupSize
mask · 属性 mask isMasked masked obscureText secureTextEntry
label · 属性 label label label semanticLabel accessibilityLabel
error · 属性 error error isError isError error
disabled · 属性 disabled disabled enabled enabled disabled
readOnly · 属性 readOnly readOnly readOnly readOnly readOnly
input · 事件 input code onValueChange onChanged onChangeText
change · 事件 change onCommit onCommit onCommit onEndEditing
complete · 事件 complete onComplete onComplete onCompleted onComplete
action · 事件 action onSubmit keyboardActions onSubmitted onSubmitEditing
备注 A control, so a custom element with a shadow root, form-associated (static formAssociated = true) as the text field is: it takes name, submits the code, restores and resets it, follows <fieldset disabled> and reports valueMissing (OTC-29). <tanrei-field label="Verification code" supporting-text="…" error-text="…"><tanrei-one-time-code-input name="code" group-size="3"></tanrei-one-time-code-input></tanrei-field>: the field is the one element in the Field's default slot and asks it for its name, description, invalid, required and disabled states with the tanrei-field-connect event, as every Tanrei control does. In the shadow root there is exactly one <input type="text"> (type="password" while mask is set), laid over the whole row, with transparent text and no caret of its own, carrying autocomplete="one-time-code", inputmode="numeric" ("text" with autocapitalize="characters" for alphanumeric), autocorrect="off", spellcheck="false", enterkeyhint="done", and aria-label, aria-description, aria-invalid, aria-required and aria-readonly set on that element — not through ElementInternals, which axe cannot see. It has no maxlength and no pattern: the browser would cut or refuse a code pasted or filled with a space in it before the element could read it (OTC-11); the element reads and cuts in beforeinput and input. The cells, the caret and the group marks are aria-hidden elements under the input, drawn from the value; the caret is the element's own, blinking with a CSS animation that prefers-reduced-motion: reduce stops. input is a composed, bubbling CustomEvent whose detail.value is the code; the native input's own input event is stopped at the shadow root. change, complete and action are composed, bubbling events (the host's value is set before each fires). Enter is left to the native input, so a form submits implicitly as from any text input. Attributes are kebab-case (group-size) except readonly, which keeps HTML's name. The host is display: block; the row is a wrapping flex row with direction: ltr whatever the page's, placed at the inline start of the host by the page's direction. Tabular figures are font-variant-numeric: tabular-nums. Under forced-colors: active each cell has a ButtonText border, the active one Highlight. Before the element is defined it has no size and shows nothing: a page that must not jump gives the tag a block size of the cell height in its own stylesheet, and a page that must work without script uses a plain <input autocomplete="one-time-code" inputmode="numeric">, which this element is a drawing of. The product may fill value itself from the browser's own retrieval of a code from a message (the WebOTP API); the element does not call it, because the message's form and the permission are the product's. Words: the element has no words of its own and reads no key of setWords(). TanreiField("Verification code") { TanreiOneTimeCodeInput(code: $code, length: 6, groupSize: 3, onComplete: verify) }. code is a Binding<String>, written on every change, which is the input event. Built on one TextField (SecureField while masked) laid over the drawn cells, with .textContentType(.oneTimeCode), so that the code from a message is offered above the keyboard, .keyboardType(.numberPad) (.asciiCapable with .textInputAutocapitalization(.characters) for alphanumeric) and .autocorrectionDisabled(). disabled follows .disabled(_:), action is .onSubmit, label is .accessibilityLabel where no TanreiField gives one (it reads \.tanreiField). The cells are hidden from accessibility; the row is laid out with .environment(\.layoutDirection, .leftToRight) and placed by the surrounding direction. TanreiField(label = "Verification code") { TanreiOneTimeCodeInput(value = code, onValueChange = { code = it }, onComplete = ::verify) }. One BasicTextField whose decorationBox draws the cells from the value; KeyboardOptions(keyboardType = KeyboardType.NumberPassword, autoCorrectEnabled = false) (KeyboardType.Ascii with KeyboardCapitalization.Characters for alphanumeric), semantics { contentType = ContentType.SmsOtpCode } for the platform's autofill, and VisualTransformation for the mask. enabled is the inverse flag; the name, the description and isError come from LocalTanreiFieldState where a TanreiField frames it. The row is drawn under CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) and placed by the surrounding direction. TanreiField(labelText: 'Verification code', child: TanreiOneTimeCodeInput(controller: code, groupSize: 3, onCompleted: verify)). One EditableText under the drawn cells with autofillHints: [AutofillHints.oneTimeCode], keyboardType: TextInputType.number (TextInputType.visiblePassword with TextCapitalization.characters for alphanumeric), autocorrect: false, enableSuggestions: false and an input formatter that reads by OTC-4. enabled is the inverse flag; the name and the states come from TanreiFieldScope where a TanreiField frames it. The row is wrapped in Directionality(textDirection: TextDirection.ltr) and aligned by the surrounding Directionality. <Field label="Verification code"><OneTimeCodeInput value={code} onChangeText={setCode} groupSize={3} onComplete={verify} /></Field>. One transparent TextInput over the drawn cells with textContentType="oneTimeCode" (iOS) and autoComplete="sms-otp" (Android; "one-time-code" on iOS), keyboardType="number-pad" ("default" with autoCapitalize="characters" for alphanumeric), autoCorrect={false} and caretHidden. The cells are importantForAccessibility="no" and accessibilityElementsHidden. The name and the states come from FieldContext where a Field frames it. The row sets direction: "ltr" and is aligned by the app's direction.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-one-time-code-input-caret-width sys.border.hairline 1px
--tanrei-comp-one-time-code-input-container-gap sys.space.space75 6px
--tanrei-comp-one-time-code-input-container-height — 48px
--tanrei-comp-one-time-code-input-container-padding-inline sys.space.space50 4px
--tanrei-comp-one-time-code-input-container-shape sys.shape.corner.small 6px
--tanrei-comp-one-time-code-input-container-width sys.space.space500 40px
--tanrei-comp-one-time-code-input-disabled-outline-width — 1px
--tanrei-comp-one-time-code-input-focused-outline-width — 2px
--tanrei-comp-one-time-code-input-hovered-outline-width — 1px
--tanrei-comp-one-time-code-input-motion-state-duration sys.motion.role.state.default 250ms
--tanrei-comp-one-time-code-input-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-one-time-code-input-outline-width sys.border.hairline 1px
--tanrei-comp-one-time-code-input-separator-thickness sys.border.hairline 1px
--tanrei-comp-one-time-code-input-separator-width sys.space.space100 8px
--tanrei-comp-one-time-code-input-standard-caret-color sys.color.primary #1b1b1b
--tanrei-comp-one-time-code-input-standard-container-color sys.color.surface-container-highest #eeeeee
--tanrei-comp-one-time-code-input-standard-disabled-caret-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-container-color — #eeeeee
--tanrei-comp-one-time-code-input-standard-disabled-error-caret-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-error-container-color — #eeeeee
--tanrei-comp-one-time-code-input-standard-disabled-error-input-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-error-outline-color — #000000 · 0%
--tanrei-comp-one-time-code-input-standard-disabled-error-separator-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-error-state-layer-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-input-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-outline-color — #000000 · 0%
--tanrei-comp-one-time-code-input-standard-disabled-separator-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-one-time-code-input-standard-error-caret-color sys.color.error #cd262f
--tanrei-comp-one-time-code-input-standard-error-container-color sys.color.surface-container-highest #eeeeee
--tanrei-comp-one-time-code-input-standard-error-input-color sys.color.on-surface #1b1b1b
--tanrei-comp-one-time-code-input-standard-error-outline-color sys.color.error #cd262f
--tanrei-comp-one-time-code-input-standard-error-separator-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-one-time-code-input-standard-error-state-layer-color comp.one-time-code-input.standard.error.input.color #1b1b1b
--tanrei-comp-one-time-code-input-standard-focused-caret-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-focused-container-color — #eeeeee
--tanrei-comp-one-time-code-input-standard-focused-error-caret-color — #cd262f
--tanrei-comp-one-time-code-input-standard-focused-error-container-color — #eeeeee
--tanrei-comp-one-time-code-input-standard-focused-error-input-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-focused-error-outline-color — #cd262f
--tanrei-comp-one-time-code-input-standard-focused-error-separator-color — #4b4b4b
--tanrei-comp-one-time-code-input-standard-focused-error-state-layer-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-focused-input-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-focused-outline-color — #0070ac
--tanrei-comp-one-time-code-input-standard-focused-separator-color — #4b4b4b
--tanrei-comp-one-time-code-input-standard-focused-state-layer-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-hovered-caret-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-hovered-container-color — #dddddd
--tanrei-comp-one-time-code-input-standard-hovered-error-caret-color — #cd262f
--tanrei-comp-one-time-code-input-standard-hovered-error-container-color — #dddddd
--tanrei-comp-one-time-code-input-standard-hovered-error-input-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-hovered-error-outline-color — #cd262f
--tanrei-comp-one-time-code-input-standard-hovered-error-separator-color — #4b4b4b
--tanrei-comp-one-time-code-input-standard-hovered-error-state-layer-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-hovered-input-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-hovered-outline-color — #000000 · 0%
--tanrei-comp-one-time-code-input-standard-hovered-separator-color — #4b4b4b
--tanrei-comp-one-time-code-input-standard-hovered-state-layer-color — #1b1b1b
--tanrei-comp-one-time-code-input-standard-input-color sys.color.on-surface #1b1b1b
--tanrei-comp-one-time-code-input-standard-outline-color — #000000 · 0%
--tanrei-comp-one-time-code-input-standard-separator-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-one-time-code-input-standard-state-layer-color comp.one-time-code-input.standard.input.color #1b1b1b
--tanrei-comp-one-time-code-input-input-typescale sys.typescale.body-large 400 16px/1.625

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/one-time-code-input.spec.json, tokens/comp/one-time-code-input.tokens.json, design/tokens/

Web 上绘制方式的不同之处

  • 这个元素是表单关联的。它接受 name,提交的只有验证码,或者空字符串。它重置为 value 属性(attribute)的值;当所在的字段说明它必填而它为空时,它报告缺少值。短于 length 的验证码不会报告任何问题:没填完的验证码要不要拒绝,由你决定。
  • shadow root 里恰好有一个 <input>,带有 autocomplete="one-time-code" 和 inputmode="numeric"(alphanumeric 时是 text,并自动大写);设置 mask 时,它是密码输入框。它没有 maxlength,也没有 pattern:否则,带空格粘贴进来的验证码还没来得及读取,就会被浏览器截断。
  • input、change、complete 和 action 都是带 detail.value 的 CustomEvent。取消 action 可以阻止表单提交。
  • readOnly 对应的属性(attribute)是 readonly,groupSize 对应的是 group-size。
  • 插入点是元素自己画的。输入法显示候选窗口时,浏览器按原生输入框的文本来放置窗口,也就是放在这一行的开头,而不是活动格子旁边。
  • 这个元素不读取用户的消息。产品可以自己向浏览器索取某条消息里的验证码,然后设置 value。
  • 在元素定义之前,它没有尺寸。请预留一个格子的高度。没有脚本也必须能用的页面,请使用普通的 <input autocomplete="one-time-code" inputmode="numeric">;这个元素画的就是它。

指南

一次性验证码输入框还是文本字段

方面 一次性验证码输入框 文本字段
里面是什么 长度已知的简短验证码,发送给用户,再由用户回填一次 任意文本
显示什么 每个字符一个格子:验证码有多长,还缺多少 文本
接受什么 数字,或者数字和大写字母 任何内容

带 autocomplete="one-time-code" 的文本字段同样能正确输入验证码;不想要格子的地方,它是合适的选择。密码、用户自己设定的 PIN 码、恢复密钥或许可证密钥、卡号和邮政编码都用文本字段:它们要么没有值得画出来的简短固定长度,要么不能画成格子。产品当作数字来读的数字,用数字字段。

推荐

  • 如果你需要的只是标签、格子、帮助文本和错误,就使用一次性验证码字段。只有在需要一次性验证码字段没有提供的排布方式时,才手写一个字段并把这个输入框放在里面。
  • 把输入框放进字段,并在字段的 supportingText 里说明应该有几个字符,以及验证码发到了哪里。
  • 按消息里的写法画出分组:以“123 456”的形式发送的验证码,用 groupSize="3"。
  • 用文字说明验证码不正确,使用字段的 error 和 errorText。错误的验证码要不要清空,由你自己决定:很多时候只是错了一位数字。
  • 校验已填完的验证码期间,使用 readOnly,而不是 disabled。禁用的输入框会失去焦点,随后需要改正验证码的用户就找不到原来的位置了。
  • 把重新获取验证码的途径,以及任何时间限制,放在输入框旁边。它们是你自己的按钮和文本。
  • 如果你在 complete 时校验或提交,请在输入框之前说明这一点。

避免

  • 不要用每个字符一个输入框的方式拼出验证码输入。屏幕阅读器会遇到好几个没有名称的文本框,自动填充和粘贴也填不了它们。
  • 不要阻止粘贴,无论是否遮蔽。只能靠眼睛照着抄的验证码,考的是记忆力。
  • 不要在没有事先告知用户的情况下移走焦点或提交表单。
  • 不要习惯性地使用 mask。只用一次、过后就失效的验证码很少需要隐藏,而隐藏会让错误更难发现。
  • 不要在字段里使用 label。控件由字段命名。

无障碍

角色
textbox
标注
One text box. Its accessible name is the label of the Field that frames it, or its own label where no Field does; its description is the Field's supporting text with, in error, the error text after it. Its value is the whole code as one string. It is exposed as invalid while error is true, required while its Field says so, read-only while readOnly is true, and as a protected text entry while mask is true. The cells, the caret and the group marks are not exposed.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab Moves focus to the field, which is one stop however many cells it draws, and on from it.
A permitted character Fills the active cell, replacing what it holds, and moves to the next cell (OTC-6). Other characters do nothing (OTC-4).
Left Arrow / Right Arrow Move the active cell to the cell on that side, in every layout direction, among the filled cells and the first empty one (OTC-8).
Home / End Move the active cell to the first cell, or to the first empty cell (the last cell while all are filled).
Backspace / Delete Remove a character; the ones after it move up, so the value keeps no gap (OTC-9).
The platform's select all, copy, cut and paste Act on the whole code: paste replaces it from the first cell, read leniently (OTC-10, OTC-11).
Enter Emits action, commits a changed value, and submits the enclosing form on the web.

备注

  • One field, not one input per character. A row of separate inputs is a group of unnamed text boxes to a screen reader, breaks the platform's code autofill and paste, turns Backspace and the arrow keys into focus management, and gives a form several values for one answer. Here the cells are a drawing of one native text element.
  • For a short code sent to the person and typed back once. A password, a PIN the person chose, a recovery or licence key and a card number are text fields with the matching type and autofill name; a number the product reads as a number is a number field.
  • It has no label, help or error text of its own: a Field frames it (FLD-1 to FLD-24) and is where a product says how many characters to expect ("Enter the 6-digit code we sent to …"), that the code was wrong, and that it is required. Errors are words (WCAG 3.3.1, 1.4.1).
  • Never moves focus or submits by itself when the code is complete (OTC-14): a change of context on input (WCAG 3.2.2) is the product's decision, taken with the complete event, and a product that submits on complete should say so before the field.
  • Codes expire. Where a product limits the time to enter one, the limit, its extension and the way to ask for a new code are the product's, beside the field (WCAG 2.2.1); the field has no timer and no resend control.
  • A code that must be copied from another place is a cognitive test unless it can be pasted or filled: paste and the platform's autofill always work and are never blocked, masked or not (WCAG 3.3.8).
  • The active cell's focused line is the focus indicator. A design system must keep it visible: without it nothing shows where a replaced character will go.
  • The cells are a left-to-right run in a right-to-left layout (OTC-27); the row stands at the start edge with the Field's label.
  • Web: a touch browser zooms the page into an input whose text is smaller than 16 px. The character style is 16 px in the default system; a layer that sets a smaller style brings the zoom back.
  • Heights are minimums: at 200% text size the cells grow and the row narrows or wraps (OTC-23, OTC-24); nothing is cut (WCAG 1.4.4, 1.4.10).
  • Not pressable: a press focuses the field and is not an action (OTC-25), so BTN-8, BTN-9 and BTN-10 are not copied.
  • How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

生成自 specs/one-time-code-input.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 它是一个文本框。它的名称是字段的标签,没有字段包着它时是它自己的 label;它的描述是字段显示出来的辅助文本或错误文本;它的值是作为一个字符串的整个验证码。
  • 处于错误状态时,它暴露为无效;所在的字段说明它必填时,暴露为必填;readOnly 为 true 时,暴露为只读;mask 为 true 时,暴露为密码框。
  • 格子、插入点和分组标记对辅助技术隐藏,也不接受焦点。
  • 原生的插入点位于活动格子处——格子已填时位于它的字符之前——所以,朗读插入点处字符的屏幕阅读器,读到的正是画成活动状态的那个格子。
  • 无论画出多少个格子,它都只是一个 Tab 停靠点。
  • 输入框自己不朗读任何内容:更改不朗读,验证码填完也不朗读。出现的错误由包着它的字段朗读。
  • 确认或取消输入法文本的那个按键不是操作键,也不会提交表单。
  • 点击区域满足最小目标。按下会让输入框获得焦点;被按下的格子已填时,这个格子成为活动格子。
  • 文本放大到 200% 时,格子随字符一起变大、变窄并换行;没有任何内容被截断,也没有任何内容横向滚动。
  • 在强制颜色模式下,每个格子都会多出一道边线,活动格子的边线采用高亮颜色。输入框自己的任何颜色都不表示错误。
  • 在从右到左的布局中,格子仍然从左到右排列——数字绝不会颠倒——这一行位于右边缘,在字段的标签下方。
  • 在任何语言里,它都没有自己的文字。

Web 上的已知差距:

  • 键入、替换和粘贴验证码时屏幕阅读器会说什么,是从各浏览器的无障碍树里读出来的,还没有用屏幕阅读器实际听过。
  • 输入法在 Chromium 中实际操作过,在 Firefox 和 Safari 中则只是用它们的事件模拟的。
  • 在元素定义之前,什么都不绘制。

需要你做的事

  • 给控件一个名称:包着它的字段的标签;可见的标签在别处时,则是它自己的 label,措辞与那个标签相同。
  • 用文字说明期望的内容:有几个字符,验证码发到了哪里。格子只向看得见的人显示长度。
  • 用字段的 errorText、supportingText 和 required 说明验证码不正确、已过期或必填。颜色绝不是唯一的提示。
  • 输入验证码的时间有限时,要说明这一点,并让用户可以申请更多时间或新的验证码。