文本字段
让用户输入和编辑文本——单行,或者随文本增高、可手动调整大小的多行——也可以按区域设置的格式逐个单位地输入日期或时间,或者输入代码并按写下的样子原样保存;它带有可以说明必填或选填的标签,以及可选的图标、前缀和后缀、辅助文本、字符计数和选择器按钮。
- 类别
- 文本输入
- 令牌
comp.field- 层级
- 组合
- 构成自
- Field, Text input
- Web
tanrei-text-fieldschedule - SwiftUI
TanreiTextFieldschedule - Jetpack Compose
TanreiTextFieldschedule - Flutter
TanreiTextFieldschedule - React Native
TextFieldschedule
- 不提供的变体
- tanrei: filled, outlined
生成自 docs/status/status.json, specs/text-field.spec.json
规格已修订,但尚未在所有平台上实现
本页描述的是规格当前所定的文本字段:stacked 变体,它是默认变体,标签位于文本字段上方;filled 和 outlined 只为 Material 和 cupertino 预设保留;高度为 32、40 和 56(标签浮在里面的 filled 或 outlined 文本字段为 40、48 和 56);带选择器按钮的日期和时间类型;随文本增高、或用调整手柄调整大小的多行文本字段(resize);按分段逐个单位输入的日期或时间;以及把文本字段标记为必填或选填的 required 和 optional。Web 组件已经全部做到,演示也是如此。文本字段还可以保存代码(type="code",以及决定代码行是否换行的 wrap):Web 已经实现,其他平台都还没有。文本字段是包住一个文本输入框的字段,也就是本页描述的样子:Web 也已经实现,并且在 Web 上 required 属性(attribute)已经不存在了。SwiftUI、Compose、Flutter 和 React Native 在上方标记为“待跟进”:在跟上之前,它们提供的是上一版——默认为 outlined,高度为 36 和 40,required 标志不绘制任何标记,没有 stacked 变体,没有日期或时间类型,没有选择器按钮,也没有调整大小的模式——它们的示例展示的也是那一版。
默认的 Stacked:一个必填的文本字段,以及带选择器按钮的日期
表单里凡是要填文本的地方,都使用文本字段:它是最普通的情形,拿来就能用,带有标签、方框、一行帮助文本、值有误时的文字,以及计数。
组成部分是一个字段和一个文本输入框。字段是框架:标签、必填或选填标记、帮助文本、错误和计数。文本输入框是控件:方框,以及在方框里键入的一切。文本字段自己什么都不绘制,也没有令牌:它的每个属性都是这两者之一的属性,原样交给它们。
它是一个块,放在哪里就和那里一样宽,它的方框会把它填满。它没有自己的宽度:想要窄一些的字段,就把它放进窄一些的地方。
交互演示
<form>
<tanrei-text-field name="email" label="邮箱" type="email" autocomplete="email" required required-text="必填"
supporting-text="仅用于登录。"></tanrei-text-field>
<tanrei-text-field name="company" label="公司" optional optional-text="选填"></tanrei-text-field>
<!-- 随文本从 3 行增高到 8 行,之后滚动。 -->
<tanrei-text-field name="message" label="消息" multiline min-rows="3" max-rows="8"></tanrei-text-field>
<!-- 用调整手柄手动调整大小。 -->
<tanrei-text-field name="notes" label="备注" multiline resize="manual" resize-label="调整备注的大小"></tanrei-text-field>
</form>
<!-- 只要方框,放在工具栏里:一个文本输入框,由它自己的 label 命名。导入文本字段时,它也随之完成定义。 -->
<tanrei-text-input label="搜索" type="search" placeholder="搜索组件">
<span slot="leading-icon">search</span>
</tanrei-text-input>
<!-- 代码:无论页面是什么方向,都从左到右、用代码字体显示,绝不自动更正,也不做拼写检查。 -->
<tanrei-text-field label="匹配模式" type="code" value="tanrei.comp.button.*.container.color"></tanrei-text-field>
<tanrei-text-field label="Webhook 请求体" type="code" multiline min-rows="4" supporting-text="JSON"></tanrei-text-field>
<!-- 不换行:代码的每一行都保持在一行里,长的行在文本字段内部滚动。 -->
<tanrei-text-field label="数据行" type="code" multiline no-wrap></tanrei-text-field>
<!-- 按区域设置的顺序逐个单位输入的日期,以 2026-09-27 的形式保存,带选择器按钮。 -->
<tanrei-text-field id="start" label="开始日期" type="date" min="2026-01-01" disabled-dates="2026-05-01 2026-12-25"
invalid-text="请把日期填写完整。" unavailable-text="请选择 2026 年及以后的工作日。"
picker picker-label="选择开始日期" segment-empty-text="未填写"></tanrei-text-field>
<tanrei-date-time-picker id="start-picker" presentation="docked" anchor="start" label="开始日期"></tanrei-date-time-picker>
<script type="module">
import '@tanrei/web/components/text-field';
const email = document.querySelector('[name="email"]');
email.addEventListener('input', () => draft(email.value)); // 每次编辑
email.addEventListener('change', () => save(email.value)); // 值已改变,并且焦点离开或按下 Enter
// 校验由你负责:文本字段绝不会自己设置 error。
email.error = submitted && !isValid(email.value);
email.errorText = '请输入完整的邮箱地址。';
// 文本字段只发出请求;由产品呈现选择器,并把值写回来。
const start = document.querySelector('#start');
const picker = document.querySelector('#start-picker');
start.addEventListener('pickerrequest', (e) => {
Object.assign(picker, { value: e.detail.value, min: e.detail.min, max: e.detail.max, disabledDates: start.disabledDates, open: true });
});
picker.addEventListener('change', () => (start.value = picker.value));
picker.addEventListener('close', () => (picker.open = false));
</script>import { TextField } from '@tanrei/react';
<TextField
label="邮箱"
type="email"
autocomplete="email"
value={email}
onInput={(e) => setEmail(e.currentTarget.value)}
onChange={() => save(email)}
required
requiredText="必填"
error={submitted && !isValid(email)}
errorText="请输入完整的邮箱地址。"
/>
<TextField label="消息" multiline minRows={3} maxRows={8} value={message}
onInput={(e) => setMessage(e.currentTarget.value)} />
// 日期:值为 ISO 8601 格式;只要有分段为空,值就为空。
<TextField
label="开始日期"
type="date"
min="2026-01-01"
value={start}
invalidText="请把日期填写完整。"
unavailableText="请选择 2026 年及以后的日期。"
segmentEmptyText="未填写"
picker
pickerLabel="选择开始日期"
onChange={(e) => setStart(e.currentTarget.value)}
onEntryError={(e) => report(e.detail.reason)}
onPickerRequest={() => setPickerOpen(true)}
/>import Tanrei
TanreiTextField("邮箱", text: $email, type: .email,
supportingText: "仅用于登录。",
errorText: "请输入完整的邮箱地址。",
error: submitted && !isValid(email),
textContentType: "email", // HTML 的自动填充名称
onCommit: save,
// 把操作传到这里:外层的 .onSubmit 会先于 onCommit 运行。
onSubmit: signIn)
// 多行用纵向的 axis 加上行数限制的范围来表示。
TanreiTextField("消息", text: $message, axis: .vertical, lineLimit: 3...8)import androidx.compose.foundation.text.KeyboardActions
import io.github.chnotchy.tanrei.components.TanreiTextField
import io.github.chnotchy.tanrei.components.TanreiTextFieldType
var email by remember { mutableStateOf("") }
TanreiTextField(
value = email,
onValueChange = { email = it },
label = "邮箱",
type = TanreiTextFieldType.Email,
contentType = "email", // HTML 的自动填充名称
supportingText = "仅用于登录。",
isError = submitted && !isValid(email),
errorText = "请输入完整的邮箱地址。",
onCommit = { save(it) },
keyboardActions = KeyboardActions(onDone = { signIn() }),
)
// singleLine 与规格里的 multiline 相反。
TanreiTextField(value = message, onValueChange = { message = it }, label = "消息", singleLine = false, minLines = 3)import 'package:tanrei/tanrei.dart';
TanreiTextField(
controller: email, // 或者 initialValue: '…'
labelText: '邮箱',
type: TanreiTextFieldType.email,
autofillHints: TanreiTextField.autofillHintsFromHtml('email'),
helperText: '仅用于登录。',
isError: submitted && !isValid(email.text),
errorText: '请输入完整的邮箱地址。',
onCommit: save,
onSubmitted: (String value) => signIn(),
);
TanreiTextField(labelText: '消息', multiline: true, minLines: 3);import { TextField } from '@tanrei/react-native';
<TextField
label="邮箱"
type="email"
autoComplete="email"
value={email}
onChangeText={setEmail}
supportingText="仅用于登录。"
error={submitted && !isValid(email)}
errorText="请输入完整的邮箱地址。"
onEndEditing={save}
onSubmitEditing={signIn}
/>
<TextField label="消息" multiline minRows={3} value={message} onChangeText={setMessage} />用它的两个成员手写出来的同一个字段,画面是一样的,对辅助技术来说也是同一个文本框,名称、描述和状态都相同:
<tanrei-field label="邮箱" required required-text="必填"
supporting-text="仅用于登录。">
<tanrei-text-input name="email" type="email" autocomplete="email"></tanrei-text-input>
</tanrei-field>手写出来的同一个字段:里面放着一个文本输入框的字段
需要文本字段没有提供的排布方式时,就往下走一层:
| 你需要 | 使用 |
|---|---|
| 标签、方框、帮助文本、错误和计数 | 文本字段 |
| 在框架里面、方框的旁边或下方放你自己的东西 | 一个字段,里面放一个文本输入框和你自己的元素 |
| 只要方框,放在位于别处的标签之下:工具栏里的筛选框、表格的单元格、标签在旁边的一行 | 单独的文本输入框,由它自己的 label 命名 |
到了下一层,变的是各样东西写在哪里。name 和 value 写在文本输入框上,这时它就是表单遇到的那个控件,它的事件也在它上面监听。label、error、disabled 和必填与否仍然写在字段上,由字段传给控件:不要设置输入框自己的那几个。
有一件事是文本字段做、而手写的字段和文本输入框不做的:让计数保持同步。手写时,设置了 counter 的话,每次变化都要把输入框的计数交给字段:
field.countText = input.countText;
input.addEventListener('input', () => (field.countText = input.countText));错误、禁用、只读、前后缀和计数器
类型:密码、搜索、数字、电话和 URL
必填和选填:小标签,以及星号
多行:随文本增高、用调整手柄调整大小(向下,以及双向)、固定
代码:单行的匹配模式、多行的 JSON,以及不换行的代码行
从右到左的页面里的代码:标签和帮助文本跟随页面,代码从左侧开始
分段输入的日期、时间,以及日期和时间。用 Tab、方向键和数字填写
Filled 和 Outlined:Material 预设的变体
m3-like 显示| 方面 | 来自字段 | 来自文本输入框 |
|---|---|---|
| 属性 | label、labelHidden、supportingText、errorText、error、disabled、required、optional、requiredMark、requiredText、optionalText |
其余的每一个:variant、size、type、value、placeholder、readOnly、行数和调整大小、wrap、maxLength、counter、前缀和后缀、autocomplete,以及日期或时间的一切 |
| 事件 | — | input、change、action、pickerRequest、entryError |
| 插槽 | label-icon |
leading-icon、trailing-icon |
| 令牌 | comp.field.* |
方框用 comp.input-box.*,每一种输入方框都读取它;只有文本输入框才绘制的部分用 comp.text-input.* |
文本输入框有自己的 label、labelHidden、error 和 disabled。文本字段设置的不是它们:文本字段的 label 是字段的 label,它为输入框命名,在 filled 或 outlined 的字段里,浮在方框中的也是它。
文本字段没有自己的令牌。它的方框读取文本输入框的令牌,它的各行——标签、标记、帮助文本、错误和计数——读取字段的令牌,无论这些令牌设置在哪里。下面的表格列出的是字段的令牌。要改动某一个实例,就在它上面设置成员的令牌:
<tanrei-text-field label="邮箱"
style="--tanrei-comp-field-label-gap: 12px"></tanrei-text-field>构成
构成
outlined-empty 之上。| 部件 | 说明 |
|---|---|
1 field | The Field: the label row with its glyph and its requirement mark, and the supporting row with the help, the error text and the count the text field gives it (TXF-67). In a look whose input floats the label it draws no label row. |
2 text-input | The text input, as the Field's one control: the box and everything typed in it, the icons, the prefix and the suffix, the segments of a date, the picker button, the resize handle, and the label that floats in the box in the looks that float one. |
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
variant | stacked | filled | outlined | "stacked" | Stacked is a filled box that shows focus and error with a line inside its edge and draws no label: the label of the Field that frames it stands above it. Filled and outlined float the label inside the box, marked by a tinted box and a bottom line, or by a border. Use one style per form. A design system may leave some out (the Tanrei system ships stacked only) or add values. — Text input |
size | small | medium | large | "medium" | Minimum height of a single-line input's container: stacked 32 / 40 / 56, as tall as a Button of the same size, so that an input and a Button side by side in one row stand as tall as each other; filled and outlined 40 / 48 / 56, which hold the label that floats inside them. Medium and large are for forms; small is for an input in an app bar, a toolbar or a dense row of controls, beside a small select or button. The hit area always meets the minimum target. — Text input |
type | text | email | password | number | search | tel | url | code | date | time | date-time | "text" | What the value is: chooses the keyboard, autocorrection, obscured entry and semantics. code is text written for a machine rather than for a reader — a snippet of configuration, a pattern, an identifier, a query, a key: a left-to-right run in the code face that the platform does not correct, capitalise, check for spelling or complete, on one line or, with multiline, on many. date, time and date-time are entered segment by segment in the locale's numeric form and held as ISO 8601. The value is a string for every type. — Text input |
value | string | "" | The text in the field; for date, time and date-time, the ISO 8601 form of what its segments give (2026-09-27, 14:30, 2026-09-27T14:30), or empty while a segment is empty or the value is not available. — Text input |
label | string | "" | Names the control, visibly and to assistive technology. Required, unless the control names itself (a checkbox or a switch with its own label): then leave it empty and the field adds only help and an error. — Field Names the text input. In a filled or outlined look the input draws it, floating in its box, and the Field draws no label row (FLD-25, TXI-13). |
labelHidden | boolean | false | Does not draw the label, which stays the accessible name. For a control whose visible label is elsewhere (a table column, a toolbar). — Field Hides the label wherever it would be drawn: the Field's row, or the input's floating label (FLD-25). |
placeholder | string | "" | A hint shown while the value is empty and the label is not in the input line (above, floated or hidden). Never a replacement for label. In a code field it is an example of the code and is drawn as code (TXI-53, TXI-54); a hint in words belongs in the supportingText of its Field. Ignored by a date, time or date-time field, whose empty segments each show their own. — Text input |
supportingText | string | "" | Help shown below the control and given to assistive technology as the control's description. While the field is in error it gives way to errorText, unless the design system keeps both (comp.field.error-text.placement append). — Field |
errorText | string | "" | Shown while error is true, in supportingText's place — or below it, which then stays, where the design system's comp.field.error-text.placement is append: what is wrong and how to put it right, in words. — Field |
error | boolean | false | Puts the field in the error condition: the error text is drawn, the control is exposed as invalid and, where it has an error condition of its own, drawn in it. What a value means is the product's to validate; the field never sets this itself. — Field The Field puts its text input in the input's own error condition (FLD-8, TXI-11). |
disabled | boolean | false | Disables the control and draws the frame as disabled. Disable a framed control here, not on the control, so that its label follows. — Field The Field disables its text input (FLD-14, TXI-5); the text field is then left out of its form (TXF-63). |
readOnly | boolean | false | The value can be focused, selected and copied, but not changed. — Text input |
required | boolean | false | What the form asks of the control: true exposes it as required and draws the required mark beside the label. It never validates: a missing answer is the product's to report with error. — Field The Field exposes its text input as required (FLD-10, TXI-23). |
optional | boolean | false | Draws the optional tag after the label and adds optionalText to the control's description, because assistive technology has no state for it. Ignored while required is true: a control is never both. — Field |
requiredMark | auto | tag | asterisk | none | "auto" | How this control shows that it is required: tag is a small filled Tag with requiredText, asterisk is "*" after the label, none draws nothing and keeps the semantics. Auto takes comp.field.requirement.mark, which follows sys.behavior.requirement-mark, the system's one answer. Ignored unless required is true. — Field |
requiredText | string | "" | The text of the required tag, in the product's language ("Required", "必須"). The product's: Tanrei has no word for it. Empty reads the product's words; with neither, FLD-24 says what is drawn. — Field |
optionalText | string | "" | The text of the optional tag, in the product's language ("Optional", "任意"); also read to assistive technology as part of the description. The product's: Tanrei has no word for it. Empty reads the product's words. — Field |
multiline | boolean | false | Accepts line breaks and shows several lines, sized as resize says. Ignored for password, date, time and date-time. — Text input |
minRows | number | 2 | The fewest lines a multi-line field shows: the height of an empty auto-resized field, the starting and smallest height of a manually resized one, and the height of a fixed one. — Text input |
maxRows | number | 0 | The most lines an auto-resized field grows to before it scrolls, and the tallest a manually resized one can be made; 0 is no limit. Ignored while resize is fixed. — Text input |
resize | auto | manual | fixed | "auto" | How a multi-line field gets its height. auto: it follows the number of lines between minRows and maxRows, with an animated change, and has no handle. manual: it starts at minRows lines and the person resizes it with the handle in its bottom end corner. fixed: it stays at minRows lines, with no handle. Ignored unless multiline. — Text input |
resizeAxis | vertical | both | "vertical" | What the person can resize by hand: the height only, or the height and the width. Ignored unless resize is manual. — Text input |
resizeLabel | string | "" | The accessible name of the resize handle, in the product's language ("Resize"). The product's: Tanrei has no word for it. Empty reads the product's words. Required while the handle is shown. — Text input |
wrap | boolean | true | Whether a multi-line code field continues a line that does not fit on following rows (true), or keeps every line of the code on one row and scrolls sideways inside its container (false) — for code whose lines are read as rows: a table of values, a log, a file in which a line break means something. The value is the same either way. Ignored unless type is code and multiline is true: prose always wraps, and a single line always scrolls in its line. — Text input |
maxLength | number | 0 | The most user-perceived characters the user can enter; 0 is no limit. Ignored for date, time and date-time. — Text input |
counter | boolean | false | Keeps the character count (and maxLength, when set): part of the input's description and announced when typing pauses (TXI-19). The input draws none: a text field gives the count to its Field, which draws it at the end of its supporting row, and a product that frames the input by hand gives it to the Field's countText. — Text input The input keeps the count, the text field gives it to its Field, and the Field draws it (TXI-19, TXF-67, FLD-27). |
counterText | string | "" | The count said to assistive technology while counter is true and maxLength is greater than 0: the whole sentence in the product's language with three places the input fills — {count} with the characters entered, {limit} with maxLength and {remaining} with how many more can be entered ("{remaining} characters left", "{limit}文字中{count}文字"). Empty reads the product's words; with neither, the count is said as the digits it is drawn in ("12/40"). A sentence that is right for every number, or a prop set per count where the language needs another form for one. — Text input |
counterNoLimitText | string | "" | The count said to assistive technology while counter is true and maxLength is 0: the whole sentence with one place, {count}, filled with the characters entered ("{count} characters", "{count}文字"). Empty reads the product's words; with neither, the count is said as its digits. — Text input |
prefixText | string | "" | Fixed text before the value, not part of it. In a code field it is code: drawn in the code face at the left of the value in every layout direction (TXI-53). — Text input |
suffixText | string | "" | Fixed text after the value, not part of it. In a code field it is code: drawn in the code face at the right of the value in every layout direction (TXI-53). — Text input |
autocomplete | string | "" | The autofill hint, as an HTML autofill field name (email, current-password, new-password, one-time-code, tel, postal-code, …); each platform maps it to its own content type. A code field left without one asks the platform to offer no remembered entry (TXI-55). — Text input |
locale | string | "" | A BCP 47 language tag ("en-US", "ja-JP") for how a date, time or date-time field shows and reads its value: the order of the year, month and day segments, the separators, the clock, the digits and the names of the units. Empty uses the platform's current locale. The calendar is the Gregorian calendar in every locale. — Text input |
min | string | "" | The earliest value a date, time or date-time field accepts from the person, in its ISO 8601 form; empty for no limit. — Text input |
max | string | "" | The latest value a date, time or date-time field accepts from the person, in its ISO 8601 form; empty for no limit. — Text input |
disabledDates | strings | | Dates inside min and max that a date or date-time field does not accept from the person, as ISO 8601 calendar dates — the same list the product gives its DateTimePicker. Ignored by a time field. — Text input |
invalidText | string | "" | The words for a date, time or date-time input committed with some segments filled and others empty: given to the Field that frames it, which shows them as its error text (TXI-30). — Text input Shown by the Field, in its supporting row (FLD-28). |
unavailableText | string | "" | The words for a date, time or date-time input committed with a value before min, after max or, for a date, listed in disabledDates: given to the Field that frames it, which shows them as its error text (TXI-30). — Text input Shown by the Field, in its supporting row (FLD-28). |
segmentEmptyText | string | "" | What assistive technology reads as the value of a date or time segment that holds nothing yet, in the product's language ("Empty"). The product's: Tanrei has no word for it. Empty reads the product's words; with neither, an empty segment has no value text. — Text input |
picker | boolean | false | Shows the picker button in a date, time or date-time field. The field presents no picker itself: it emits pickerRequest, and the product presents a DateTimePicker in the matching mode and sets value to what was picked. — Text input |
pickerLabel | string | "" | The accessible name of the picker button ("Choose date"). Required while picker is shown. — Text input |
插槽
| 插槽 | 说明 |
|---|---|
leading-icon | An icon at the start of the input. — Text input |
trailing-icon | An icon at the end of the input, or the product's own icon-only Button (clear, reveal password). A date or time field's picker button comes after it. — Text input |
label-icon | A glyph before the label. — Field |
事件
| 事件 | 说明 |
|---|---|
input | The user changed the value; carries the new value. Emitted for every change, not when the product sets value. A date, time or date-time field emits it when its value changes — when its last segment is filled, a filled field is edited, or it is emptied — not for a segment that leaves the value empty. — Text input |
change | The user committed a changed value (focus left the field, or the action key was pressed in a single-line field); carries the value. — Text input |
action | The user pressed the keyboard's action key (Enter, Return, Go, Search, Done) in a single-line field. — Text input |
pickerRequest | The user activated the picker button of a date, time or date-time field; carries the value, type, min and max. The product presents the picker and sets value to the picked value. — Text input |
entryError | The user committed a date, time or date-time field that is half-filled or whose value is not available; carries the reason (invalid: some segments are empty; unavailable: before min, after max or a listed date) and the text of the row of segments as drawn. The entry error itself is shown by the Field that frames the input (TXI-30). — Text input |
resizeEnd | The person finished changing the size of a field resized by hand: a drag of its handle ended, or a key or an activation of the handle changed the size (TXI-69). Carries lines, the number of lines the input shows, and width, the field's width in pixels WHILE resizeAxis is both (null otherwise). Not emitted when a change of resize, resizeAxis, minRows or maxRows gives the size back. — Text input |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
TXF-62 | THE text field SHALL be a Field that frames one text input, and nothing else. It SHALL give label, labelHidden, supportingText, errorText, error, disabled, required, optional, requiredMark, requiredText and optionalText to its Field and every other prop to its text input, each unchanged; SHALL hold the label-icon slot for its Field and the leading-icon and trailing-icon slots for its text input; and SHALL emit its text input's events as its own, each once. THE text field SHALL be as wide as the place it stands in, and its text input SHALL fill it: it is a block with no width of its own, as its Field is (FLD-1), around a control that fills the width it is given (TXI-63). It SHALL draw no part of its own, have no token of its own and hold no state of its own but the count it carries from one member to the other (TXF-67): what it shows, how it behaves and what it exposes to assistive technology SHALL be exactly what that Field and that text input do when a product places the one in the other (FLD-1 to FLD-28; TXI-1 to TXI-63), with the Field given the input's count — the label row and its requirement mark (FLD-2 to FLD-6), or the label that floats in the box with the Field's row left out (FLD-25, TXI-13, TXI-50); the supporting row, the error and the count (FLD-7 to FLD-9, FLD-26, FLD-27, TXI-19, TXF-67); the name, the description and the states (FLD-10, FLD-11, FLD-14, TXI-23); and a date's entry error shown in the Field's row (FLD-28, TXI-30). |
TXF-63 | WHERE the platform has forms (the web) THE text field SHALL be the one control the form sees: it SHALL take name, submit its text input's value, reset to the value it was first given, restore it on navigation, report to the form's constraint validation exactly what its text input would report there (TXI-23, TXI-30), and be left out WHILE disabled is true or a disabled group of the form holds it; its text input and its Field SHALL take no part in the form themselves. WHEN the action key asks for the form's submission (TXI-3) the form asked SHALL be the text field's own. |
TXF-64 | WHEN the text field is given focus — by the product, by a press on any part of it that FLD-12 or TXI-8 gives to the input, or by a form that reports its validity — THE text field SHALL pass the focus to its text input (in a date, time or date-time field, to the segment TXI-43 names) and SHALL add no stop to the focus order: the stops inside it are its text input's, in that input's order (an interactive slot element, the picker button, the resize handle). WHILE focus is anywhere inside it THE text field SHALL count as focused for its Field (FLD-13). |
TXF-65 | IF the text field is placed in a Field THE text field SHALL in debug builds report that it is a field already and that the bare control is the text input, and both SHALL still render (FLD-15). WHILE it stands in a field group THE text field SHALL follow that group's disabled, through its own Field, and take nothing else from the group (FGR-20). |
TXF-66 | THE text field SHALL have no words of its own and SHALL read none: requiredText and optionalText are read by its Field (FLD-24), resizeLabel and segmentEmptyText by its text input (TXI-52), each from the instance's prop and then from the product's words. |
TXF-67 | WHILE counter is true THE text field SHALL give its Field, as its count text (FLD-27), the count its text input keeps (TXI-19), as the input writes it, and SHALL give the new one in the same frame in which the value changes — by typing, by a paste, because the product set value or maxLength — without a motion and without an announcement of its own: the input describes and announces its count itself (TXI-19) and the Field keeps the drawn text from assistive technology (FLD-27). WHILE counter is false THE text field SHALL give its Field no count text. THE text field SHALL NOT count, format or write a digit itself. |
TXF-68 | 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 text field 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 text field is invalid — except those the text field owns (aria-label, aria-labelledby, aria-disabled), which it SHALL move and not expose: its name is label (TXF-62). 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 text field SHALL take it off that element. WHEN the product sets an aria- attribute the text field owns THE text field SHALL report it in debug builds. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-text-field | TanreiTextField | TanreiTextField | TanreiTextField | TextField |
variant · 属性 | variant | variant | variant | variant | variant |
size · 属性 | size | size | size | size | size |
type · 属性 | type | type | type | type | type |
value · 属性 | value | text | value | controller | value |
label · 属性 | label | label | label | labelText | label |
labelHidden · 属性 | labelHidden | labelsHidden | labelHidden | labelHidden | labelHidden |
placeholder · 属性 | placeholder | prompt | placeholder | hintText | placeholder |
supportingText · 属性 | supportingText | supportingText | supportingText | helperText | supportingText |
errorText · 属性 | errorText | errorText | errorText | errorText | errorText |
error · 属性 | error | error | isError | isError | error |
disabled · 属性 | disabled | disabled | enabled | enabled | disabled |
readOnly · 属性 | readOnly | readOnly | readOnly | readOnly | readOnly |
required · 属性 | required | isRequired | required | required | required |
optional · 属性 | optional | isOptional | optional | optional | optional |
requiredMark · 属性 | requiredMark | requiredMark | requiredMark | requiredMark | requiredMark |
requiredText · 属性 | requiredText | requiredText | requiredText | requiredText | requiredText |
optionalText · 属性 | optionalText | optionalText | optionalText | optionalText | optionalText |
multiline · 属性 | multiline | axis | singleLine | multiline | multiline |
minRows · 属性 | minRows | lineLimit | minLines | minLines | minRows |
maxRows · 属性 | maxRows | lineLimit | maxLines | maxLines | maxRows |
resize · 属性 | resize | resize | resize | resize | resize |
resizeAxis · 属性 | resizeAxis | resizeAxis | resizeAxis | resizeAxis | resizeAxis |
resizeLabel · 属性 | resizeLabel | resizeLabel | resizeContentDescription | resizeSemanticLabel | resizeAccessibilityLabel |
wrap · 属性 | noWrap | wrap | softWrap | softWrap | wrap |
maxLength · 属性 | maxLength | maxLength | maxLength | maxLength | maxLength |
counter · 属性 | counter | counter | counter | showCounter | counter |
counterText · 属性 | counterText | counterText | counterText | counterSemanticText | counterAccessibilityText |
counterNoLimitText · 属性 | counterNoLimitText | counterNoLimitText | counterNoLimitText | counterNoLimitSemanticText | counterNoLimitAccessibilityText |
prefixText · 属性 | prefixText | prefix | prefix | prefixText | prefix |
suffixText · 属性 | suffixText | suffix | suffix | suffixText | suffix |
autocomplete · 属性 | autocomplete | textContentType | contentType | autofillHints | autoComplete |
locale · 属性 | locale | locale | locale | locale | locale |
min · 属性 | min | min | min | min | min |
max · 属性 | max | max | max | max | max |
disabledDates · 属性 | disabledDates | disabledDates | disabledDates | disabledDates | disabledDates |
invalidText · 属性 | invalidText | invalidText | invalidText | invalidText | invalidText |
unavailableText · 属性 | unavailableText | unavailableText | unavailableText | unavailableText | unavailableText |
segmentEmptyText · 属性 | segmentEmptyText | segmentEmptyText | segmentEmptyText | segmentEmptyText | segmentEmptyText |
picker · 属性 | picker | onPickerRequest | onPickerRequest | onPickerRequest | onPickerRequest |
pickerLabel · 属性 | pickerLabel | pickerLabel | pickerContentDescription | pickerSemanticLabel | pickerAccessibilityLabel |
input · 事件 | input | text | onValueChange | onChanged | onChangeText |
change · 事件 | change | onCommit | onCommit | onCommit | onEndEditing |
action · 事件 | action | onSubmit | keyboardActions | onSubmitted | onSubmitEditing |
pickerRequest · 事件 | pickerrequest | onPickerRequest | onPickerRequest | onPickerRequest | onPickerRequest |
entryError · 事件 | entryerror | onEntryError | onEntryError | onEntryError | onEntryError |
resizeEnd · 事件 | resizeend | onResizeEnd | onResizeEnd | onResizeEnd | onResizeEnd |
| 备注 | The tag is tanrei-text-field, the class TanreiTextField, the React name TextField. Members: <tanrei-field> holding one <tanrei-text-input> in its default slot; the product places none. The host is display: block, as wide as its place, and the input fills it (TXF-62). Value member: tanrei-text-input. The host is the form's element — name, the value and its state, validity anchored on the input, reset to the value attribute, restore, form, a disabled fieldset — and the input inside has no name. Props, set as properties on the member each is from. On the Field: label, labelHidden, supportingText, errorText, error, disabled, required, optional, requiredMark, requiredText, optionalText. On the text input: variant, size, type, value, placeholder, readOnly, multiline, minRows, maxRows, resize, resizeAxis, resizeLabel, noWrap (the inverse of wrap, attribute no-wrap), maxLength, counter, prefixText, suffixText, autocomplete, locale, min, max, disabledDates, invalidText, unavailableText, segmentEmptyText, picker, pickerLabel. Attributes are the members' own: kebab-case, except readonly, maxlength, autocomplete, min and max. Props of one name on both members — label, labelHidden, error, disabled — are the Field's; the input's are never set by the element and reach the input through the connection. Events: input, change, action, pickerrequest, entryerror are the text input's own composed events and leave by themselves, with the host as their target and value already new; action stays cancelable. None is stopped and none is copied. Slots: label-icon is passed to the Field, leading-icon and trailing-icon to the text input, each by a slot element of the same name rendered only while the host has a child for it; the product's own Button in trailing-icon stays the product's element. Parts: the member hosts are field and text-input. Exported from the Field: label, label-icon, requirement-label, supporting-text, counter. Exported from the text input: container, outline, active-indicator, label, requirement-label, input, segment, prefix, suffix, leading-icon, trailing-icon, picker-button, resize-handle. Two names collide, label and requirement-label. Each is exported with its member's name before it, always (field-label, text-input-label, field-requirement-label, text-input-requirement-label), and under the bare name from both members as well, because the two are one thing to a person and are never drawn at once: FLD-25 is the sentence that holds it — while its control draws the label itself the Field draws no label row and no requirement tag, and a stacked input draws neither (TXI-13, TXI-50). So tanrei-text-field::part(label) keeps meaning the drawn label. Focus: the shadow root does not delegate. focus() and blur() call the text input's; a press on the label row — the label, its glyph, the requirement mark — focuses the input by FLD-12, the Field's own sentence (the Field calls focus() on its control, which stands beside it in the same tree); a press on the box, and on a label that floats in it, by TXI-8; and a press on the supporting row — the help, the error text, the count — or on the gap between a row and the box does nothing (FLD-12). The connection is the Field's with its control, in both directions (FLD-25, FLD-26, FLD-28, TXI-62); the element takes no part in it. A member's prop it sets itself (fills): the Field's countText, from the text input's read-only countText after each of the input's updates, in the same task (TXF-67). Parts and tokens of its own: none. The element writes no custom property: --tanrei-comp-field-*, --tanrei-comp-input-box-* and --tanrei-comp-text-input-* reach the members by inheritance, and one text field is set apart by setting any of them on the element. What its twin cannot do that it does: keep the Field's count text equal to the input's count as the person types (TXF-67); nothing else. The twin: <tanrei-field label="Email" supporting-text="We never share your address." required><tanrei-text-input name="email" type="email" autocomplete="email"></tanrei-text-input></tanrei-field>. Its Field draws the help and the error text together (FLD-7, FLD-31), and the five fixtures *-supporting-error* hold the pair to its twin. | Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: text: Binding<String> (as TextField); every edit writes the binding, which is the input event. password draws a SecureField; multiline is TextField(axis: .vertical) with .lineLimit(minRows...maxRows) — not TextEditor, which has no prompt and does not grow. labelHidden follows the .labelsHidden() environment; disabled follows .disabled(_:); action is the onSubmit: parameter, called after the commit (TXI-3); an enclosing .onSubmit modifier still fires, but before the commit, because SwiftUI runs the outer handler first. onCommit is Tanrei's own parameter (SwiftUI deprecated its own), called when focus leaves with a changed value. type maps to .keyboardType, .textInputAutocapitalization, .autocorrectionDisabled and .submitLabel; autocomplete maps HTML names to UITextContentType. locale follows the \.locale environment when not given. The picker button is shown when an onPickerRequest: closure is given (a nil closure is picker false); the product presents the picker and writes the binding. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the \.tanreiWords environment value. Not built on this platform yet, for type code: .autocorrectionDisabled(), .textInputAutocapitalization(.never), .keyboardType(.asciiCapable), a monospaced design of the input's size, and .environment(\.layoutDirection, .leftToRight) on the input row alone; smart quotes, smart dashes and smart insert and delete are traits of the UIKit and AppKit text view (smartQuotesType, smartDashesType, smartInsertDeleteType, isAutomaticQuoteSubstitutionEnabled), so a code field is drawn on that view where SwiftUI's TextField gives no access to them; wrap: false needs that view too (a text container that does not track the view's width, in a scroll view), because TextField(axis: .vertical) always wraps. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). | Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: Compose idiom (as TextField): value / onValueChange, with a TextFieldValue overload for products that manage selection and composition themselves; enabled and singleLine are inverse flags; isError, readOnly, minLines, maxLines, prefix and suffix keep Compose's names. Built on BasicTextField. type maps to KeyboardOptions (keyboard type, capitalization, autocorrect, IME action) and, for password, PasswordVisualTransformation; autocomplete maps to ContentType semantics. onCommit is Tanrei's own callback, called when focus leaves with a changed value or the IME action commits. locale defaults to the current configuration's locale. The picker button is shown when onPickerRequest is not null (null is picker false); pickerContentDescription follows Compose's naming for an icon's accessible name. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from LocalTanreiWords. Not built on this platform yet, for type code: KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false, keyboardType = KeyboardType.Ascii), TextStyle(textDirection = TextDirection.Ltr, textAlign = TextAlign.Left, fontFamily = …, fontFeatureSettings = "liga 0") on the input row inside CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr); softWrap = false puts the BasicTextField in a horizontalScroll that follows the cursor's rectangle. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). | Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: Flutter idiom (as TextField and InputDecoration): the value lives in a TextEditingController (or an internal one with initialValue); labelText, hintText, helperText, errorText, prefixText, suffixText, minLines, maxLines, maxLength, readOnly, enabled and autofillHints keep Flutter's names, enabled being the inverse flag. isRequired and showCounter avoid Dart's required keyword and Flutter's own counter, which Tanrei replaces (it counts grapheme clusters with characters). maxRows: 0 is maxLines: null. type maps to keyboardType, obscureText, autocorrect, textCapitalization and textInputAction. onCommit is Tanrei's own callback, called when focus leaves with a changed value. locale defaults to Localizations.localeOf(context). The picker button is shown when onPickerRequest is not null (null is picker false). Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the TanreiWords inherited widget, which the app fills from its own localizations. Not built on this platform yet, for type code: autocorrect: false, enableSuggestions: false, textCapitalization: TextCapitalization.none, smartDashesType: SmartDashesType.disabled, smartQuotesType: SmartQuotesType.disabled, spellCheckConfiguration: const SpellCheckConfiguration.disabled(), textDirection: TextDirection.ltr, textAlign: TextAlign.left, a TextStyle with the code family and fontFeatures: [FontFeature.disable('liga')], the input row in a Directionality(textDirection: TextDirection.ltr); softWrap: false is an EditableText of unbounded width in a horizontal Scrollable that keeps the caret in view, because a multi-line TextField always wraps. Not built on this platform yet: helperText stays while the field is in error and errorText is drawn below it (FLD-7, FLD-31), unlike InputDecoration; today the error text takes the help's place. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement (FLD-7, FLD-31), size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). | Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: React Native idiom (as TextInput): value / onChangeText, onEndEditing for a commit, onSubmitEditing for the action key; multiline, readOnly, maxLength and autoComplete keep TextInput's names. type maps to keyboardType, secureTextEntry, autoCapitalize, autoCorrect, inputMode and returnKeyType. A controlled value echoed back during a composition must not reset it (TXI-4). React Native gives JavaScript no signal that a composition is in progress, so the component cannot defer a write the product makes mid-composition; echoing the value back is safe. For date, time and date-time, onChangeText receives the value (ISO 8601 or empty), not the text as typed (TXI-29). The picker button is shown when onPickerRequest is given (undefined is picker false). Locale data needs Intl with formatToParts, which Hermes has only in part. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the TanreiWordsProvider context. Not built on this platform yet, for type code: autoCorrect={false}, autoCapitalize="none", spellCheck={false}, autoComplete="off", smartInsertDelete={false}, keyboardType="ascii-capable" on iOS and "visible-password" on Android only where suggestions cannot be turned off otherwise, style={{ writingDirection: 'ltr', textAlign: 'left', fontFamily: … }}, the input row with direction: 'ltr'. TextInput has no multi-line form that does not wrap: wrap={false} needs a native text view, and until one exists the field wraps and its README says so. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-field-counter-gap | sys.space.space200 | 16px |
--tanrei-comp-field-error-text-gap | sys.space.space50 | 4px |
--tanrei-comp-field-error-text-placement | — | 0 |
--tanrei-comp-field-label-icon-gap | sys.space.space100 | 8px |
--tanrei-comp-field-label-icon-size | sys.icon.small | 20px |
--tanrei-comp-field-label-gap | sys.space.space100 | 8px |
--tanrei-comp-field-motion-state-duration | sys.motion.role.state.default | 250ms |
--tanrei-comp-field-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-field-requirement-asterisk-gap | sys.space.space25 | 2px |
--tanrei-comp-field-requirement-gap | sys.space.space100 | 8px |
--tanrei-comp-field-requirement-mark | sys.behavior.requirement-mark | 0 |
--tanrei-comp-field-standard-counter-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-disabled-counter-color | — | |
--tanrei-comp-field-standard-disabled-error-text-color | — | |
--tanrei-comp-field-standard-disabled-error-counter-color | — | |
--tanrei-comp-field-standard-disabled-error-label-icon-color | — | |
--tanrei-comp-field-standard-disabled-error-label-color | — | |
--tanrei-comp-field-standard-disabled-error-required-asterisk-color | — | |
--tanrei-comp-field-standard-disabled-error-supporting-text-color | — | |
--tanrei-comp-field-standard-disabled-error-value-text-color | — | |
--tanrei-comp-field-standard-disabled-label-icon-color | — | |
--tanrei-comp-field-standard-disabled-label-color | — | |
--tanrei-comp-field-standard-disabled-required-asterisk-color | — | |
--tanrei-comp-field-standard-disabled-supporting-text-color | — | |
--tanrei-comp-field-standard-disabled-value-text-color | — | |
--tanrei-comp-field-standard-error-text-color | sys.color.error | |
--tanrei-comp-field-standard-error-counter-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-error-label-icon-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-error-label-color | sys.color.on-surface | |
--tanrei-comp-field-standard-error-required-asterisk-color | sys.color.primary | |
--tanrei-comp-field-standard-error-supporting-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-error-value-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-focused-counter-color | — | |
--tanrei-comp-field-standard-focused-error-text-color | — | |
--tanrei-comp-field-standard-focused-error-counter-color | — | |
--tanrei-comp-field-standard-focused-error-label-icon-color | — | |
--tanrei-comp-field-standard-focused-error-label-color | — | |
--tanrei-comp-field-standard-focused-error-required-asterisk-color | — | |
--tanrei-comp-field-standard-focused-error-supporting-text-color | — | |
--tanrei-comp-field-standard-focused-error-value-text-color | — | |
--tanrei-comp-field-standard-focused-label-icon-color | — | |
--tanrei-comp-field-standard-focused-label-color | — | |
--tanrei-comp-field-standard-focused-required-asterisk-color | — | |
--tanrei-comp-field-standard-focused-supporting-text-color | — | |
--tanrei-comp-field-standard-focused-value-text-color | — | |
--tanrei-comp-field-standard-label-icon-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-label-color | sys.color.on-surface | |
--tanrei-comp-field-standard-required-asterisk-color | sys.color.primary | |
--tanrei-comp-field-standard-supporting-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-standard-value-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-supporting-text-gap | sys.space.space50 | 4px |
--tanrei-comp-field-value-text-gap | sys.space.space200 | 16px |
--tanrei-comp-field-label-typescale | sys.typescale.label | 500 14px/1.4286 |
--tanrei-comp-field-supporting-text-typescale | sys.typescale.label-small | 500 12px/1.3333 |
--tanrei-comp-field-value-text-typescale | sys.typescale.label | 500 14px/1.4286 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
variant: stacked (71)
stacked-empty
variant: stacked label: Email stacked-filled
variant: stacked value: [email protected] label: Email stacked-placeholder
variant: stacked label: Email placeholder: [email protected] stacked-large
variant: stacked size: large value: [email protected] label: Email stacked-small
variant: stacked size: small value: tokens label: Search stacked-label-hidden
variant: stacked label: Search labelHidden: true placeholder: Search stacked-supporting
variant: stacked value: [email protected] label: Email supportingText: We never share your address. stacked-error
variant: stacked value: ada@ label: Email errorText: Enter a complete address. error: true stacked-supporting-error
variant: stacked type: password value: ada1 label: Password supportingText: At least 8 characters, with one number. errorText: This password is too short. error: true stacked-supporting-error-long
variant: stacked value: 12 Example Stret label: Billing address supportingText: Use the address your bank has on file, w… errorText: We could not find this address. Check th… error: true stacked-supporting-error-rtl
variant: stacked type: password value: ada1 label: סיסמה supportingText: לפחות 8 תווים, כולל ספרה אחת. errorText: הסיסמה קצרה מדי. error: true direction: rtl stacked-disabled
variant: stacked value: [email protected] label: Email disabled: true stacked-read-only
variant: stacked value: [email protected] label: Email readOnly: true stacked-icons
variant: stacked value: tanrei label: Search stacked-label-icon
variant: stacked value: Ada Lovelace label: Display name slots: label-icon stacked-affixes
variant: stacked type: number value: 120 label: Amount prefixText: $ suffixText: USD stacked-counter
variant: stacked value: Ada Lovelace label: Display name maxLength: 40 counter: true stacked-supporting-error-counter
variant: stacked value: Ada label: Display name supportingText: Shown beside your posts. errorText: This name is taken. Choose another. error: true maxLength: 40 counter: true stacked-multiline
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 stacked-password
variant: stacked type: password value: correct horse label: Password stacked-date
variant: stacked type: date value: 2026-09-27 label: Start date locale: en-US stacked-date-empty
variant: stacked type: date label: Start date locale: en-US segmentEmptyText: Empty stacked-date-picker
variant: stacked type: date value: 2026-09-27 label: Start date locale: en-US picker: true pickerLabel: Choose date stacked-date-picker-large
variant: stacked size: large type: date value: 2026-09-27 label: Start date locale: en-US picker: true pickerLabel: Choose date stacked-date-ja
variant: stacked type: date value: 2026-09-27 label: 開始日 locale: ja-JP picker: true pickerLabel: 日付を選択 stacked-time-picker
variant: stacked type: time value: 14:30 label: Start time locale: en-US picker: true pickerLabel: Choose time stacked-date-time-picker
variant: stacked type: date-time value: 2026-09-27T14:30 label: Starts locale: en-US picker: true pickerLabel: Choose date and time stacked-date-error
variant: stacked type: date value: 2026-09-27 label: Start date errorText: Choose a date after today. error: true locale: en-US picker: true pickerLabel: Choose date stacked-date-disabled
variant: stacked type: date value: 2026-09-27 label: Start date disabled: true locale: en-US picker: true pickerLabel: Choose date stacked-multiline-auto-grown
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true stacked-multiline-auto-grown-icons
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true stacked-multiline-auto-max
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true maxRows: 3 stacked-multiline-manual
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 resize: manual resizeLabel: Resize stacked-multiline-manual-both
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 resize: manual resizeAxis: both resizeLabel: Resize stacked-multiline-manual-rtl
variant: stacked value: תודה על הסקירה. label: הודעה multiline: true minRows: 3 resize: manual resizeAxis: both resizeLabel: שינוי גודל direction: rtl stacked-multiline-fixed
variant: stacked value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 resize: fixed stacked-time-empty
variant: stacked type: time label: Start time locale: en-US segmentEmptyText: Empty stacked-time-24h
variant: stacked type: time value: 14:30 label: 開始時刻 locale: ja-JP stacked-date-rtl
variant: stacked type: date value: 2026-09-27 label: תאריך התחלה locale: he-IL picker: true pickerLabel: בחירת תאריך direction: rtl stacked-required
variant: stacked label: Email required: true requiredText: Required stacked-optional
variant: stacked label: Company optional: true optionalText: Optional stacked-required-asterisk
variant: stacked label: Email required: true requiredMark: asterisk stacked-required-ja
variant: stacked value: 山田 太郎 label: 氏名 required: true requiredText: 必須 stacked-optional-ja
variant: stacked label: 会社名 optional: true optionalText: 任意 stacked-required-label-icon
variant: stacked value: Ada Lovelace label: Display name required: true requiredText: Required slots: label-icon stacked-required-error
variant: stacked label: Email errorText: Enter your email address. error: true required: true requiredText: Required stacked-required-disabled
variant: stacked value: [email protected] label: Email disabled: true required: true requiredText: Required stacked-required-rtl
variant: stacked label: אימייל required: true requiredText: חובה direction: rtl stacked-code
variant: stacked type: code value: tanrei.comp.button.*.container.color label: Pattern stacked-code-empty
variant: stacked type: code label: Pattern placeholder: tanrei.comp.*.color stacked-code-large
variant: stacked size: large type: code value: tanrei.comp.button.*.container.color label: Pattern stacked-code-affixes
variant: stacked type: code value: ^sys\.color\. label: Pattern supportingText: A regular expression, matched against a… prefixText: / suffixText: /u stacked-code-multiline
variant: stacked type: code value: {
"id": "acme",
"extends": "tanrei"… label: Layer multiline: true minRows: 3 stacked-code-multiline-empty
variant: stacked type: code label: Layer placeholder: { "id": "acme", "extends": "tanrei" } multiline: true minRows: 3 stacked-code-multiline-wrap
variant: stacked type: code value: { "match": "tanrei.comp.button.*.hovered… label: Rule multiline: true stacked-code-multiline-nowrap
variant: stacked type: code value: { "match": "tanrei.comp.button.*.hovered… label: Rule multiline: true minRows: 3 wrap: false stacked-code-multiline-tabs
variant: stacked type: code value: build:
pnpm generate
pnpm check label: Recipe multiline: true stacked-code-multiline-error
variant: stacked type: code value: {
"id": "acme",
} label: Layer errorText: Line 3: a name was expected before }. error: true multiline: true minRows: 3 stacked-code-multiline-counter
variant: stacked type: code value: {
"id": "acme",
"extends": "tanrei"… label: Layer multiline: true maxLength: 200 counter: true stacked-code-multiline-manual
variant: stacked type: code value: { "match": "tanrei.comp.button.*.hovered… label: Rule multiline: true minRows: 3 resize: manual resizeAxis: both resizeLabel: Resize wrap: false stacked-code-multiline-read-only
variant: stacked type: code value: { "match": "tanrei.comp.button.*.hovered… label: Rule readOnly: true multiline: true wrap: false stacked-code-multiline-disabled
variant: stacked type: code value: {
"id": "acme",
"extends": "tanrei"… label: Layer disabled: true multiline: true stacked-code-rtl
variant: stacked type: code value: tanrei.comp.button.*.container.color label: النمط supportingText: معرّف رمز، مع * لأي جزء. direction: rtl stacked-code-empty-rtl
variant: stacked type: code label: النمط placeholder: tanrei.comp.*.color direction: rtl stacked-code-affixes-rtl
variant: stacked type: code value: ^sys\.color\. label: النمط prefixText: / suffixText: /u direction: rtl stacked-code-multiline-rtl
variant: stacked type: code value: {
"id": "acme",
"extends": "tanrei"… label: الطبقة supportingText: الصق ملف الطبقة هنا. required: true requiredText: مطلوب multiline: true minRows: 3 maxLength: 200 counter: true direction: rtl stacked-code-multiline-empty-rtl
variant: stacked type: code label: الطبقة placeholder: { "id": "acme", "extends": "tanrei" } multiline: true minRows: 3 direction: rtl stacked-code-multiline-brace-rtl
variant: stacked type: code value: { label: الطبقة multiline: true minRows: 3 direction: rtl stacked-code-multiline-arabic-first-rtl
variant: stacked type: code value: ["مرحبا", "acme"]
// تعليق: comment label: المصادر multiline: true minRows: 3 direction: rtl stacked-code-multiline-nowrap-rtl
variant: stacked type: code value: { "match": "tanrei.comp.button.*.hovered… label: القاعدة errorText: السطر 2: القاعدة ينقصها سبب. error: true multiline: true minRows: 3 wrap: false direction: rtl stacked-code-multiline-manual-rtl
variant: stacked type: code value: {
"id": "acme",
"extends": "tanrei"… label: الطبقة multiline: true minRows: 3 resize: manual resizeAxis: both resizeLabel: تغيير الحجم direction: rtl variant: filled (7)
filled-empty
variant: filled label: Email filled-filled
variant: filled value: [email protected] label: Email filled-large
variant: filled size: large value: [email protected] label: Email filled-error
variant: filled value: ada@ label: Email errorText: Enter a complete email address. error: true filled-disabled
variant: filled value: [email protected] label: Email disabled: true filled-multiline
variant: filled value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 filled-code-multiline
variant: filled type: code value: {
"id": "acme",
"extends": "tanrei"… label: Layer multiline: true minRows: 3 variant: outlined (19)
outlined-empty
variant: outlined label: Email outlined-filled
variant: outlined value: [email protected] label: Email outlined-large
variant: outlined size: large value: [email protected] label: Email outlined-label-hidden
variant: outlined type: search label: Search labelHidden: true placeholder: Search components outlined-supporting
variant: outlined value: [email protected] label: Email supportingText: We only use it to sign you in. outlined-error
variant: outlined value: ada@ label: Email errorText: Enter a complete email address. error: true outlined-supporting-error
variant: outlined value: ada@ label: Email supportingText: We never share your address. errorText: Enter a complete email address. error: true outlined-disabled
variant: outlined value: [email protected] label: Email disabled: true outlined-read-only
variant: outlined value: [email protected] label: Email readOnly: true outlined-icons
variant: outlined value: tanrei label: Search outlined-affixes
variant: outlined type: number value: 120 label: Amount prefixText: $ suffixText: USD outlined-counter
variant: outlined value: Ada Lovelace label: Display name maxLength: 40 counter: true outlined-multiline
variant: outlined value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 outlined-password
variant: outlined type: password value: correct horse label: Password outlined-date-picker
variant: outlined type: date value: 2026-09-27 label: Start date locale: en-US picker: true pickerLabel: Choose date outlined-multiline-manual
variant: outlined value: Thanks for the review.
The new tokens ar… label: Message multiline: true minRows: 3 resize: manual resizeLabel: Resize outlined-required
variant: outlined label: Email required: true requiredText: Required outlined-optional
variant: outlined label: Company optional: true optionalText: Optional outlined-code-multiline
variant: outlined type: code value: {
"id": "acme",
"extends": "tanrei"… label: Layer multiline: true minRows: 3 生成自 specs/text-field.spec.json, tokens/comp/field.tokens.json, design/tokens/
- 这个元素的 shadow root 里有一个
<tanrei-field>,其中放着一个<tanrei-text-input>,此外什么都没有。导入这个元素,两个成员也随之完成定义。 - 这个元素是一个块,会填满所在的地方,而不是一个和浏览器自己的文本输入框一样宽的行内方框。
- 这个元素是与表单关联的元素。它接受
name和form,提交它的值——禁用时什么都不提交——重置为value属性(attribute)的值,在导航时恢复,跟随被禁用的<fieldset>;设置了required而它为空时,报告缺少值。它的两个成员不向表单添加任何条目。在单行的字段里按 Enter 会按下元素所在表单的默认按钮;取消action可以阻止这一点。 input、change、action、pickerrequest和entryerror是文本输入框自己的事件。它们的目标是这个元素,在每个监听器里event.target.value都是新的值。detail 的类型是输入框的:TanreiTextInputActionDetail、TanreiTextInputPickerRequestDetail和TanreiTextInputEntryErrorDetail。- 部件:
field和text-input是两个成员。成员自己的部件在这里的名称与下一层相同:label-icon、supporting-text、counter;container、outline、active-indicator、input、segment、prefix、suffix、leading-icon、trailing-icon、picker-button、resize-handle。label和requirement-label是画出来的那一个,无论由哪个成员绘制;field-label、text-input-label、field-requirement-label和text-input-requirement-label指的是某一个成员的。 - 这个元素自己不能获得焦点,也没有角色:
focus()会把焦点交给输入框。按标签行、方框或浮动标签,都会让输入框获得焦点。按帮助文本、错误文本、计数,或者某一行与方框之间的空隙,什么都不会发生。 - 用
label为它命名——不该绘制标签的地方再加上label-hidden——绝不要用aria-label,它在这里不为任何东西命名。 - 属性(attribute)就是成员自己的那些:用 kebab-case 书写,例如
supporting-text、error-text、min-rows和no-wrap,只有readonly、maxlength、autocomplete、min和max例外。没有required属性(attribute):请写required。 - 在元素定义之前,它没有尺寸。请预留高度。
当用户要键入一个值时,使用文本字段:姓名、电子邮件地址、搜索词、消息。当用户是从一组已知的值里挑选时,请使用专为选择而设计的控件。标签在别处、因而只要方框的地方——工具栏里的筛选框、表格的单元格——请使用文本输入框。
一个表单从头到尾使用同一种变体。
| 变体 | 特征 | 用在哪里 |
|---|---|---|
| Stacked(默认) | 标签位于一个没有边线的着色方框上方。聚焦时在方框边缘以内画一条线,不会挪动任何东西;出错时,静止状态下画一条细线。 | 所有系统。它是 Tanrei 系统提供的唯一变体。 |
| Filled | 一个着色的方框和一条底线,标签浮在里面 | Material 和 cupertino 预设 |
| Outlined | 一圈聚焦时变粗的边框,标签浮到边框上 | Material 和 cupertino 预设 |
在所有系统里,包括各个预设,默认都是 stacked。想要 Material 式浮动标签的 Material 产品,可以把文本字段的 variant 设为 outlined 或 filled。在 Tanrei 系统下,filled 和 outlined 仍然可用,但开发时会给出警告。
medium(40)是紧凑的文本字段,large(56)是标准的文本字段。筛选条件和密集的设置界面使用 medium,登录和结账表单使用 large。small(32)是小号下拉选择框的高度,用于应用栏、工具栏或一排紧凑控件中的文本字段,此时输入文本为 14。在默认系统中,另外两种尺寸的输入文本都是 16。高度是最小值:文本放大时,文本字段会随之增高,点击区域也始终满足最小目标尺寸。
- 每个文本字段都需要
label。在 stacked 文本字段中,它位于方框上方,从起始边开始,绝不移动,也不改变大小;标签太长时会换行,而不是被截断。label-icon插槽可以在它前面放一个图标。在 filled 或 outlined 文本字段中,文本字段为空时标签停在文本字段里,聚焦或已有内容时则浮起。 labelHidden不再绘制标签,但它仍然是无障碍名称,帮助文本和错误也仍然位于方框下方。只要方框的地方——工具栏里的筛选框、单元格、标签在旁边的一行——请改用带有自己label的文本输入框。- 占位符是一条提示,例如一个示例值。它在文本字段为空、并且标签不在输入行里时显示:在 stacked 文本字段中始终如此,在其他变体中则只在标签浮起或隐藏时如此。它绝不是名称。
required 和 optional 说明表单对一个文本字段的要求:required 表示必须填写,optional 表示可以不填。一个文本字段不可能既必填又选填:两者都设置时它是必填的。
- 必填会把文本字段暴露为必填,并在标签旁边绘制必填标记。
- 选填会绘制选填小标签,并把它的文字加入文本字段的描述。
- 标记默认是 stacked 标签后面的一个小标签:必填用 filled 样式,选填用 tonal 样式。它的文字是
requiredText和optionalText,它们是你的:写在文本字段上,或者用setWords()为整个产品只给一次。Tanrei 不自带任何文字:没有requiredText时,小标签的位置上是一个星号;没有optionalText时,不绘制小标签。它不会让标签所在的行变高;标签没有给它留出空间时,它会换到单独的一行。 - 改用星号。 必填的文本字段绘制哪种标记,是设计系统为所有表单做的一项选择,即
sys.behavior.requirement-mark:默认系统中是小标签,Material 预设中是星号,cupertino 中什么都不画。requiredMark可以为单个文本字段覆盖它(tag、asterisk或none;none保留语义,但什么都不画)。filled 或 outlined 文本字段在要求绘制小标签的地方绘制星号。 - 浮动标签中的选填。 filled 或 outlined 文本字段在浮动的标签旁边没有放选填小标签的位置:它改为在标签后面以一个词的形式绘制
optionalText(“Company Optional”“公司名称(选填)”),随标签一起静止和浮动。按照你的表单的写法来写这个词,带不带括号都可以。 - 它不做任何校验。 文本字段绝不会因为缺少值而设置
error。这要由你用error和errorText来做。
在一个表单里,标记两者中较少的那一种:大多数文本字段必填时,标记选填的;反之亦然。如果使用星号——filled 或 outlined 文本字段对必填字段总是使用星号——请在表单中靠近顶部的地方说明一次它的含义(“* 必填”):星号是一种惯例,而不是文字,并非人人都知道。
必填的文本字段会显示一个标记:不想要标记的地方,把 requiredMark 设为 none。
supportingText是文本字段下方的一行帮助文本。它也会作为文本字段的描述被朗读。- 校验由你负责。值有误时,设置
error并编写errorText:文本字段处于错误状态时,它取代辅助文本的位置,所以处于错误状态的表单不会比平时更高。帮助文本里仍然重要的内容,要在错误文本中再说一遍。希望帮助文本始终可见的设计系统,把comp.field.error-text.placement设为append:这时错误文本绘制在帮助文本的下方,文本字段处于错误状态时会高出一行(参见字段)。 - 要说明如何改正这个值,而不只是说它错了:写“请输入完整的邮箱地址。”,而不是“无效”。
- 在用户离开文本字段或提交表单之后才显示错误,不要在用户还在输入时显示。
maxLength限制用户可以输入的内容。它按用户看到的字符来计数,所以一个表情符号或一个带重音的字母只算一个。粘贴的内容会被截短到刚好放得下。你自己设置的值绝不会被截短。counter在辅助文本那一行的末尾显示计数;设置了maxLength时,后面还跟着上限(“12/40”)。- 只有当上限对用户有意义时才显示计数器,例如显示名称或简短的个人简介。
type 决定键盘、自动更正和是否遮蔽输入。无论哪种类型,值都是字符串。
| 类型 | 键盘和行为 |
|---|---|
text |
通用键盘。 |
email |
电子邮件键盘,不自动大写,也不自动更正。 |
password |
遮蔽的安全输入,不自动更正。始终为单行。 |
number |
带小数分隔符的数字键盘。值不会被解析、取整或校验。 |
search |
通用键盘,操作键显示为搜索。 |
tel |
电话拨号键盘。 |
url |
URL 键盘,不自动大写,也不自动更正。 |
code |
通用键盘,并且不对文本做任何处理:不自动更正,不自动大写,不做拼写检查,也不替换字符。单行,或者在设置 multiline 时为多行。参见代码。 |
date、time、date-time |
日期、时间,或两者兼有,按区域设置的数字格式逐个单位输入。始终为单行。参见日期和时间。 |
加上 autocomplete,并给它一个 HTML 自动填充名称(email、current-password、one-time-code 等),平台就能填写这个文本字段。每个平台会把它映射为自己的内容类型。长度已知、按格子绘制的简短验证码,应使用一次性验证码输入框。
multiline 接受换行,并显示多行。resize 说明文本字段的高度从何而来:
resize |
高度 | 调整手柄 | minRows 和 maxRows |
|---|---|---|---|
auto(默认) |
跟随行数,带一段短促的动效 | 无 | 最少和最多的行数。超过最多行数后滚动;0 表示没有限制 |
manual |
由用户决定。初始为 minRows |
位于末尾边的底角 | 能调到的最小和最大高度 |
fixed |
minRows 行 |
无 | maxRows 被忽略 |
- Auto 适合大多数文本字段:评论、消息、描述。长文本不应把表单的其余部分挤开时,设置
maxRows。 - Manual 适合用户会回头阅读和整理的文本:备注、长描述、代码。
resizeAxis默认为vertical;设为both时,用户还可以在布局允许的范围内改变文本字段的宽度。调整后的大小会保持下来,文本在其中换行和滚动。 - Fixed 适合布局不能变动的表单,例如地址块。
- 调整手柄不靠拖动也能用。 它是位于末尾边底角的小尺寸调整手柄,在指针移到文本字段上或文本字段获得焦点之前不显示;它也是一个焦点停靠点,由
resizeLabel命名。方向键每次调整一行,Page Up 和 Page Down 每次调整minRows行,Home 和 End 调到最小和最大高度;点击、Enter 或 Space 让高度贴合文本,如果已经贴合,则回到minRows。resizeAxis为both时,同一次点击还会让文本字段与所在位置一样宽,再点一次则恢复原来的宽度,所以用指针不拖动也能改变宽度。它的目标是最小目标,尽管它的线条很小。用户改变大小之后,文本字段会触发resizeend,其中带有它显示的行数(在两个方向都能调整时,还有它的宽度),方便你保存用户选定的大小。禁用的文本字段没有调整手柄;只读的文本字段保留它,因为阅读时可能需要更多空间。 - 文本上方的东西都不会动。 多行文本字段的第一行就在单行文本字段绘制其文本行的位置,它的图标、前缀和后缀都以这一行为准居中。其余各行都排在下面,所以高度变化时,标签和第一行保持原位。
type="code" 用于写给机器、而不是写给读者的文本:匹配模式、标识符、密钥、查询语句、一小段 JSON 或 YAML。它还是同一个文本字段——标签、帮助文本、错误、计数器、尺寸和在表单中的行为都一样——只是输入行会按代码写下的样子原样保存它。
- 在任何语言里都从左到右。 值、它的插入点、占位符、前缀和后缀排成从左到右的一行,从左侧开始;在从右到左的页面里也是如此,也不管代码的第一个字母是什么:空的文本字段、只有一个
{的文本字段,以及第一个字符串是阿拉伯文的文本字段,布局都一样。其余的一切跟随页面:标签、图标、帮助文本或错误文本、计数器和调整手柄。用从右到左的文字写的字符串或注释,只在它所在的行内排布,不会打乱周围任何东西的顺序。 - 代码字体,字号与输入文本相同。 字体族是代码角色的;字号和行高仍然是输入文本的,所以代码字段和旁边的文本字段一样高。每个空格都会保留,制表符会对齐到下一个制表位,任何两个字符都不会合成连字。不会按含义给任何内容着色。
- 不做改动。 文本字段会要求平台不要更正、大写、检查拼写、翻译或补全这个值,也不要把直引号换成弯引号。粘贴进来的是纯文本,一个字符都不变。值绝不会被去掉首尾空白、解析或校验:它的格式是否正确,要由你用
error和errorText来说明。 - 单行或多行。 单行适合匹配模式或密钥。设置
multiline后,默认情况下,放不下的代码行会接着排到后面的行上;值里只有代码本身的换行。 - 不换行的代码行。
wrap为 false(在 Web 上是no-wrap)时,代码的每一行都保持在字段的一行里,适合数值表、日志,或者换行本身有含义的文件。长的行在文本字段内部横向滚动,不会把任何东西撑宽。只有多行的代码字段才读取wrap:普通文本始终换行,单行则始终在自己的行内滚动。 - Tab 移动焦点。 没有任何按键改变含义:Tab 绝不缩进,Enter 不添加缩进,也不会替你补上右括号。制表符只能通过粘贴或由你的产品写入,并且会保留下来。
- 它不是编辑器。 没有语法着色,没有行号,没有折叠,也没有逐行的错误标记。只展示、不键入的代码,请使用代码块;需要编辑器时,请嵌入你的产品选定的那一个。
- 占位符是一段代码示例,按代码绘制。用文字写的提示应放在
supportingText里,文本字段也在那里说明它期望什么(“JSON”)。
在从右到左的文本字段里,代码值从左侧开始,而 email、url、tel、number 或 password 的值虽然从左到右书写,却位于文本字段的起始边,也就是右侧。其他平台还没有实现代码类型。
date、time 或 date-time 文本字段是输入行里的一排分段,每个单位一个:月、日和年,时、分,以及 12 小时制下的上午/下午。它们按区域设置的顺序排列,并使用该区域设置的分隔符。文本字段以 ISO 8601 格式保存值,不带时区。
| 类型 | 值 | en-US 中的一行 | ja-JP 中的一行 |
|---|---|---|---|
date |
2026-09-27 |
09/27/2026 | 2026/09/27 |
time |
14:30 |
2:30 PM | 14:30 |
date-time |
2026-09-27T14:30 |
09/27/2026, 2:30 PM | 2026/09/27 14:30 |
- 区域设置。
locale(未设置时为平台的区域设置)决定顺序、分隔符、时制和数字。空的分段显示它自己的占位符(“mm”“dd”“yyyy”),所以这一行始终显示出它的格式。每个分段的宽度取它的占位符和它最宽的值两者中较宽的那个,所以输入或清空时这一行都不会移动。这些类型会忽略placeholder、prefixText、suffixText、maxLength和multiline。 - 移动。 Tab 依次经过每个分段,然后是选择器按钮。无论布局方向如何,左方向键和右方向键都移到相应一侧的分段:这一行是从左到右书写的,就像用数字写的日期一样。按下某个分段会让它获得焦点。获得焦点的分段画成选中的文本,没有插入点。
- 步进。 上方向键和下方向键把分段的值加一或减一,到头后循环;在空的分段上,它们从今天的日期或当前时间开始。Page Up 和 Page Down 的步幅更大,Home 和 End 移到范围的两端。
- 键入。 数字填入分段;当再输入任何数字都无法延续它,或者键入了分隔符时,焦点移到下一个分段。Backspace 删除一位数字,在空的分段上则退回上一个分段。年份需要四位数字。不存在的日期无法输入:新选的月份没有那么多天时,日会变成该月的最后一天。
- 粘贴。 把完整的日期或时间粘贴到任意一个分段上,都会填满整行。它按区域设置的顺序被读作三组数字,而第一组如果是四位数字,就读作年份,所以粘贴 ISO 格式的日期在任何地方都有效。复制和剪切针对的是整行。
- 值在每个分段都填好、并且结果可用之前一直为空。值发生变化时触发
input——最后一个分段填好了、已填满的一行被编辑了,或者被清空了——而让值保持为空的分段变化不触发。当焦点离开文本字段的分段和选择器按钮,或者按下操作键,并且值已改变时,触发change。在分段之间移动不提交任何东西。 - 输入错误。 只填了一半就离开的文本字段显示
invalidText。离开时值早于min、晚于max或列在disabledDates中的文本字段显示unavailableText。两者都画成错误,并触发带有原因(invalid或unavailable)的entryError;下一次编辑会结束它。这是文本字段唯一会自己设置的错误,它不会动你的error属性。 - 你设置的值会原样显示,绝不校验,也不触发任何事件。
文本字段的时制就是它的区域设置的时制:它不像日期时间选择器那样有 12 或 24 小时制的设置,也没有分钟步长。
选择器按钮。 设置 picker 后,文本字段在末尾绘制一个纯图标的 Button,日期用 calendar_month,时间用 schedule,由 pickerLabel 命名。激活它会触发 pickerRequest,其中带有值、类型、min 和 max;只填了一半的一行会发送空值,并且不引发输入错误,因为选择器马上就会给出一个完整的值。文本字段自己不呈现任何东西:由你的产品以对应的模式打开 DateTimePicker(停靠式的要锚定到文本字段上),并根据选择器的 change 设置文本字段的 value。给选择器设置与文本字段相同的 min、max 和 disabledDates。选择器关闭时,焦点回到这个按钮。文本字段禁用时按钮也禁用,文本字段只读时则不绘制按钮。
- 把不属于值的固定文本放进
prefixText和suffixText:货币符号、单位。 - 清除或显示密码之类的操作,以你自己的纯图标的 Button 放进后置插槽,并给它自己的名称。选择器按钮是文本字段唯一内建的操作,它排在你的按钮之后。
- 可能写到好几行的文本使用
multiline。默认情况下,它从minRows增高到maxRows,之后滚动。 - 在表单要求填写的文本字段上设置
required,并且在提交表单时,仍然要在errorText中说明缺了什么。 - 由机器读取的值——匹配模式、密钥、一小段配置——使用
type="code",并在标签和supportingText里说明它必须是什么。
- 不要把占位符当作标签。用户一开始输入,它就消失了。
- 不要把
number用于不是数量的值,例如卡号或邮政编码。请使用text并配上合适的autocomplete。 - 不要在一个表单里混用不同的变体。
- 不要把文本字段放进字段。它已经是一个字段;调试构建会报告这一点。字段框住的控件是文本输入框。
- 不要用
text文本字段加上自己的解析来处理日期。请使用date,这样区域设置的顺序、分段的按键和输入错误都是现成的。 - 不要在设置
required的同时,还在标签里写“(必填)”。标记已经说明了这一点,屏幕阅读器会读两遍。 - 不要让手动调整大小的文本字段成为看到长文本的唯一途径。给它足够的
minRows,让它一出现就好用。 - 不要仅仅为了显示一个不能更改的值而禁用文本字段。请使用
readOnly,这样用户仍然可以聚焦、选择并复制它。 - 不要用
::part(input)或你自己的样式从外部给文本字段设置代码字体,也不要给这个元素加上dir或spellcheck。请使用type="code":只换字体的话,文本字段看起来没问题,却仍然会更正键入的内容。 - 不要把代码字段当作编辑器,也不要用禁用的代码字段来展示长代码:禁用的文本字段没有可以用来滚动的插入点。请使用
readOnly,或者代码块。
- 角色
textbox- 标注
- The text field adds nothing to what its members expose, and the one thing it coordinates — the drawn count (TXF-67) — is kept from assistive technology, which hears the count from the input: the Field gives the text input its name, its description and its required, invalid and disabled states (FLD-8, FLD-10, FLD-11, FLD-14), and the text input carries them on its own element (TXI-23). Assistive technology meets one text box — or, for a date or a time, one group of spin buttons — named by label, exactly as it meets a Field around a text input written by hand.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Every key | The text input's (its accessibility block; TXI-3, TXI-41, TXI-43 to TXI-45, TXI-59). The text field handles no key and adds no focus stop (TXF-64). |
备注
- One level down is the same thing: a Field with a text input in it. Every fixture has that pair as its twin, and the accessibility tree of the two is the same (TXF-62). What the pair by hand does not do by itself is keep the Field's count text equal to the input's count as the person types (TXF-67): the product writes it at each input.
- On the web the two members stand in the text field's own shadow root, so the form meets the text field and not the input (TXF-63); focus passes through to the input (TXF-64).
生成自 specs/text-field.spec.json, tokens/sys/measure.tokens.json
- 文本是在平台自己的文本元素里编辑的,所以输入法、听写、自动填充、选择和剪贴板都和在平台其他地方一样工作。日期或时间是明确的例外:那一排分段是文本字段自己实现的。
- 日期或时间文本字段是一个由标签命名的组。每个分段是一个微调按钮(spin button),由它的单位和标签命名(“月,开始日期”),带有它的范围和值;空的分段读作
segmentEmptyText。 - 必填的文本字段暴露为必填。标记本身对辅助技术隐藏,所以不会读两遍;选填的文字随文本字段的描述一起朗读。
- 调整手柄是一个由
resizeLabel命名的焦点停靠点,暴露为一个分隔条(separator),它的值是行数。方向键、Home 和 End 都能调整大小,所以没有任何操作依赖拖动;它的点击区域满足最小目标。 - 标签就是无障碍名称,无论它是画出来的还是隐藏的。占位符绝不是。
- 显示出来的辅助文本或错误文本,以及计数,是文本字段的描述。前缀和后缀在显示时也是,所以单位或货币会和值一起朗读。
- 它暴露禁用状态,并在平台具备的地方暴露无效、必填和只读状态。日期或时间文本字段里的输入错误,暴露的方式与
error完全相同。 - 选择器按钮是一个由
pickerLabel命名的 Button,暴露为会打开对话框,并且是最后一个分段之后的一个 Tab 停靠点。 - 文本字段获得焦点期间出现或变化的错误,会以礼貌(polite)的方式朗读一次。计数在输入停顿一秒之后按显示的样子(“12/40”)朗读,绝不在每次按键时朗读。
- 在平台具备搜索框角色的地方,搜索文本字段暴露为搜索框。
- 代码字段和其他各种类型一样,是同一种文本框,设置
multiline时为多行:没有任何角色、角色描述或文字说明它保存的是代码,它的值也没有任何部分被标记为拼写错误。Tab 始终会离开它。代码行不换行时,插入点会带着它滚动,而焦点显示在容器上,容器不滚动。 - 焦点由文本字段自己的边线来显示,无论它来自键盘还是指针:stacked 文本字段是方框里面的一条线,filled 或 outlined 文本字段则是变粗并着色的边线。点击区域满足最小目标。
- 按标签或方框会把焦点移到输入框上,但不操作任何东西。按帮助文本、错误文本或计数,什么都不会发生。
- 名称、描述和状态的关联方式,与字段把它们关联到它框住的任何控件时完全一样。在下一层手写的字段,是同一个文本框。
- 缺少标签会在调试构建中报告。
已知的平台差距:
- SwiftUI、Compose、Flutter 和 React Native 保留
required标志,而它不绘制任何标记;它们没有调整大小的模式和调整手柄,也没有分段。 - Web:月或分填入第一位数字时,那一排分段会偏移几个像素;区域设置的最后一个分段是两位数字时,
input可能触发两次;在 Android 的屏幕键盘上,空分段上的 Backspace 无法退回上一个分段;比狭窄的文本字段更宽的一行会溢出,而不是滚动。 - 产品无法通过把
value设为它本来就有的空字符串,来清空只填了一半的日期行。 - Web:在光晕(glow)焦点样式下,filled 文本字段的光晕在底部两角是圆的,而这两个角本身是直角。
- Web:输入法的组合输入和自动填充后的标签只在 Chromium 中测试过。
- Web:代码行不换行的只读代码字段,用键盘滚动的方式与浏览器滚动任何不可编辑的文本相同——在 Firefox 中用插入点,在 Chrome 中用方向键,在 Safari 中用 Option 加方向键。页面的方向改变时,Firefox 会让这样的文本字段回到它的左边缘。
- SwiftUI、Compose、Flutter 和 React Native 还没有实现代码类型:在这些平台上,
type没有code,文本字段也没有wrap。 - SwiftUI 没有无效、必填和只读状态。它们会被画出来或强制执行,但不会被朗读。错误只能通过描述里的文本听到。
- SwiftUI 看不到输入法的组合输入,所以
maxLength也会作用于仍在组合中的文本,你在组合期间设置的值也不会被暂缓。 - SwiftUI:选中的文本以插入点的着色高亮,而不是文本字段的选区颜色。
- SwiftUI 把只读文本字段的值画成可选择的文本,而不是文本字段。
- SwiftUI 从 iOS 18 和 macOS 15 起才读取
.labelsHidden()。在更早的版本上,标签仍然会画出来。 - React Native 看不到输入法的组合输入。你在组合期间更改的值,以及
maxLength限制,都会立即应用,并可能结束这次组合。 - React Native 没有无效、必填和只读状态。错误只能通过描述里的文本听到;必填和只读不会被朗读。
- React Native:朗读、自动填充和各类型的键盘都只用模拟对象测试过,还没有在真机上测试。
- iOS 上的 React Native 对插入点和选区高亮只有一种着色。文本字段保留插入点的颜色,所以高亮是这种颜色的一种淡色,而不是文本字段的选区颜色。
- React Native:filled 文本字段的光晕只贴合它顶部的两个角;占位符和插入点的颜色是立即切换的,而不是按状态动效渐变。
- Flutter 把搜索文本字段暴露为搜索输入类型,而不是搜索框角色。
- 在 Android 和其他触摸屏上,Flutter 不绘制选区手柄和选区工具栏。长按和键盘快捷键仍然可以选择和复制;iOS 显示它的系统菜单。
- Flutter:前置插槽里的可交互元素在焦点顺序中排在输入之前,而不是之后。
- Compose 没有必填状态:文本字段改为把“Required”作为状态描述来朗读。
- Compose:
maxLength和计数器按用户看到的字符计数,但表情符号序列和旗帜没有测试过。测试是在 JVM 上的 Robolectric 下运行的,它的字符规则不认识它们。 - Compose:TalkBack 对错误和计数的朗读、真实的输入法以及真实的自动填充服务,都还没有在真机上测试过。
- 写一个说明这个值是什么的标签,即使把它隐藏起来,也要保留它。
- 编写说明如何改正这个值的错误文本,并自己设置
error。在 SwiftUI 和 React Native 上,这段文本是用户听到错误的唯一途径。对于日期或时间文本字段,还要编写invalidText和unavailableText,并给出segmentEmptyText:没有它,空的分段朗读时不带任何值。 - 用你的产品所用的语言给选择器按钮设置
pickerLabel,并在pickerRequest时打开选择器。 - 设置
required,并给出requiredText和optionalText:写在文本字段上,或者用setWords({ requiredText, optionalText })为整个产品只给一次;resizeLabel和segmentEmptyText也可以这样给。在 Web 上,Tanrei 不自带任何文字;缺少的文字会在开发时报告。参见产品的文字。在 SwiftUI、Compose、Flutter 和 React Native 跟上之前,它们的文本字段不绘制任何标记,SwiftUI 和 React Native 也不朗读必填状态:在这些平台上,请在标签里写明“必填”。 - 用
resizeLabel为调整手柄命名。没有它,调整手柄仍然会绘制,也能使用,但没有名称。 - 把单位或货币放进
prefixText或suffixText,而不是放进值里,这样它会和值一起朗读,而不需要用户输入。 - 对于代码字段,在
label和supportingText里说明代码必须是什么(“Webhook 请求体”“JSON”),并在errorText里说明它错在哪里:文本字段并不认识这种语言。 - 用你的产品所用的语言,给后置的图标按钮一个它自己的名称。
- 在 Compose 上,“Invalid”和“Required”这两个词来自库的字符串资源(
tanrei_invalid、tanrei_required)。在你的资源里覆盖它们,就可以翻译它们。 - 在 Web 上,iOS Safari 会放大文本小于 16 px 的输入框。默认系统的文本字段在两种尺寸下都使用 16 px 的文本;把它改小的层会让这种放大重新出现。