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

表格

呈现有行有列的数据,带具名的列、可选的行标题、说明文字和表尾行。放不下时在自己的框内滚动。列标题可以请求让各行按这一列排序,行也可以被选择;表格自己从不排序。

content_copy以 Markdown 格式查看
类别
容器
令牌
comp.table
层级
基础

概览

Web
tanrei-table
schedule
SwiftUI
TanreiTable
radio_button_unchecked
Jetpack Compose
TanreiTable
radio_button_unchecked
Flutter
TanreiTable
radio_button_unchecked
React Native
Table
radio_button_unchecked

生成自 docs/status/status.json, specs/table.spec.json

行标题、靠末尾对齐的数字、一行表尾

按状态和平台统计的组件数
状态 Web SwiftUI Compose Flutter
计划中 2 6 6 6
Alpha 31 30 30 29
Beta 4 1 1 2
合计 37 37 37 37

表格用于有两个维度的数据:价目表、套餐对比、带类型和默认值的设置项列表、几样东西在几个平台上的状态、用户要处理的记录。列标题可以请求让各行按这一列排序,没有行的表格显示你的空状态,各行下方的状态行容纳你的句子和你的分页。行可以各用一个复选框来选择,也可以用单选按钮只选一行,正在等待行到来的表格会说明自己正忙。表格自己从不排序、筛选、获取数据或分页:给它什么行,它就按给它的顺序画什么行。它不调整列宽。

在 Web 上,它是包在你自己的原生 <table> 外面的 <tanrei-table>。这个元素没有 shadow root,表格也从不会被设成别的 display,所以爬虫、复制粘贴到电子表格,以及屏幕阅读器的表格导航,遇到的都是一个普通的 HTML 表格。其他平台尚未实现。

用法

<link rel="stylesheet" href="@tanrei/web/table.css" />

<tanrei-table row-headers>
  <table>
    <caption>各地区访问量</caption>
    <thead>
      <tr>
        <th scope="col">地区</th>
        <th scope="col" data-align="end" data-wrap="false">1 月</th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">岛屿</th><td data-align="end">212</td></tr>
    </tbody>
    <tfoot>
      <tr><th scope="row">所有地区</th><td data-align="end">6,846</td></tr>
    </tfoot>
  </table>
</tanrei-table>

<script type="module">
  import '@tanrei/web/components/table';
</script>
import { Table } from '@tanrei/react';
import '@tanrei/web/table.css';

<Table density="compact" striped label="表格的令牌">
  <table>
    <thead>
      <tr><th scope="col">令牌</th><th scope="col">类型</th></tr>
    </thead>
    <tbody>
      {tokens.map((token) => (
        <tr key={token.name}><td data-wrap="false">{token.name}</td><td>{token.type}</td></tr>
      ))}
    </tbody>
  </table>
</Table>

@tanrei/web/table.css 在任何脚本运行之前就画出整个表格。元素自己也会把同一份样式表加进页面,所以只加载了脚本的页面同样能得到画好的表格,只是晚一会儿。

你要写的内容

部件 在 Web 上
列 <thead> 唯一那一行里的一个 <th scope="col">。它的文字就是列标题。
列的对齐方式 列标题单元格上的 data-align="start"、"center" 或 "end"。end 用于需要相互比较的数字。
文本不换行的列 列标题单元格上的 data-wrap="false",用于标识符、日期和数字。
行 <tbody> 里的一个 <tr>。表尾行是 <tfoot> 里的一个 <tr>。
单元格 一个 <td>。里面是文本、组件,或者什么都没有。
行标题 作为第一个单元格的 <th scope="row">,同时在元素上设置 row-headers。
名称 一个 <caption>;如果表格上方的标题已经为它命名,则用元素上的 label。
可排序的列 列标题单元格上的 aria-sort="none"、"ascending" 或 "descending",以及包在其文字外面的 <button type="button">。单元格上的 data-value 是 sort 事件带出的名称。
没有行 <tbody> 里的一个 <tr data-part="empty">,其中只有一个 <td colspan>,里面放你的空状态。
状态行 表格之后的一个 <div data-part="status">,里面放一个 <p role="status"> 和你的分页。你没有写这个段落时,元素上的 status 会写出它。
可以选择的各行 元素上的 selection="multiple"(最多只选一行时用 selection="single"),以及每一行里作为第一个单元格的 <td data-part="selection">。
可以选择的一行 <tr> 上的 data-value,以及它的选择单元格里的一个 <tanrei-checkbox label-hidden>,single 时则是一个 <tanrei-radio-button label-hidden>。<tr> 上的 data-selected 表示它已被选中。
针对所有行的复选框 表头行的选择单元格里的一个 <tanrei-checkbox label-hidden>。元素上的 select-all-label 为它命名。single 时这个单元格留空。
加载中的表格 元素上的 loading。
占位行,代表尚未到达的行 一个 <tr data-placeholder aria-hidden="true">,它的单元格留空,或者放一个骨架屏占位符。

元素会把列标题单元格上的对齐和换行设置带给这一列的每个单元格。要让它们在脚本运行之前就绘制出来,请像本页的演示那样,在每个单元格上重复 data-align 和 data-wrap。

排序

按下某个列标题:表格提出请求,由本页为各行排序

商品
备注
电子秤 0 189.00
滤纸 120 29.00 每包 100 张
手冲壶 15 239.00 电磁炉和燃气灶均可使用
手摇磨豆机 8 399.00

可排序的列,是一个说明各行当前顺序的列标题单元格,它的文字外面包着你自己的按钮:

<th scope="col" aria-sort="ascending" data-value="name"><button type="button">名称</button></th>
<th scope="col" aria-sort="none" data-value="price" data-align="end"><button type="button">价格</button></th>

样式表让整个列标题单元格成为这个按钮,并根据 aria-sort 画出标记:各行没有按这一列排序时是双向箭头,按它升序排列时是向上的箭头,按它降序排列时是向下的箭头。你不用写任何图标。请加载 @tanrei/web/icons.css 和图标字体,因为这些标记是图标集中的字形。

表格从不排序。按下按钮会触发 sort,屏幕上什么都不变;由你为各行排序,并写上新的 aria-sort:

table.addEventListener('sort', (event) => {
  const { column, index, direction } = event.detail; // 'price', 2, 'ascending' | 'descending'
  showRows(sortedBy(column, direction));              // 你的数据、你的比较方式,或者你的服务器
  for (const th of headerCells) th.setAttribute('aria-sort', th.dataset.value === column ? direction : 'none');
});

direction 是用户请求的方向:ascending;如果各行已经按这一列升序排列,则是 descending。没有清除排序的第三次按下。你不必给出用户请求的顺序:可以把各行排成另一种顺序,也可以不按任何顺序排列。

对于由服务器排序的表格,请用链接代替按钮:<a href="?sort=price&order=asc">价格</a>,指向另一种顺序的同一个表格。它不需要脚本就能工作。这时 sort 会在浏览器跟随链接之前触发,event.preventDefault() 则让页面留在原处。

没有行

没有行:你的空状态位于表头行下方

符合条件的商品
名称 库存 价格
search_off移除一个筛选条件,或者换一个更短的词搜索。

写一行,其中只有一个横跨所有列的单元格,把你的空状态放在里面:

<tbody>
  <tr data-part="empty">
    <td colspan="3"><tanrei-empty-state headline="没有符合这些筛选条件的商品">…</tanrei-empty-state></td>
  </tr>
</tbody>

表格在它的上下留出空间,给它表格的框的宽度,并在各列横向滚动时让它始终可见。表格不添加任何自己的文字。没有这样一行时,没有行的表格就只有表头。

状态行

你的句子和你的分页,位于各行下方

版本发布(从新到旧)
版本 发布日期 更改
2.4.0 5 月 12 日 表格让表头始终可见。
2.3.2 4 月 28 日 修复了用两位数年份输入日期的问题。
2.3.1 4 月 20 日 修复了菜单在屏幕之外打开的问题。
2.3.0 4 月 2 日 侧边面板,以及更低调的焦点环。
2.2.1 3 月 18 日 修复了工具提示一直不关闭的问题。
2.2.0 3 月 4 日 从右到左的布局。

第 1–6 个,共 42 个版本

61224

在表格之后、元素之内写状态行:把你的句子放在一个作为状态的段落里,再加上你的分页。

<tanrei-table>
  <table>…</table>
  <div data-part="status">
    <p role="status">第 1–20 件,共 132 件</p>
    <tanrei-pagination label="商品的页面" variant="compact" size="small" page="1" count="7" previous-label="上一页" next-label="下一页" count-text="第 {page} 页,共 {count} 页"></tanrei-pagination>
  </div>
</tanrei-table>

句子位于起始边缘,分页位于末尾;两者并排放不下时,分页排到句子下方。状态行不随各列横向滚动;如果表格的各行在自己的框内滚动,它会留在表格底部。表格什么都不数,也不写任何文字:“第 1–20 件,共 132 件”是你的,用你的语言写。你更改段落的文本时,新的文本会以礼貌(polite)的方式朗读给辅助技术的用户。如果你的 HTML 里没有这个段落,元素上的 status 属性会替你写出它。你自己写了段落时,被朗读的就是你写的那一个,这个属性不改变任何东西。

放在末尾的是尺寸为 small 的分页:像这里一样用紧凑形式,或者在用户按页码跳转时用带页码的形式。如果让用户选择每页显示多少行,就在分页之前放一个用于此事的下拉选择框,并把两者放进一个由你布局的元素里;当前显示的行的范围是你的句子,不属于分页。上一页/下一页链接不放在这里:它们通向有标题的页面,而不是同一个列表中的行。表格只负责摆放你给它的内容:页面变化时,由你更换行和句子。

选择行

选择行:表格保存选择,由本页来数

需要补货的商品
名称 库存 价格
滤纸 120 29.00
手摇磨豆机 8 399.00
手冲壶 15 239.00
电子秤 0 189.00

已选择 2 件,共 4 件

在元素上设置 selection="multiple",并让每一行都以一个选择单元格开头。在表头行里,这个单元格放针对所有行的复选框。在表体的行里,它放这一行的复选框,行则用 data-value 带上你用来识别这条记录的名称:

<tanrei-table selection="multiple" row-headers select-all-label="选择所有商品">
  <table>
    <caption>需要补货的商品</caption>
    <thead>
      <tr>
        <td data-part="selection"><tanrei-checkbox label-hidden></tanrei-checkbox></td>
        <th scope="col">名称</th>
        <th scope="col" data-align="end">价格</th>
      </tr>
    </thead>
    <tbody>
      <tr data-value="grinder" data-selected>
        <td data-part="selection"><tanrei-checkbox label-hidden checked></tanrei-checkbox></td>
        <th scope="row">手摇磨豆机</th>
        <td data-align="end">399.00</td>
      </tr>
    </tbody>
  </table>
  <div data-part="status"><p role="status">已选择 1 件,共 4 件</p></div>
</tanrei-table>

选择由表格保存。用户更改某个复选框时,元素会在那一行上写 data-selected,让表头行的复选框与各行保持一致,并触发一次 selectionchange,其中带有所有选中行的值,按行的顺序排列:

table.addEventListener('selectionchange', (event) => {
  if (!(event instanceof CustomEvent)) return;       // 见下面的说明
  const { values } = event.detail;                   // ['grinder', 'scale']
  // 你的句子,用你的语言,写进你自己写的那个段落
  table.querySelector('[role="status"]').textContent = `已选择 ${values.length} 件,共 ${total} 件`;
});
  • 要自己选中某一行,就在它的 <tr> 上写 data-selected;去掉这个属性,这一行就不再被选中。元素会选中或取消选中这一行的复选框,不触发任何事件。
  • 一行是否被选中,写在这一行自己身上(data-selected)。你换一种顺序重新给出的行,原来选中的仍然选中,没有选中的仍然没有选中;第一次交给表格的行,只有它自己这样标明时才是选中的。
  • 没有 data-value 的行不能被选择。请像表尾行那样,把它的选择单元格留空。
  • 表头行的复选框,在所有行都被选中时是选中的,只有一部分行被选中时是半选的,没有可以选择的行时是禁用的。它代表的是交给表格的那些行,绝不代表其他页的行。
  • 选择列不是数据列。row-headers 和 sticky-first-column 指的是它后面的那一列,sort 事件的 index 也不把它计算在内。
  • 在可以选择的行上任何位置按下,都会像按下复选框一样改变它的复选框。单元格里的链接、按钮和输入框保留各自的按下行为;拖动、选择文本、双击中的第二次点击以及按住修饰键的点击都不会改变任何东西。整行作为一个整体绘制悬停和按下状态;键盘操作仍然在复选框上。

浏览器自己也有一个名为 selectionchange 的事件,用于文本选区的变化;单元格里的文本字段发出的这个事件会经过表格。表格的事件是带有 detail.values 的 CustomEvent,浏览器的则不是。请像示例那样检查这一点。

对于没有脚本也必须能用的页面,请使用原生复选框。复选框为 checked 的行,仅凭样式表就会画成选中的样子,这些复选框也会随你的表单一起提交:

<tr data-value="grinder">
  <td data-part="selection"><input type="checkbox" name="product" value="grinder" checked aria-label="选择手摇磨豆机" /></td>
  <th scope="row">手摇磨豆机</th>
</tr>

选择一行

选择一行:每行一个单选按钮,表头行没有

要换成的套餐
套餐 席位 每月
入门版 3 0.00
团队版 20 48.00
商务版 100 180.00

用户要选择一条记录时(要换成的套餐、收货地址、作为对比基准的构建),设置 selection="single"。每个表体行的选择单元格放一个单选按钮来代替复选框,表头行的选择单元格留空,因为没有针对所有行的控件:

<tanrei-table selection="single" row-headers>
  <table>
    <caption>要换成的套餐</caption>
    <thead>
      <tr><td data-part="selection"></td><th scope="col">套餐</th><th scope="col" data-align="end">席位</th></tr>
    </thead>
    <tbody>
      <tr data-value="team" data-selected>
        <td data-part="selection"><tanrei-radio-button label-hidden name="plan" checked></tanrei-radio-button></td>
        <th scope="row">团队版</th>
        <td data-align="end">20</td>
      </tr>
    </tbody>
  </table>
</tanrei-table>
  • 选择一行,原先选中的行就不再选中。selectionchange 只携带一个值:{ values: ['team'] }。
  • 按下已选中的行或它的单选按钮,什么也不会改变。单选按钮再按一次也不会取消选中。如果要让用户回到一行都不选的状态,请自己提供一个控件,并从行上移除 data-selected。
  • 同一个表格的单选按钮共用一个名称,因此是一个组:在焦点顺序中只占一个停靠点,方向键把选择从一行移到另一行。请给它们起你自己的名称,表单会提交这个名称;没有名称的,元素会给它们一个。
  • 如果你给多行写上 data-selected,最后写上的那一行被选中;同时写上的几行中,第一行被选中。同时写上多行会在调试构建中报告。
  • 其余与复选框相同:按下行的任何位置都会选中它,data-selected 由你来写,原生的 <input type="radio" name="…"> 在没有脚本时也能工作。

加载中

加载中:占位行,以及说明这一点的状态行

本周订单
客户 下单日期 总额

正在加载

获取行的期间设置 loading。表格会以忙碌状态暴露给辅助技术,并照常画出这期间你给它的内容。它什么都不获取,也什么都不等待:你说它在加载多久,它就加载多久。

<tanrei-table loading>
  <table>
    <thead>…</thead>
    <tbody>
      <tr data-placeholder aria-hidden="true">
        <td><span class="tanrei-skeleton-placeholder"></span></td>
        <td data-align="end"><span class="tanrei-skeleton-placeholder" style="inline-size: 3rem; margin-inline-start: auto"></span></td>
      </tr>
    </tbody>
  </table>
  <div data-part="status"><p role="status">正在加载</p></div>
</tanrei-table>
  • 首次加载时,给出占位行:<tr data-placeholder aria-hidden="true">。占位行至少有一行文本那么高,没有复选框,并且对辅助技术隐藏。
  • 占位行的单元格可以留空,也可以各放一个骨架屏占位符(class="tanrei-skeleton-placeholder",来自 @tanrei/web/skeleton.css)。占位符与单元格里的一行文本一样高,所以占位行与替换它的行一样高。在这里只用这个类就够了:表格已经说明自己正忙。
  • 换页或换一种顺序时,保留表格原有的行,并设置 loading。用户正在读的行仍然可读。
  • 什么都不变淡,什么都不禁用。表格加载期间,列标题和复选框照常工作。
  • 设置了 loading 期间,即使没有别的行,也不画空状态所在的那一行。

Markdown

Markdown 渲染器输出的是一个裸的 <table>。请在构建时把它包起来;Tanrei 不会在运行时移动你的内容。用 rehype 只需一小步:

import { visit, SKIP } from 'unist-util-visit';

/** 把文档里的每个表格包进 <tanrei-table>。 */
export function rehypeTanreiTable() {
  return (tree) =>
    visit(tree, 'element', (node, index, parent) => {
      if (node.tagName !== 'table' || parent?.tagName === 'tanrei-table') return;
      parent.children[index] = { type: 'element', tagName: 'tanrei-table', properties: {}, children: [node] };
      return SKIP;
    });
}

渲染器自己在单元格上写的 align="right" 会被读作 end,align="left" 读作 start,所以在从右到左的页面上,“数字靠右”就变成了“数字靠末尾”。

规格

紧凑并带条纹,用于供人扫读的表格

令牌 引用 类型
comp.table.separation sys.behavior.separation number
comp.table.divider.color sys.color.outline-variant color
comp.table.divider.thickness sys.border.hairline dimension
comp.table.row.gap sys.space.space50 dimension
comp.table.stripe.container.color sys.color.surface-container-low color
comp.table.caption.gap sys.space.space100 dimension

比它的框更宽:在框内滚动,第一列保持不动

各地区访问量
地区 1 月 2 月 3 月 4 月 5 月 6 月 7 月
北部地区 1,204 1,310 1,288 1,402 1,396 1,450 1,512
中部地区 3,020 2,985 3,110 3,204 3,190 3,275 3,301
南部地区 870 902 915 899 940 968 1,002
岛屿 212 220 198 240 251 263 270

高度由布局给定:各行滚动,表头保持不动

版本发布
版本 发布日期 更改
2.4.0 5 月 12 日 表格让表头始终可见。
2.3.2 4 月 28 日 修复了用两位数年份输入日期的问题。
2.3.1 4 月 20 日 修复了菜单在屏幕之外打开的问题。
2.3.0 4 月 2 日 侧边面板,以及更低调的焦点环。
2.2.1 3 月 18 日 修复了工具提示一直不关闭的问题。
2.2.0 3 月 4 日 从右到左的布局。
2.1.0 2 月 11 日 日期时间选择器。

构成

部件 说明
1 container The table's box: as wide as its layout gives it, as tall as its rows unless the layout limits it. It has no fill, edge or corner of its own. When the columns or the rows do not fit, it is the box they scroll in, and then the one part of the table that takes focus.
2 caption · 可选 The name of the table, drawn above its first row at the start edge. It does not scroll sideways with the columns.
3 header-row · 可选 The first row: one header cell for each column. It can stay in view while the rows scroll under it.
4 header-cell · 可选 The name of a column, in a label style and a quieter color than the data.
5 sort-button · 可选 The whole header cell of a column that can be sorted, as one button: the column's name and its mark. It carries hover, focus and press, and asks the product for the rows in the column's order.
6 sort-mark · 可选 The glyph beside the name of a column that can be sorted: a two-way arrow while the rows are not in its order, an arrow up while they ascend by it, an arrow down while they descend.
7 row One record: a cell for each column, as tall as its tallest cell. A row that can be chosen is, for a pointer, one target that presses its checkbox or radio button, with its own hover and press (TBL-58, TBL-59).
8 row-header · 可选 The first cell of a row where the first column names its rows: the cell style at its emphasized weight. The first column can stay in view while the others scroll under it.
9 selection-cell · 可选 The cell before the first column of a table whose rows can be chosen: in a body row the row's checkbox, or its radio button where one row at most is chosen; in the header row the checkbox for every row, or nothing where one row at most is chosen; in a footer row nothing.
10 cell One value: text that wraps inside its column, or a component of the product's (a tag, a link, inline code). It may be empty.
11 divider · 可选 The one hairline between two rows, under the header row and above the footer rows, where rows are told apart by a line; also the line along the inner edge of a first column that stays in view, and along an edge of the box beyond which there is more to scroll to.
12 stripe · 可选 A quiet fill on every second body row.
13 selected-row · 可选 The fill of a body row that is chosen, across its whole width, in place of a stripe.
14 placeholder-row · 可选 A body row that stands for a record that has not arrived: as tall as a row of one line, holding what the product puts in its cells (a Skeleton), and no checkbox or radio button.
15 empty · 可选 What stands in the place of the body rows of a table that has none: the product's own content (an Empty state), as wide as the table's box, under the header row.
16 footer-row · 可选 A row after every body row that sums the columns up or notes something about them: the cell style at its emphasized weight.
17 status · 可选 One line under the last row, outside the columns: the product's sentence about what the table shows now (the range of rows shown, how many are chosen) at the start edge, and its pagination at the end edge. It does not scroll sideways with the columns.

属性

属性 类型 默认值 说明
caption string "" The name of the table, drawn above it ("Response times by region"). It is also what assistive technology calls the table and its scroll region. Prefer it to label wherever the page does not already name the table in sight.
label string "" The table's accessible name where no caption is drawn, because a heading just above it already says what it is. Not drawn. Ignored where caption is given.
density regular | compact "regular" How close the rows are. Regular (a 46 row in the default system) is for a table a person reads: a few rows, cells that wrap. Compact (a 32 row, smaller text) is for a table a person scans: hundreds of rows, identifiers and values. A design system may add values.
separation auto | divider | gap "auto" How a row is told from the row next to it. divider: one hairline between them. gap: empty space and no line. Auto takes comp.table.separation, the design system's answer (divider in the default system).
striped boolean false Fills every second body row with a quiet color, for a wide table of many similar rows where the eye loses its row on the way across. It can be combined with either separation.
rowHeaders boolean false Says that the first column names its rows (a component, a region, a plan), so its cells are drawn and exposed as row headers. False for a table whose rows are named by no single column.
stickyHeader boolean false Keeps the header row at the top of the table's box while its rows scroll under it. It has an effect only where the layout limits the table's height, so that the rows scroll in the table's own box.
stickyFirstColumn boolean false Keeps the first column at the start edge while the other columns scroll sideways under it, for a wide table whose first column says what each row is. Released by itself where the column would take more than half of the box.
selection none | single | multiple "none" Whether rows can be chosen, and how many. multiple puts a checkbox before the first column of every body row that has a value, and one in the header row that stands for all of them. single puts a radio button there instead and nothing in the header row: one row at most is chosen, and a person moves the choice from row to row but does not take it back (TBL-60). none draws no such column and ignores a row's selected.
selectAllLabel string "" The accessible name of the checkbox in the header row that chooses every row ("Select all rows"). Not drawn; read only while selection is multiple. From the product's words where the instance gives none.
sortedAscendingLabel string "" What a platform that has no sort state of its own says after the name of the column the rows ascend by ("sorted ascending"). Not drawn. A platform that exposes the direction itself (the web) does not read it.
sortedDescendingLabel string "" What a platform that has no sort state of its own says after the name of the column the rows descend by ("sorted descending"). Not drawn. A platform that exposes the direction itself (the web) does not read it.
loading boolean false Says that the product is fetching the rows — the first ones, another page of them, or the same ones in another order. The table is exposed as busy and draws the rows it is given meanwhile: the ones it had, or placeholder rows. It fetches nothing itself.
status string "" The product's sentence about what the table shows now ("1–20 of 132", "3 of 132 selected", "Loading"), drawn under the last row and told politely to assistive technology when it changes. The table counts nothing and writes no word of its own. Empty draws no status text.

插槽

插槽 说明
empty What the table draws in the place of its body rows while it has none and is not loading: the product's Empty state, or its own sentence. The table gives it the box's width and room above and below, and does nothing else to it. A fixture's string is drawn as plain text in its place.
pagination What stands at the end edge of the status line: one element, the product's Pagination — at size small, and compact where the product does not page by number — or a group the product lays out of a Select for the rows per page and that Pagination, as the table paginations of Carbon and MUI are. The range of rows shown ("1–20 of 132") is the status text, not the slot's. It is never Previous and next links, which lead to titled pages and not to the rows of one list. The table places it and does nothing else to it. A fixture's string is drawn as plain text in its place.

事件

事件 说明
sort The user pressed the header of a sortable column; carries column (the column's value), index (its place among the columns of data, from 0) and direction (ascending or descending: the one asked for, TBL-34). The table changes nothing: the product orders its rows and sets the column's sort.
selectionChange The user changed which rows are chosen — by a row's checkbox or radio button, by a press on the row, by the checkbox for every row, or by an arrow key on a radio button; carries values: the values of the chosen rows after the change, in row order. While selection is single it holds the one chosen row's value.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

column

One column of the table: the text of its header cell, how its cells are aligned and wrapped, and whether its header asks for the rows in its order.

属性 类型 默认值 说明
header string "" The text of the column's header cell ("Price"). Empty only where the table has no header row at all, and then for every column.
align start | center | end "start" Where the content of the column's cells sits, header and footer cells included. start for text. end for numbers that are compared, so that their digits line up. center for a mark or a single word that is the same kind of answer in every row.
wrap boolean true Whether the text of the column's cells may wrap. False keeps every cell on one line, for identifiers, dates, numbers and short values; the column is then as wide as its widest cell.
sortable boolean false Says that the rows can be put in this column's order: its header cell is a button that asks for it. The table never orders rows itself. It needs a header: a sortable column without one is drawn as a plain column.
sort none | ascending | descending "none" The order the rows are in by this column, as the product has put them: the header draws it and exposes it. The product sets it after it has ordered its rows; one column at a time is not none.
value string "" The name the product knows the column by ("price"), carried by the sort event. Not drawn. Empty: the event carries the column's index alone.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 th TanreiTableColumn TanreiTableColumn TanreiTableColumn TableColumn
header · 属性 scope header header header header
align · 属性 data-align alignment align align align
wrap · 属性 data-wrap wraps wrap wrap wrap
sortable · 属性 button isSortable sortable sortable sortable
sort · 属性 aria-sort sort sort sort sort
value · 属性 data-value value value value value
备注 A <th scope="col"> in the <thead> row; its text is the header. data-align="end" and data-wrap="false" on that <th> reach every cell of the column: the element writes the positions of such columns on the wrapper (data-columns-end, data-columns-center, data-columns-nowrap) and one rule per position places the cells, so no cell is written to; without script, and from a Markdown renderer, the same attributes (or the renderer's own align attribute, where left and right are read as start and end) sit on each cell. A sortable column: the <th> holds the product's own <button type="button"> (or, for a table ordered by the server, an <a href> to the same table in the other order) around the header's text, and carries aria-sort="none", "ascending" or "descending"; the stylesheet draws the mark from that attribute, so the product writes no glyph. data-value on the <th> is the column's value. An entry of the columns prop.

row · 分组 — 包含条目

One row of the table: a cell for each column, in the order of the columns; a record that can be chosen where the table's selection says so.

属性 类型 默认值 说明
footer boolean false Says that the row is a footer row: drawn after every body row, in the emphasized weight, never striped. For sums, averages and notes about the columns.
value string "" The name the product knows the record by, carried by selectionChange. Not drawn. A body row without a value cannot be chosen: it has no checkbox or radio button.
selected boolean false Whether the row is chosen. Read only while the table's selection is single or multiple and the row has a value; while it is single, one row at most is chosen (TBL-60).
selectLabel string "" The accessible name of the row's checkbox or radio button ("Select Islands"): words about this one record, so the instance's. Not drawn. Empty: it is named by the row's header cell where rowHeaders is true.
placeholder boolean false Says that the row stands for a record that has not arrived. It is drawn as a body row at least one line tall, holds what the product puts in its cells (a Skeleton) or nothing, has no checkbox or radio button and is hidden from assistive technology.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tr TanreiTableRow row TanreiTableRow Table.Row
footer · 属性 tfoot isFooter footer footer footer
value · 属性 data-value value value value value
selected · 属性 data-selected isSelected selected selected selected
selectLabel · 属性 aria-label selectLabel selectLabel selectLabel selectLabel
placeholder · 属性 data-placeholder isPlaceholder placeholder placeholder placeholder
备注 A <tr>; a footer row is a <tr> inside <tfoot>. A row that can be chosen has data-value and a first cell <td data-part="selection"> holding the product's checkbox — <tanrei-checkbox label-hidden label="…">, or a native <input type="checkbox" aria-label="…"> that also works in a form without script; selectLabel is that checkbox's own name. data-selected on the <tr> draws the row as chosen; the element keeps it in step with the checkbox, and the stylesheet also reads a native checkbox that is checked, so a row is drawn chosen before and without script. While selection="single" the selection cell holds a radio button instead — <tanrei-radio-button label-hidden name="…" label="…">, or a native <input type="radio" name="…" aria-label="…"> — the radio buttons of one table share one name (the element gives one to those that have none, so that they form one group), and the header row's selection cell is empty. A placeholder row is <tr data-placeholder aria-hidden="true">. A call in the table's scope: row(footer = false) { cell("…") }.

cell · 分组 — 包含条目

One value of a row: plain text, a component of the product's, or nothing.

插槽 说明
content What the cell holds in place of plain text: a tag, a link, inline code, a button, a picture, any view of the product's. The table places it and styles nothing inside it (TBL-15).
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 td TanreiTableCell cell TanreiTableCell Table.Cell
备注 A <td>, or a <th scope="row"> for a row header; its children are the content. A cell takes a string or a view builder. cell(text) or cell { content } in a row's scope. TanreiTableCell.text("…") or TanreiTableCell(child: …). Its children are a string or elements.

tag

A tag placed in a cell: the Tag component, as it is drawn anywhere else.

属性 类型 默认值 说明
label string "" The tag's text.
variant tonal | outlined | filled "tonal" The tag's look.
status default | info | warning | error | success "default" What the tag reports.
size small | medium "medium" The tag's size; small in a table cell, so that it does not make its row taller.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-tag TanreiTag TanreiTag TanreiTag Tag
label · 属性 label title label label label
variant · 属性 variant variant variant variant variant
status · 属性 status status status status status
size · 属性 size size size size size

状态

行为

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

ID 需求
TBL-1 THE table SHALL draw, from top to bottom, its caption where it has one, one header row holding one header cell for each column, its body rows in the order given, and its footer rows after every body row, each row holding one cell for each column in the order of the columns; THE table SHALL be at least as wide as the layout gives it, SHALL draw no fill, no edge, no corner and no elevation of its own around its rows, and SHALL bring no outer space of its own, so that it is spaced by what holds it.
TBL-2 THE table SHALL size every column from its content: a column SHALL never be narrower than the widest thing in it that cannot break — a word, a number, a cell of a column whose wrap is false, a component — with the cell's inline paddings; WHILE the columns at those least widths fit the table's box THE table SHALL share the box's width among them in proportion to their content, so that the table fills its box and a column of long text takes what the short columns leave; THE table SHALL NOT give a column a width in a unit of its own, and a width the product gives a column SHALL replace what the content would give it.
TBL-3 THE table SHALL wrap the text of a cell inside its column, breaking lines by the rules of the text's language, and SHALL never cut it or end it with an ellipsis; a run with no break opportunity (an identifier, an address) SHALL NOT be broken inside and widens its column instead (TBL-2, TBL-17); WHERE a column's wrap is false THE table SHALL keep every cell of that column, its header cell included, on one line.
TBL-4 THE table SHALL pad every cell at the top and bottom by comp.table.cell.padding-block.regular and at the start and end by comp.table.cell.padding-inline.regular while density is regular, and by comp.table.cell.padding-block.compact and comp.table.cell.padding-inline.compact while density is compact, the first and the last cell of a row included; SHALL set a cell's text in comp.table.cell.text.typescale.regular or comp.table.cell.text.typescale.compact by density, in comp.table.cell.text.color, with digits in figures of equal width so that the numbers of a column line up; SHALL make a row as tall as its tallest cell and no taller, with no height of its own; and SHALL align the cells of a row to the top, so that the first lines of a row share a baseline.
TBL-5 THE table SHALL set a column's header cell in comp.table.header.text.typescale.regular or comp.table.header.text.typescale.compact by density, in comp.table.header.text.color, with the paddings of a cell (TBL-4), and SHALL fill the header row with comp.table.header.container.color; the header cells of a row SHALL align to the bottom, so that the last line of each sits next to the data it names. IF every column's header is empty THE table SHALL draw no header row and no line for it, and the table then relies on its row headers (TBL-6).
TBL-6 WHILE rowHeaders is true THE table SHALL treat the first cell of every body row and of every footer row as the header of its row: it SHALL draw that cell in the cell's text style at its emphasized weight — the style's weight plus sys.typescale.emphasis.body — in comp.table.cell.text.color, with a cell's paddings and no fill of its own, and SHALL expose it as a row header (TBL-24); WHILE rowHeaders is false THE table SHALL draw and expose the first column as cells, except that a first cell the product's own content already marks as the header of its row (on the web a th whose scope is row) SHALL be drawn and exposed as one whatever rowHeaders says: rowHeaders adds row headers and takes none away. WHILE selection is multiple or single the first column is the first column of data, after the selection column (TBL-42).
TBL-7 THE table SHALL align the content of every cell of a column, its header cell and its footer cells included, as the column's align names — start: at the start edge of the cell; end: at the end edge, for numbers that are compared digit by digit; center: in the middle, for a mark or a single word that is the same kind of answer in every row — and SHALL align every line of a wrapped cell the same way; a value of align the table does not define SHALL be treated as start.
TBL-8 WHILE the table's separation is divider (TBL-9) THE table SHALL draw one line of comp.table.divider.thickness in comp.table.divider.color across its whole width under the header row, between every two rows that follow each other, and between the last body row and the first footer row, without adding to a row's height or moving its content; it SHALL draw no line above the first row or below the last, and no line between columns.
TBL-9 THE table SHALL tell a row from the row next to it as its separation names: WHILE separation is auto, as comp.table.separation names, which follows sys.behavior.separation unless a design system sets it apart; WHILE separation is divider or gap, as that value names, whatever the tokens say; divider draws the lines of TBL-8 and gap leaves the space of TBL-10. WHEN separation, or the token it follows, changes THE table SHALL draw the other form at once, without animation, keeping its scroll position; WHEN separation names a value the table does not define THE table SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, divider, and state that in its README.
TBL-10 WHILE the table's separation is gap (TBL-9) THE table SHALL leave comp.table.row.gap of empty space under the header row, between every two rows that follow each other, and between the last body row and the first footer row, SHALL draw none of the lines of TBL-8, and SHALL leave no such space above the first row or below the last; the rows SHALL stay exposed as rows (TBL-24).
TBL-11 WHILE striped is true THE table SHALL fill every second body row — the second, the fourth and so on, counted among the body rows as they are shown — with comp.table.stripe.container.color across the row's whole width, paddings included, and SHALL fill neither the header row nor a footer row with it; the fill SHALL change nothing else about the row, and the row separation of TBL-9 SHALL be drawn as without it.
TBL-12 WHERE caption is not empty THE table SHALL draw it above the header row, at the start edge of the table's box, in comp.table.caption.text.typescale and comp.table.caption.text.color, wrapping inside the box's width, comp.table.caption.gap above the first row; the caption SHALL stay where it is while the columns scroll sideways (TBL-17) and SHALL be the table's accessible name (TBL-23).
TBL-13 WHERE a row's footer is true THE table SHALL draw it after every body row, in the order given, as a footer row: its cells in the cell's text style at its emphasized weight — the style's weight plus sys.typescale.emphasis.body — in comp.table.cell.text.color, aligned as their columns are, never striped, and exposed as rows of the table that follow its body; IF a row whose footer is false is given after a footer row THE table SHALL still draw every body row before every footer row and in debug builds report the order.
TBL-14 WHERE a cell has no content THE table SHALL draw nothing in it, keep its place and its paddings so that the cells after it stay in their columns, and expose it as an empty cell; THE table SHALL NOT put a dash, a word or any other mark of its own in it. IF a row holds fewer cells than the table has columns THE table SHALL keep the row's place and its height and leave the places of the missing cells empty at the row's end: a platform that builds its rows SHALL draw them as empty cells, and a platform that draws the product's own rows (the web) SHALL write no cell into them, so that there the row's line and a stripe end at its last cell, and in debug builds SHALL report the row; IF it holds more THE table SHALL draw only as many as it has columns and in debug builds report the row.
TBL-15 WHERE a cell holds a component or any content of the product's that is not plain text — a tag, a link, inline code, a button, a picture — THE table SHALL place it by the column's alignment (TBL-7) on the cell's first line, SHALL add nothing to it and style nothing inside it, and SHALL let pointer, touch and keyboard input reach it as it would outside the table, in the reading order of the cells.
TBL-16 THE table and its cells SHALL NOT take pointer, touch or keyboard input of their own and SHALL NOT change anything they draw under a pointer, a press or focus, and a row SHALL NOT either except as TBL-58 and TBL-59 say: no row has a focused state, is a stop in the focus order or takes a key; a row that cannot be chosen — every row of a table whose selection is none, the header row, a footer row, a placeholder row and a body row without a value — has no hovered or pressed state and is not pressed as a whole; and a cell is not a stop in the focus order. Four parts of the table take input, each by its own sentences: the header cell of a sortable column, which is a button (TBL-32 to TBL-41); the checkboxes of a table whose selection is multiple, which are the Checkbox component, and the radio buttons of a table whose selection is single, which are the Radio button component (TBL-42 to TBL-47, TBL-60, TBL-61); a body row that can be chosen, which a pointer presses as its checkbox or radio button (TBL-58, TBL-59); and the scroll region while the table scrolls (TBL-22). WHILE no column is sortable and selection is none THE table SHALL emit no event.
TBL-17 WHEN the columns at their least widths (TBL-2) are wider than the table's box THE table SHALL keep its box at the width the layout gives it and scroll its columns sideways inside that box, header row, body rows and footer rows together, beginning at the start edge; it SHALL never be wider than its layout and SHALL never make what holds it scroll sideways; a wheel, a trackpad, a drag on a touch screen and the keys of TBL-22 SHALL scroll it.
TBL-18 WHERE the layout gives the table less height than its rows need THE table SHALL scroll its rows up and down inside its own box, the caption above them as the first thing in the box, where it MAY scroll away with the rows on a platform whose caption is part of what scrolls (the web's native caption), and the status line staying at the bottom of the box (TBL-51); WHERE the layout sets no such limit THE table SHALL be exactly as tall as its caption and its rows, and SHALL set no height and no number of rows of its own.
TBL-19 WHILE stickyHeader is true and the table scrolls its rows in its own box (TBL-18) THE table SHALL keep the header row at the top of that box while the rows pass under it, filled with comp.table.sticky.container.color under comp.table.header.container.color so that nothing shows through it, and SHALL draw a line of comp.table.divider.thickness in comp.table.divider.color under it whatever the separation is while a row is under it; WHILE stickyHeader is false, or the table does not scroll its rows in its own box, THE header row SHALL scroll with the rows.
TBL-20 WHILE stickyFirstColumn is true and the table scrolls sideways (TBL-17) THE table SHALL keep the first column — its header cell, its cells and its footer cells — at the start edge of the box while the other columns pass under it, each of its cells filled with comp.table.sticky.container.color under the fill its row gives it (comp.table.header.container.color in the header row, comp.table.stripe.container.color in a striped row), and SHALL draw a line of comp.table.divider.thickness in comp.table.divider.color along its inner edge for the table's whole height while a column is under it; WHILE both stickyHeader and stickyFirstColumn hold THE first header cell SHALL stay above both. IF the first column is wider than half of the table's box THE table SHALL let it scroll with the other columns, so that a narrow window is never filled by the column that stays.
TBL-21 WHILE the table scrolls sideways and columns lie beyond an inline edge of its box THE table SHALL draw a line of comp.table.divider.thickness in comp.table.divider.color along that edge of the box for the table's whole height, and SHALL remove it when the scroll reaches that edge; at the start edge of a table whose first column is kept in view the line of TBL-20 is that line. THE table SHALL show that there is more with these lines alone: no shadow, no fade and no color that is not the divider's.
TBL-22 WHILE the table scrolls in its own box, sideways or up and down, THE table SHALL make that box one stop in the focus order, before anything in its cells that takes focus, exposed as a region that carries the table's name (TBL-23); WHILE it has keyboard focus the box SHALL draw the system's focus indicator inside its edge — sys.focus.outer-width wide in the color role focus-ring, set in from the edge by sys.focus.offset so that nothing clips it, and never animated — and the arrow keys, Page Up, Page Down, Home and End SHALL scroll it as the platform scrolls a region, the left and right arrow keys moving the view left and right in either layout direction; WHILE the table does not scroll THE table SHALL NOT be a stop in the focus order and SHALL expose no region.
TBL-23 THE table SHALL take its accessible name from caption where caption is not empty, and from label otherwise, and SHALL NOT draw label; IF both are empty THE table SHALL be exposed without a name and in debug builds report that a table has no name, and WHILE such a table scrolls in its own box that box SHALL still be a stop in the focus order (TBL-22), exposed as a region without a name, because the keyboard must be able to scroll it; IF both are given THE caption SHALL be the name and label SHALL be ignored.
TBL-24 THE table SHALL be exposed to assistive technology as a table with its number of rows and columns, its header cells as column headers, its first cells as row headers while rowHeaders is true, and every cell associated with the header of its column and, where there is one, of its row, so that a person moving cell by cell hears what each value belongs to; a platform that has no table semantics SHALL expose each row as one group in reading order and each cell with its column header before its own content. Keeping a header row or a first column in view, striping, the density and scrolling SHALL change nothing that is exposed.
TBL-25 WHEN the table's rows, columns or cells change THE table SHALL draw the new content at once, without a motion, keeping its scroll position where the content it showed still exists, and SHALL NOT announce the change by itself. The one place where the table tells a change is its status line, and what that says is the product's (TBL-52); WHEN rows are replaced because the product ordered or fetched them THE table SHALL keep keyboard focus on the sort button, the checkbox or the radio button that had it, where that control still exists.
TBL-26 THE table 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 table is shown THE table SHALL draw itself for the new direction at once, without a motion of its own, keeping its content and the column a person was looking at in view.
TBL-27 WHERE the layout direction is right-to-left THE table SHALL lay its columns out from the right, the first column at the right edge; SHALL apply paddings and the alignments start and end by the layout direction; SHALL begin its sideways scroll at the right edge; SHALL keep a first column that stays in view (TBL-20) at the right edge with its line on its left; and SHALL draw the lines of TBL-21 at the edges beyond which columns lie. Top and bottom SHALL NOT change: the header row is the first row and the footer rows the last in either direction.
TBL-28 THE table SHALL lay out the text of each cell, of each header cell and of the caption as text of its own direction, isolated from the cells beside it: it takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right name, an identifier, a number with its sign, unit or currency, a version and a date keep their own order in a right-to-left layout; THE table SHALL place that text by the column's alignment in the layout direction, not by the text's own, and SHALL NOT reverse the digits of a number: a column of numbers whose align is end is at the left edge of its cells in a right-to-left layout, with its digits written left to right.
TBL-29 WHILE the text size is enlarged THE table SHALL grow every text style with it, give up none of its paddings and none of its lines (they MAY grow with the text where the design system's units follow the text size), size its columns again from the larger content (TBL-2), wrap what can wrap and scroll in its own box where the columns no longer fit (TBL-17); it SHALL cut no content, SHALL set no fixed height on a row or a cell, SHALL lose no content when the reader raises line height, paragraph spacing, letter spacing or word spacing, and SHALL need no sideways scrolling of what holds it at a width of 320 points.
TBL-30 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 table SHALL draw every line of TBL-8, TBL-19, TBL-20 and TBL-21 in the platform's text color and its text in that color, SHALL keep header cells, row headers and footer cells told apart by their text styles, and, because the mode removes fills, SHALL draw the lines of TBL-8 in a striped table whatever its separation is, so that its rows are still told apart.
TBL-31 WHEN density names a value that the active design system does not have THE table SHALL render with the default value's tokens, and in debug builds report the unknown value.
TBL-32 WHERE a column's sortable is true and its header is not empty THE table SHALL draw that column's header cell as one button, the sort button: its container is the whole box of the header cell, paddings included, with square corners; it holds the column's header as its label, in the header's text style (TBL-5), and the sort mark — one glyph of the system's icon set in a box of comp.table.header.icon.size, drawn through the icon helper with sys.icon.family.unselected, sys.icon.fill.unselected, the weight comp.table.header.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max — comp.table.header.gap after the label's box, at the height of the label's last line, so that a header that wraps keeps its mark at the edge of its label rather than after the last word; in a column whose align is end the mark SHALL stand before the label, so that the end of the label stays on the edge its numbers end at. The mark's box SHALL be kept whatever the column's sort is, so that a change of order moves no label and changes no column's width. IF sortable is true and header is empty THE table SHALL draw the column as one that is not sortable and in debug builds report it.
TBL-33 THE table SHALL draw the order the product sets and never make one: WHILE a sortable column's sort is none its sort button is the part sort — the label in comp.table.sort.label.color, the glyph swap_vert in comp.table.sort.icon.color, the container in comp.table.sort.container.color; WHILE its sort is ascending or descending it is the part sorted — the label in comp.table.sorted.label.color, the glyph arrow_upward for ascending and arrow_downward for descending in comp.table.sorted.icon.color, the container in comp.table.sorted.container.color. THE table SHALL NOT reorder, compare, filter or hide a row, SHALL NOT change a column's sort itself, and SHALL draw its rows in the order it is given whatever any sort says. WHEN a column's sort changes THE table SHALL draw the other glyph and colors at once, with no motion but that of a state change (TBL-35). IF more than one column's sort is not none THE table SHALL draw each as it is given and in debug builds report it: the rows have one order. A sort on a column whose sortable is false SHALL be ignored.
TBL-34 WHEN the user activates a sort button by pointer, touch, Enter or Space THE table SHALL emit sort exactly once, carrying the column's value, its index among the columns of data and the direction asked for — ascending WHILE the column's sort is none or descending, descending WHILE it is ascending — and SHALL change nothing it draws but the answer to the press: the mark, the colors and the rows stay as they are until the product sets sort and gives the rows in their new order. The direction is what the person asked; the product MAY set another, or none. THE table SHALL keep keyboard focus on the sort button through the press and through the change of rows that follows it. WHERE the product gave the control a destination in place of an action (on the web an anchor in the header cell, whose address is the same table in the other order) THE table SHALL emit sort once and then let the platform follow the destination unless the product cancelled the event, and Space SHALL NOT activate it, as it activates no link.
TBL-35 WHILE a sort button is hovered, keyboard-focused or pressed THE table SHALL render its container, its label, its mark and its opacity from the component tokens of its part (sort or sorted) in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. A change between states SHALL take comp.table.motion.state-duration with comp.table.motion.state-easing and SHALL be immediate where motion is reduced. The state layer SHALL cover the header cell and nothing beside it, and WHILE the header row or the column is kept in view (TBL-19, TBL-20) SHALL be drawn over comp.table.sticky.container.color, so that nothing shows through it.
TBL-36 WHEN a sort button receives keyboard focus THE table SHALL show the system's focus indicator inside the edge of its header cell without animation — sys.focus.outer-width wide in the color role focus-ring, set in from the edge by sys.focus.offset so that neither the next cell, the box's edge nor a header kept in view clips it, with the inner stroke of sys.focus.inner-width where the system has one; focus gained by pointer or touch SHALL NOT show it. WHEN keyboard focus moves to a sort button, a checkbox or a radio button that is under a header row, a first column or a status line kept in view, or outside the box, THE table SHALL scroll its box so that the whole control is in view beside them.
TBL-37 WHEN a sort button is pressed by pointer, touch, Enter or Space THE table SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
TBL-38 WHILE a sort button is pressed THE table SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; label, mark and opacity still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.table.motion.feedback-duration with comp.table.motion.state-easing, fades over comp.table.motion.state-duration with comp.table.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
TBL-39 THE table SHALL show the keyboard focus of a sort button as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. A sort button's container has square corners, the header cell's, so sys.shape.corner-smoothing has nothing to ease on it.
TBL-40 THE table SHALL make a sort button's hit area its whole header cell, with no gap to the hit area of the header cell beside it. WHERE input is touch, or the platform cannot tell the input type, a header row that holds a sort button SHALL be at least sys.target.touch high, drawn so, at both densities, because a header cell cannot extend a hit area over the row under it; for a fine pointer it SHALL be at least sys.target.pointer high WHILE density is regular, and WHILE density is compact as high as its density gives. A header cell narrower than the target SHALL extend its hit area along the row no further than halfway into the paddings between it and the cell beside it.
TBL-41 THE table SHALL expose a sortable column's header cell as the column header it is (TBL-24) with the order its sort names — ascending, descending, or none for a column that can be sorted and is not — and the sort button inside it as a button named by the column's header; the sort mark SHALL be hidden from assistive technology. On the web these are aria-sort on the header cell and the product's own button in it. WHERE a platform has no sort state THE table SHALL say the order after the button's name with sortedAscendingLabel or sortedDescendingLabel, which come from the instance or from the product's words under the keys of the same names and from nowhere else; IF such a platform is given neither, THE table SHALL expose the button by its name alone and in debug builds report the missing word once. THE table SHALL NOT announce an order itself when it changes: the header cell's state says it, and a product that wants it spoken writes it in status (TBL-52).
TBL-42 WHILE selection is multiple or single THE table SHALL draw a selection column before its first column: in the header row a cell holding the checkbox for every row WHILE selection is multiple (TBL-45) and nothing WHILE it is single (TBL-60), in each body row that has a value a cell holding that row's control — a checkbox WHILE selection is multiple, a radio button WHILE it is single — and an empty cell in a body row without a value, in a placeholder row and in a footer row. Each checkbox is the Checkbox component and each radio button the Radio button component, as their own specs draw them, with no label drawn, placed on the cell's first line (TBL-15) with the edge of its box or ring, not of its round container, at the cell's padding, so that the boxes and rings stand on the line the text of the other cells starts at: the selection cell's paddings are a cell's paddings less half of what the control's container exceeds its box by — comp.checkbox.container.size over comp.checkbox.box.size, comp.radio-button.container.size over comp.radio-button.ring.size — (never less than 0), the round container reaching into them, and the column is as wide as the control's container and those paddings. The selection column is not a column of data: it has no header text, the first column of rowHeaders and of stickyFirstColumn is the one after it, and a sort event's index does not count it. WHILE stickyFirstColumn is true the selection column SHALL stay in view with the first column (TBL-20), and the two together are what is released when they would take more than half of the box. WHILE selection is none THE table SHALL draw no such column and no checkbox or radio button, SHALL ignore every row's selected, and SHALL emit no selectionChange.
TBL-43 WHILE selection is multiple or single and a body row's selected is true THE table SHALL draw that row as chosen: comp.table.row.selected.container.color across its whole width, paddings and the selection cell included, in place of the stripe of TBL-11 where the row would have one; the text of its cells and of its row header in comp.table.row.selected.text.color; and its checkbox or radio button checked. What a cell holds that is not plain text SHALL keep its own colors (TBL-15). In a column kept in view the fill SHALL lie over comp.table.sticky.container.color. The lines and the gap between rows (TBL-8, TBL-10) SHALL be drawn as without it. The fill SHALL change at once, without a motion of the table's own. A footer row and a placeholder row are never chosen.
TBL-44 WHEN the user changes a row's checkbox or radio button — by a press, by Space while it has focus, or, for a radio button, by an arrow key (TBL-61) — THE table SHALL set that row's selected to the control's new state, WHILE selection is single let the row that was chosen go (TBL-60), and emit selectionChange exactly once, carrying the values of every chosen row after the change in row order. A press on the row outside its selection cell presses its control where TBL-58 says so, and this sentence holds for that change as for any other. WHEN the product sets a row's selected THE table SHALL draw it and SHALL NOT emit. THE table SHALL keep which rows are chosen by their values: WHEN rows are given again in another order (TBL-33) each row SHALL be drawn chosen or not by its own selected, and the table SHALL carry no selection over to a row it is not given.
TBL-45 WHILE selection is multiple THE table SHALL keep the checkbox for every row in step with the body rows it holds that have a value: checked WHILE each of them is chosen, in its mixed (indeterminate) state WHILE some are and some are not, unchecked WHILE none is, and disabled WHILE there is no such row. WHEN the user activates it while it is unchecked or mixed THE table SHALL choose every such row, and while it is checked SHALL clear every one, and SHALL emit selectionChange exactly once with the values after the change. "Every row" is every row the table has been given: it SHALL NOT claim to choose records the product has not given it (the rows of other pages), and a product that offers that says so with a control of its own. WHILE selection is single there is no such checkbox (TBL-60).
TBL-46 THE table SHALL name each row's checkbox or radio button by that row's selectLabel; WHERE selectLabel is empty and rowHeaders is true the control SHALL be named by the text of the row's header cell; IF it has neither THE table SHALL expose the control without a name and in debug builds report the row once per table. WHILE selection is multiple THE table SHALL name the checkbox for every row by selectAllLabel, from the instance or from the product's words under the key selectAllLabel and from nowhere else; IF neither gives it THE table SHALL still draw the checkbox, expose it without a name and in debug builds report the missing word. A table whose selection is multiple or single SHALL still be exposed as a table (TBL-24), its rows as rows and its selection cells as cells: which rows are chosen is said by the checked state of their checkboxes or radio buttons, the header checkbox says checked, mixed or not checked, and the table SHALL add no selected state to a row and no role of a grid.
TBL-47 THE table SHALL make each sort button and each checkbox its own stop in the focus order, and the radio buttons of a table whose selection is single one stop together (TBL-61), in reading order — the scroll region first while the table scrolls (TBL-22), then the header row's controls from the start, then each row's checkbox, or the one radio button that is the stop, followed by whatever in that row's cells takes focus — and SHALL handle no key that moves focus from cell to cell: the arrow keys scroll the region or do what the focused control does with them, which for a radio button is TBL-61. A checkbox's or radio button's hit area SHALL be its whole selection cell, in width and in height; for a pointer the rest of its row answers too (TBL-58). WHERE input is touch, or the platform cannot tell the input type, a row that holds a checkbox or a radio button SHALL be at least sys.target.touch high, drawn so, at both densities, and its selection cell at least sys.target.touch wide; for a fine pointer the row SHALL be at least sys.target.pointer high WHILE density is regular, and WHILE density is compact as high as its density gives.
TBL-48 WHILE the table has no body row, loading is false and the empty slot has content THE table SHALL draw that content in the place of the body rows: under the header row and its line, before any footer row, as wide as the table's box and staying in view while the columns scroll sideways, with comp.table.empty.padding-block above and below it and a cell's inline padding at its sides (TBL-4), with no line, no stripe and no fill of the table's through it. THE table SHALL expose it as one row holding one cell that spans every column, SHALL add no word, glyph or role of its own to it, and SHALL NOT announce it (TBL-25): where a search or a filter emptied the table, the product's Empty state is the live one. WHILE the table has no body row and the empty slot has no content THE table SHALL draw its caption, its header row and its footer rows and nothing between them. WHILE the table has a body row THE table SHALL NOT draw the empty slot's content.
TBL-49 WHILE loading is true THE table SHALL be exposed as busy; SHALL draw the rows it is given as they are — the rows it had, or placeholder rows (TBL-50) — and SHALL NOT draw the empty slot's content, even with no body row; SHALL keep its header row, its sort buttons, its checkboxes, its radio buttons and its status line working as without it; SHALL change no color, no opacity and no size of its own because of it; and SHALL announce nothing itself when loading begins or ends: the product says it in status (TBL-52). THE table SHALL fetch nothing, wait for nothing and time nothing: loading is true for as long as the product says so.
TBL-50 WHERE a row's placeholder is true THE table SHALL draw it as a body row — counted for stripes, parted from its neighbours as TBL-9 says, with a cell's paddings — at least as tall as a body row holding one line of text at the table's density, however little its cells hold; SHALL place in each cell what the product gives it there (a Skeleton, by TBL-15) or nothing; SHALL draw no checkbox or radio button in its selection cell and count it neither as a body row for the header checkbox (TBL-45) nor as a row for the empty slot (TBL-48); and SHALL hide the row from assistive technology, so that a person hears that the table is busy and not a row of nothing. IF a row's placeholder is true while loading is false THE table SHALL draw it all the same and in debug builds report it.
TBL-51 WHERE status is not empty or the pagination slot has content THE table SHALL draw a status line under its last row: the status text at the start edge in comp.table.status.text.typescale and comp.table.status.text.color, the pagination slot's content at the end edge, both centered on the line's height, with comp.table.status.padding-block above and below, a cell's inline padding at its sides (TBL-4) and at least comp.table.status.gap between the two; WHERE the two do not fit on one line THE table SHALL put the status text above the pagination, comp.table.status.gap apart, each at the start edge, and the text SHALL wrap and never be cut. The status line SHALL be as wide as the table's box and SHALL stay where it is while the columns scroll sideways (TBL-17). WHILE separation is divider THE table SHALL draw one line of comp.table.divider.thickness in comp.table.divider.color above it. WHILE the table scrolls its rows in its own box (TBL-18) the status line SHALL stay at the bottom of that box while the rows pass under it, filled with comp.table.sticky.container.color, with that line above it whatever the separation is while a row is under it. WHERE status is empty and the slot has no content THE table SHALL draw no status line and leave no room for one.
TBL-52 THE table SHALL expose the status text as a status that assistive technology is told of politely WHEN it changes, without moving focus, and SHALL tell it nothing when the text is first drawn with the table. THE table SHALL write no word into it and SHALL count nothing for it: how many rows there are, which of them are shown and how many are chosen are the product's to say, in its own language. THE table SHALL place the content of the pagination slot and SHALL NOT style it, give it keys or listen to it: the table shows the rows it is given, emits nothing about pages, and holds no page number and no page size.
TBL-53 WHERE the layout direction is right-to-left THE table SHALL draw the selection column at the right edge, before the first column; SHALL place a sort mark after its label on the left, and before it — on the right — in a column whose align is end; SHALL draw the status text at the right edge of the status line and the pagination at the left; and SHALL keep the selection column and a first column that stay in view at the right (TBL-27). The glyphs swap_vert, arrow_upward and arrow_downward show an order and not a direction of reading, and SHALL NOT be mirrored. The status text SHALL be laid out as text of its own direction (TBL-28).
TBL-54 WHERE the platform draws with a person's own forced colors THE table SHALL draw a sort button's label and mark in the platform's button-text color and its focus indicator in the platform's highlight color, SHALL keep the sorted column told from the others by its glyph alone, and, because the mode removes the fill of a chosen row, SHALL rely on the row's control — its checkbox, which the Checkbox draws checked in that mode, or its radio button, which the Radio button draws selected there — to say that the row is chosen; it SHALL draw the line above a status line kept in view in the platform's text color (TBL-30).
TBL-55 WHILE the text size is enlarged THE table SHALL grow a sort button's label and its mark's box with it, wrap the label inside its column where the column's wrap allows it and keep the mark on the label's last line, grow the status text and wrap it, keep the selection column as wide as its checkbox or radio button and paddings, and lay the status line out in two lines where one no longer fits (TBL-51); nothing of these SHALL be cut, and TBL-29 holds for a table with every one of them at a width of 320 points.
TBL-56 WHILE the system's icon set cannot yet draw a sort mark (its font is still loading) THE table SHALL draw nothing in the mark's box, never the glyph's name as text, SHALL keep the box, and SHALL draw the glyph once the set can; the sort button SHALL work all the same.
TBL-57 WHERE the platform draws the product's own elements and script does not run (the web) THE table SHALL still draw, from the product's markup alone, everything of TBL-32, TBL-33, TBL-42, TBL-43, TBL-48, TBL-50 and TBL-51 that is drawing: a sort button and the mark of the order the header cell names, the hover, focus and press of the button, the selection column, a chosen row, the empty content, placeholder rows and the status line; a sort button that is an anchor, and checkboxes and radio buttons that are the platform's own inside the product's form, SHALL work as the platform makes them work — radio buttons of one name exclude each other and share one stop without script (TBL-60, TBL-61). What needs script and is then absent: the events, a press on a row outside its checkbox or radio button and the row's hover and press (TBL-58, TBL-59), which are then not drawn, the header checkbox kept in step and choosing every row (TBL-45), the busy state where the product did not write it, keeping focus through a change of rows, and what TBL-22 already loses. Nothing SHALL be hidden that script would have to reveal.
TBL-58 WHILE selection is multiple or single, WHEN a body row that can be chosen (it has a value and a checkbox or radio button, TBL-42) is pressed and released outside its selection cell with the primary mouse button, a touch or a pen, without the pointer moving further than the platform's touch slop between the two, THE table SHALL press that row's control exactly as a press on the control does: a checkbox's row is chosen or let go, a radio button's row is chosen where it was not and stays chosen where it was (TBL-60), and selectionChange is emitted once where the choice changed (TBL-44). For a pointer the whole row is the control's hit area, the way a whole card answers a press while the controls inside it keep their own. THE table SHALL NOT press the control IF the press lands on or inside something in a cell that answers a press itself — a link, a button, a field, a select, a label, a summary, an editable element, an element with a tab index, an element with a widget role, a custom element that takes focus, or an element to which the product gave the pointer cursor where the row's own is not (the pointer the row shows for itself does not count) — which keeps its own behaviour; IF the release ends a text selection, that is, the selection is not empty and lies in the row once the press is released; IF the pointer moved further than the slop (a drag, a scroll); IF a modifier key (Control, Command, Shift, Alt or Option) is held or a button other than the primary is used; IF the press is the second or a later press of a multiple click; IF the product cancelled the click; or IF the press lands inside a table nested in a cell, whose rows are its own. A press in the selection cell is the control's (TBL-47). The header row, whose checkbox stands for every row, a footer row, a placeholder row and a body row without a value SHALL take no such press. The row SHALL get no role, no name, no focus stop, no selected state and no announcement of its own: the checkbox or radio button stays the one control of the row, which the keyboard and assistive technology reach as TBL-46, TBL-47 and TBL-61 say, and a press on the row is a larger target for the same action. WHERE the platform's row answers no press (on the web, without the element's script) the row SHALL show neither the pointer cursor nor a state, and a press there SHALL choose nothing.
TBL-59 WHILE selection is multiple or single and a fine pointer is over a body row that can be chosen where a press would be its control's — its selection cell, or its cells outside anything that answers a press itself (TBL-58) — THE table SHALL draw that row hovered, as one target — the chosen row of a table whose selection is single too, whose press is its radio button's even though it changes nothing, as the radio button itself answers a hover and a press while it is selected — across its whole width, paddings and the selection cell included: its fill in the hovered container color of the row's condition — selected WHILE the row is chosen, unselected otherwise; the default system mixes comp.table.row.selected.state-layer.color or comp.table.row.unselected.state-layer.color into that condition's resting fill — laid over what the row is filled with at rest (its stripe, the chosen fill, comp.table.sticky.container.color in a column kept in view) and short of the gap of TBL-10; the text of its cells in the hovered text color of its condition; and the pointer cursor. WHILE the pointer is over something in a cell that answers a press itself, that thing SHALL keep its own cursor, hover and press, and the row SHALL be drawn at rest. WHEN such a row is pressed by pointer, touch or pen where TBL-58 or TBL-47 makes the press its control's THE table SHALL draw the row pressed without delay and keep it so for at least sys.motion.duration.press-hold, even when the press ends sooner, answering as sys.behavior.pressed-feedback names: overlay draws the pressed container and text colors of the row's condition across the row; dim applies the table's pressed opacity to the whole row while its fill and text stay with the state under the press; none draws no answer; and ripple, which has no one box to grow in across the cells of a row, answers as overlay. The press SHALL end when the pointer leaves the row, moves further than the slop or is cancelled. The fills SHALL change at once, without a motion of the table's own, as the chosen fill does (TBL-43). The checkbox or radio button SHALL keep its own hover, focus and press; a key that presses it or moves to it SHALL NOT draw the row pressed, and its keyboard focus is shown by the control alone. Under forced colors the row's state fills are gone with the other fills (TBL-54).
TBL-60 WHILE selection is single THE table SHALL let one row at most be chosen: each body row that can be chosen holds a radio button in its selection cell (TBL-42), and the header row's selection cell SHALL hold nothing, because no control stands for every row; IF the product puts a control there THE table SHALL NOT draw it and in debug builds report it. The radio buttons of one table SHALL be one group whose options exclude each other (RAD-3): on the web the radio buttons of one name, the name the product gave them or, for those it gave none, one the table gives them. WHEN the user chooses a row that is not chosen — by a press on its radio button or on the row (TBL-58), by Space, or by an arrow key (TBL-61) — THE table SHALL set that row's selected, let the row that was chosen go, and emit selectionChange exactly once, carrying the value of the chosen row alone. WHEN the user activates the radio button of the chosen row, or presses that row, THE table SHALL keep the row chosen and SHALL NOT emit: a radio button is never cleared by its own activation (RAD-2), and a product that lets a person go back to no row chosen offers a control of its own for that, which clears selected. WHEN the product sets selected THE table SHALL draw it and SHALL NOT emit (TBL-44); IF more than one row says it is chosen THE table SHALL draw chosen the one whose selected the product set last — of several set at once, the first in row order — and let the others go without emitting, and in debug builds report several set at once. IF a body row's selection cell holds a checkbox WHILE selection is single, or a radio button WHILE it is multiple, THE table SHALL treat the row as one that cannot be chosen, SHALL NOT draw that control, and in debug builds report it.
TBL-61 WHILE selection is single THE table SHALL expose each row's radio button as a radio, checked or not, named as TBL-46 says, with its position among the table's radio buttons and their number, and SHALL wrap them in no radio group, give a row no selected state and add no role of a grid: a radio group cannot hold the rows of a table, and the table's name and the column the radio buttons stand in say what they choose. THE radio buttons SHALL be one stop in the focus order — the chosen row's, or, while no row is chosen, the first row's that can be chosen — at the place of that row's control in the reading order of TBL-47, and Tab SHALL leave them rather than move between them (RAD-5). WHILE one of them has keyboard focus, Down Arrow and Right Arrow SHALL move focus to the radio button of the next row that can be chosen and Up Arrow and Left Arrow to the previous one's, wrapping from the last to the first, and choose that row as TBL-60 says, with one selectionChange for each key; in a right-to-left layout Left Arrow and Right Arrow SHALL exchange what they do. THE table SHALL scroll its box so that a radio button that receives keyboard focus is in view beside a header row or a status line kept in view (TBL-36), and WHEN rows are given again focus SHALL stay with the radio button of the row of the same value (TBL-25).

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-table TanreiTable TanreiTable TanreiTable Table
caption · 属性 caption caption caption caption caption
label · 属性 label accessibilityLabel contentDescription semanticLabel accessibilityLabel
density · 属性 density density density density density
separation · 属性 separation separation separation separation separation
striped · 属性 striped striped striped striped striped
rowHeaders · 属性 rowHeaders rowHeaders rowHeaders rowHeaders rowHeaders
stickyHeader · 属性 stickyHeader stickyHeader stickyHeader stickyHeader stickyHeader
stickyFirstColumn · 属性 stickyFirstColumn stickyFirstColumn stickyFirstColumn stickyFirstColumn stickyFirstColumn
selection · 属性 selection selection selection selection selection
selectAllLabel · 属性 select-all-label selectAllLabel selectAllLabel selectAllLabel selectAllLabel
sortedAscendingLabel · 属性 sorted-ascending-label sortedAscendingLabel sortedAscendingLabel sortedAscendingLabel sortedAscendingLabel
sortedDescendingLabel · 属性 sorted-descending-label sortedDescendingLabel sortedDescendingLabel sortedDescendingLabel sortedDescendingLabel
loading · 属性 loading isLoading loading loading loading
status · 属性 status status status status status
sort · 事件 sort onSort onSort onSort onSort
selectionChange · 事件 selectionchange selection onSelectionChange onSelectionChanged onSelectionChange
备注 Form: <tanrei-table> is a custom element with **no shadow root**, wrapped around a native <table> that stays in the light DOM — <tanrei-table><table><caption>…</caption><thead>…</thead><tbody>…</tbody><tfoot>…</tfoot></table></tanrei-table>. The native table is never given another display, so its semantics, a crawler's reading, copy and paste as a table and a screen reader's table navigation are the browser's own. The wrapper is the scroll box (display: block; overflow: auto; max-inline-size: 100%; the product limits its height with max-block-size). Everything drawn comes from one stylesheet, @tanrei/web/table.css, whose selectors start at tanrei-table and are wrapped in :where(); it styles the page without script (an element that is not yet defined is still an element), and the element adopts the same sheet into the root it is connected to, so it also works inside another component's shadow root. Props are attributes of the wrapper: density, separation, striped, row-headers, sticky-header, sticky-first-column, label. caption is the native <caption>; the caption attribute is a convenience that writes one. label becomes aria-label on the table. Script adds only what CSS cannot: while the wrapper overflows it sets tabindex="0", role="region" and aria-labelledby (the caption) or aria-label (TBL-22), and removes them when it does not; it sets data-more-start and data-more-end for the lines of TBL-21, writes the positions of the columns whose header names data-align or data-wrap on the wrapper (data-columns-end, -center, -nowrap; one rule per position places the cells and no cell is written to), gives first cells role="rowheader" where row-headers is set on a table whose body cells are <td> (Markdown), and releases a first column wider than half of the box (TBL-20). Without script the table is fully drawn, scrolls in its box, and keeps its sticky parts; it lacks the two edge lines and, in a browser that does not focus scrollers by itself, the focus stop. Sticky parts are position: sticky on the cells (inset-block-start: 0 on thead th, inset-inline-start: 0 on the first cell of each row) with border-collapse: separate and the lines drawn as cell borders, because collapsed borders do not travel with a sticky cell. The caption stays in place sideways with position: sticky; inset-inline-start: 0 and a width of the wrapper (100cqi). font-variant-numeric: tabular-nums on cells (TBL-4); unicode-bidi: plaintext per cell (TBL-28). Markdown: a renderer emits a bare <table>; the product wraps it at build time (a rehype step of a few lines, shown in the README) — Tanrei does not transform content. Inside .tanrei-prose the element is an island by its tag name (PRS-16). A cell that spans (colspan, rowspan) and a second header row keep working as HTML and are drawn with the same cell styles, but no fixture covers them. As built: the wrapper is a grid of one cell, not display: block, so that the two "there is more" lines are as tall as the table; the attributes the element writes are data-more-start, data-more-end, data-more-above, data-first-column-released, data-columns and the three positions above, and data-separation on the table; the region's aria-labelledby or aria-label is copied from the table's own name where the product named it natively. **Sorting, selection, empty, loading and status: proposed form, for the platform's agent to settle.** Everything new is the product's own markup inside the same <table>, drawn by table.css without script (TBL-57). Sorting: the stylesheet makes the control in a th[aria-sort] fill the cell (the cell's paddings move onto the control) and draws the mark as a generated glyph from the attribute; the element listens for the control's activation and emits sort (cancelable where the control is an anchor) with detail: { column, index, direction }; it never writes aria-sort. Selection: selection="multiple" on the wrapper; the header row's first cell is <td data-part="selection"> (a cell and not a header cell: a header without text is announced as an empty one) holding the checkbox for every row, which the element keeps checked, indeterminate or unchecked and disables with no row to choose, and names from select-all-label or the product's words (setWords({ selectAllLabel })); the element listens to change from the row checkboxes, writes data-selected, and emits selectionchange with detail: { values }. Neither checkbox is moved, wrapped or replaced. sorted-ascending-label and sorted-descending-label are accepted and not read: aria-sort is the browser's. Empty: the product writes <tbody><tr data-part="empty"><td colspan="…">…</td></tr></tbody> with its <tanrei-empty-state> inside; the stylesheet keeps the cell's content as wide as the box (100cqi) and in view sideways, as it does the caption, and the element corrects a colspan that is not the number of columns. Loading: the loading attribute; the element writes aria-busy on the table (a server may write it itself). Status: after the <table>, inside the wrapper, <div data-part="status"><p role="status">…</p> <nav class="tanrei-pagination">…</nav></div>; it is sticky at the inline start with the box's width, and at the block end, with the sticky fill; the status attribute is a convenience that writes the paragraph, as caption writes a caption. Coarse pointers get the taller header row and rows by @media (pointer: coarse). Proposed spelling, for the platform's agent to settle. TanreiTable(caption:, columns: [TanreiTableColumn], density:, …) { TanreiTableRow { TanreiTableCell("…"); TanreiTableCell { TanreiTag("Beta", size: .small) } } }. Built on Grid and GridRow (columns sized from content, which is TBL-2) inside a ScrollView whose axes are the ones that overflow; a table of several hundred rows uses a LazyVStack of rows with column widths measured once from the header and a sample of rows. The platform's own Table is not used: it is a data table of its own look, with selection and sorting, and it collapses to its first column in a compact width. Sticky parts are pinned section headers (pinnedViews) for the header row and an overlaid first column whose offset follows the scroll. Accessibility: SwiftUI has no table container for custom views, so TBL-24's fallback applies — each row is an accessibility container in reading order, each cell's label is its column header followed by its content, header cells carry the header trait, and the scroll view is named by the caption. Sorting, selection, empty, loading and status: TanreiTableColumn(header:, value:, isSortable:, sort:), TanreiTable(selection: $values, onSort: { column, index, direction in … }) where selection is a binding to the set of chosen values, and rows take value:, selectLabel:, isPlaceholder:. A sortable header is a Button whose accessibility value is sortedAscendingLabel or sortedDescendingLabel (SwiftUI has no sort trait); the row's checkbox is TanreiCheckbox. The empty slot and the pagination slot are view builders (empty:, pagination:); the status text is posted as an announcement when it changes; isLoading sets nothing SwiftUI has a trait for, so placeholder rows are hidden and the status says it. Single selection (selection single, TBL-60, TBL-61): the row's control is the platform's Tanrei radio button in place of the checkbox, the header row holds none, and the product's selection holds one value at most. Proposed spelling, for the platform's agent to settle. TanreiTable(columns = listOf(TanreiTableColumn("Price", align = End)), caption = …) { row { cell("…"); cell { TanreiTag("Beta", size = Small) } } }. Compose has no table layout: a custom Layout measures each column's least and preferred widths (intrinsics) and shares the width as TBL-2 says, inside horizontalScroll; a long table is a LazyColumn of rows with the widths measured from the header and the rows composed so far, and stickyHeader for the header row. Semantics: the container sets collectionInfo(rowCount, columnCount) and each cell collectionItemInfo(rowIndex, columnIndex), header cells are heading(), and each cell's content description is preceded by its column header, so TalkBack announces position and header. The scroll container is focusable and named by the caption. Sorting, selection, empty, loading and status: TanreiTableColumn(header, value = "price", sortable = true, sort = TanreiTableSort.Ascending), onSort = { column, index, direction -> }, selection = TanreiTableSelection.Multiple, rows as row(value = "a1", selected = …, selectLabel = …) with onSelectionChange = { values -> }; the product holds both states. A sortable header is Modifier.clickable(role = Role.Button) with stateDescription from sortedAscendingLabel or sortedDescendingLabel; the checkboxes are TanreiCheckbox (ToggleableState.Indeterminate for the header's); empty and pagination are composable slots; the status text is a polite liveRegion. Single selection (selection single, TBL-60, TBL-61): the row's control is the platform's Tanrei radio button in place of the checkbox, the header row holds none, and the product's selection holds one value at most. Proposed spelling, for the platform's agent to settle. TanreiTable(caption: …, columns: [TanreiTableColumn(header: "Price", align: TanreiTableAlign.end)], rows: [TanreiTableRow(cells: [...])]). Built on the framework's Table widget with IntrinsicColumnWidth (flex for a column that wraps), inside a horizontal SingleChildScrollView in a Scrollbar; a long table uses a two-dimensional scroll view with pinned rows and columns for the sticky parts. The Material library's DataTable is not used: it brings its own look, sorting and selection, and reads a Material theme. Semantics: the table, row, cell and column-header roles where the framework version has them, and otherwise one Semantics per cell whose label is the column header followed by the content; the scroll view is a focusable region named by the caption. Sorting and selection: TanreiTableColumn(header:, value:, sortable:, sort:), onSort: (column, index, direction) {}, selection: TanreiTableSelection.multiple, TanreiTableRow(value:, selected:, selectLabel:, placeholder:) and onSelectionChanged: (values) {}; the product holds both states. A sortable header is an InkWell in Semantics(button: true, value: sortedAscendingLabel); the checkboxes are TanreiCheckbox with tristate for the header's; empty and pagination are widgets; the status text is Semantics(liveRegion: true); placeholder rows are in ExcludeSemantics. Single selection (selection single, TBL-60, TBL-61): the row's control is the platform's Tanrei radio button in place of the checkbox, the header row holds none, and the product's selection holds one value at most. Proposed spelling, for the platform's agent to settle. <Table caption="…" columns={[{ header: "Price", align: "end" }]}><Table.Row><Table.Cell>…</Table.Cell></Table.Row></Table>. React Native has no table layout: rows are Views in a horizontal ScrollView (inside a vertical one, or a FlatList with stickyHeaderIndices for a long table), and column widths are measured in a first pass with onLayout and applied in a second, so the first frame of a large table is drawn after measuring. A first column that stays in view is a second column of cells drawn over the scroll view and kept in step with its vertical offset. There is no table role on either native platform: TBL-24's fallback applies — a row is one accessible group and each cell's accessibilityLabel is its column header followed by its content; header cells have accessibilityRole="header". Follows the app's direction where an ancestor's is not known. Sorting and selection: columns { header, value, sortable, sort }, onSort={({ column, index, direction }) => …}, selection="multiple", <Table.Row value selected selectLabel placeholder> and onSelectionChange={(values) => …}; the product holds both states. A sortable header is a Pressable with accessibilityRole="button" and accessibilityValue={{ text: sortedAscendingLabel }}; the checkboxes are Checkbox (accessibilityState={{ checked: "mixed" }} for the header's); empty and pagination are elements; the status text has accessibilityLiveRegion="polite" on Android and is posted with AccessibilityInfo.announceForAccessibility on iOS; accessibilityState={{ busy: true }} while loading. Single selection (selection single, TBL-60, TBL-61): the row's control is the platform's Tanrei radio button in place of the checkbox, the header row holds none, and the product's selection holds one value at most.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-table-caption-gap sys.space.space100 8px
--tanrei-comp-table-caption-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-table-cell-padding-block-compact — 6px
--tanrei-comp-table-cell-padding-block-regular — 12px
--tanrei-comp-table-cell-padding-inline-compact sys.space.space150 12px
--tanrei-comp-table-cell-padding-inline-regular sys.space.space200 16px
--tanrei-comp-table-cell-text-color sys.color.on-surface #1b1b1b
--tanrei-comp-table-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-table-divider-thickness sys.border.hairline 1px
--tanrei-comp-table-empty-padding-block sys.space.space400 32px
--tanrei-comp-table-focused-opacity — 1
--tanrei-comp-table-header-container-color — #000000 · 0%
--tanrei-comp-table-header-gap sys.space.space50 4px
--tanrei-comp-table-header-icon-size sys.icon.extra-small 16px
--tanrei-comp-table-header-icon-weight sys.icon.mark.weight 500
--tanrei-comp-table-header-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-table-hovered-opacity — 1
--tanrei-comp-table-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-table-motion-state-duration sys.motion.role.state.large 250ms
--tanrei-comp-table-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-table-pressed-opacity — 1
--tanrei-comp-table-row-gap sys.space.space50 4px
--tanrei-comp-table-row-selected-container-color sys.color.secondary-container #262626 · 16%
--tanrei-comp-table-row-selected-hovered-container-color — #262626 · 22.7%
--tanrei-comp-table-row-selected-hovered-text-color — #262626
--tanrei-comp-table-row-selected-pressed-container-color — #262626 · 24.4%
--tanrei-comp-table-row-selected-pressed-text-color — #262626
--tanrei-comp-table-row-selected-state-layer-color comp.table.row.selected.text.color #262626
--tanrei-comp-table-row-selected-text-color sys.color.on-secondary-container #262626
--tanrei-comp-table-row-unselected-container-color — #000000 · 0%
--tanrei-comp-table-row-unselected-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-table-row-unselected-hovered-text-color — #1b1b1b
--tanrei-comp-table-row-unselected-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-table-row-unselected-pressed-text-color — #1b1b1b
--tanrei-comp-table-row-unselected-state-layer-color comp.table.cell.text.color #1b1b1b
--tanrei-comp-table-row-unselected-text-color comp.table.cell.text.color #1b1b1b
--tanrei-comp-table-separation sys.behavior.separation 0
--tanrei-comp-table-sort-container-color — #000000 · 0%
--tanrei-comp-table-sort-focused-container-color — #4b4b4b · 10%
--tanrei-comp-table-sort-focused-icon-color — #4b4b4b
--tanrei-comp-table-sort-focused-label-color — #4b4b4b
--tanrei-comp-table-sort-focused-state-layer-color — #4b4b4b
--tanrei-comp-table-sort-hovered-container-color — #4b4b4b · 8%
--tanrei-comp-table-sort-hovered-icon-color — #4b4b4b
--tanrei-comp-table-sort-hovered-label-color — #4b4b4b
--tanrei-comp-table-sort-hovered-state-layer-color — #4b4b4b
--tanrei-comp-table-sort-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-table-sort-label-color comp.table.header.text.color #4b4b4b
--tanrei-comp-table-sort-pressed-container-color — #4b4b4b · 10%
--tanrei-comp-table-sort-pressed-icon-color — #4b4b4b
--tanrei-comp-table-sort-pressed-label-color — #4b4b4b
--tanrei-comp-table-sort-pressed-state-layer-color — #4b4b4b
--tanrei-comp-table-sort-state-layer-color comp.table.sort.label.color #4b4b4b
--tanrei-comp-table-sorted-container-color — #000000 · 0%
--tanrei-comp-table-sorted-focused-container-color — #1b1b1b · 10%
--tanrei-comp-table-sorted-focused-icon-color — #1b1b1b
--tanrei-comp-table-sorted-focused-label-color — #1b1b1b
--tanrei-comp-table-sorted-focused-state-layer-color — #1b1b1b
--tanrei-comp-table-sorted-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-table-sorted-hovered-icon-color — #1b1b1b
--tanrei-comp-table-sorted-hovered-label-color — #1b1b1b
--tanrei-comp-table-sorted-hovered-state-layer-color — #1b1b1b
--tanrei-comp-table-sorted-icon-color sys.color.on-surface #1b1b1b
--tanrei-comp-table-sorted-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-table-sorted-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-table-sorted-pressed-icon-color — #1b1b1b
--tanrei-comp-table-sorted-pressed-label-color — #1b1b1b
--tanrei-comp-table-sorted-pressed-state-layer-color — #1b1b1b
--tanrei-comp-table-sorted-state-layer-color comp.table.sorted.label.color #1b1b1b
--tanrei-comp-table-status-gap sys.space.space200 16px
--tanrei-comp-table-status-padding-block sys.space.space100 8px
--tanrei-comp-table-status-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-table-sticky-container-color sys.color.surface #ffffff
--tanrei-comp-table-stripe-container-color sys.color.surface-container-low #f9f9f9
--tanrei-comp-table-caption-text-typescale sys.typescale.body-small 400 13px/1.5385
--tanrei-comp-table-cell-text-typescale-compact sys.typescale.body-small 400 13px/1.5385
--tanrei-comp-table-cell-text-typescale-regular sys.typescale.body 400 14px/1.5714
--tanrei-comp-table-header-text-typescale-compact sys.typescale.label-small 500 12px/1.3333
--tanrei-comp-table-header-text-typescale-regular sys.typescale.label 500 14px/1.4286
--tanrei-comp-table-status-text-typescale sys.typescale.body-small 400 13px/1.5385

测试样例

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

生成自 specs/table.spec.json, tokens/comp/table.tokens.json, design/tokens/

指南

它是不是表格

有行有列的数据才用表格。不要用表格把东西并排摆放:辅助技术会为每个单元格朗读行、列和行列标题,对并非表格性质的内容来说这只是噪音。并排摆放请用布局。

没有第二个维度的一组键和值,更接近描述列表,而不是表格。

密度

density 只有一行文字的行的高度 用于
regular(默认) 46 供人阅读的表格:行数不多,单元格会换行。
compact 32 供人扫读的表格:几百行,标识符和值。

行没有自己的高度。它与其中最高的单元格一样高。

区分各行

separation 绘制什么
auto(默认) 设计系统选定的方式:在默认系统中是一道细线。
divider 行与行之间、表头行下方和表尾行上方各一道细线。
gap 留空,不画线。

striped 每隔一行用一种柔和的颜色填充表体的行。它用于有许多相似行的宽表格,视线横向移动时容易跟丢所在的行。它可以与任何一种分隔方式组合。

列

  • 列按自己的内容确定宽度。列绝不会比其中不可断开的内容更窄,长文本的列则占用短列留下的空间。
  • 文本会换行,绝不截断。标识符不在内部断开,而是把所在的列撑宽。
  • 数字列靠末尾对齐,让数位对齐。文本留在起始侧。只有标记,或者在每一行里都是同一类答案的单个词,才居中。
  • 只在不得不这样做的地方自己指定列宽(在列标题单元格上写 style="width: …")。它会取代由内容得出的宽度。

放不下的时候

  • 表格在自己的框内滚动,而不是让页面滚动。框的某条边上的一道细线,表示这条边之外还有列。 表格外面不需要再套滚动区域:它自己会标示边缘。
  • sticky-first-column 让第一列始终可见,其余各列在它下面滚动。当它会占去框的一半以上时,它会自行放开,所以手机屏幕绝不会被固定的那一列占满。
  • sticky-header 让表头行在各行滚动时始终可见。它在你的布局限制了表格高度的地方起作用(元素上的 max-block-size);它不跟随页面。
  • 如果表格位于页面表面之外的另一种表面上(卡片、对话框),并且让表头行或第一列始终可见,或者带有状态行,请把 --tanrei-comp-table-sticky-container-color 设为那个表面的颜色。这些部分用这个颜色填充,这样从它们下面经过的行和列才不会透出来。

排序指引

  • 用户会凭某一列去找某一行时,才让这一列可排序:名称、日期、金额、类别。备注列不属于这种情况。
  • 每个可排序的列都画出标记,静止时也画,这样在触摸屏上也能看出哪些列标题可以按下。
  • 一次只按一列排序。第二个排序键要用你自己的控件。
  • 想让新的顺序被朗读出来,就在状态行里说明(“已按价格从高到低排序”)。表格自己什么都不朗读。
  • 带有可排序列的表格,表头行在 regular 密度下至少高 44;用于触摸时,两种密度下都是 48。

选择行指引

  • 用户要同时对几条记录执行操作(删除、导出、移动)时,才让他们选择行。只针对一条记录的操作,放在它所在的行里。
  • 答案是一条记录时(套餐、地址、作为对比基准的版本),用选择一行(single)。不要用它来打开记录。
  • 在行上任何位置按下都会选择它。如果一行还要打开它的记录,请为此在行标题里放一个链接,而不是靠按下整行。
  • 在状态行里说明选中了多少行(“已选择 3 件,共 132 件”)。表格什么都不数。
  • 把对选中的行执行操作的按钮放在表格上方。按钮是你的;表格告诉你选中了什么。
  • “全部 132 件”要用你自己的控件来提供。表头行的复选框选择的是交给表格的那些行,绝不是其他页的行。
  • 每个复选框都是焦点顺序中的一个停靠点。一页的行数要控制在用户可以用 Tab 逐个走完的范围内,其余的分页。一个表格的单选按钮合起来只算一个停靠点。
  • 带有复选框或单选按钮的行在 regular 密度下至少高 44;用于触摸时,两种密度下都是 48。紧凑的行在使用指针时仍然紧凑。
  • Tag 和行内代码在你没有选择其他变体时都是 tonal 的;在默认系统中,tonal 的填充与选中行的填充几乎相同,所以在选中的行上看不出它们的形状。它们的文字仍然可读。在可以选择行的表格里,请使用 outlined 的 Tag。

加载指引

  • 首次加载用占位行,数量与你预计的行数大致相当。
  • 更换顺序或换页时保留原有的行。不要用占位行或加载指示器替换用户正在读的行。
  • 在状态行里写“正在加载”,结束后说明到达的内容(“第 1–20 件,共 132 件”)。
  • 清除 loading 时移除占位行。留下来的占位行仍然会被画出来,并在调试构建中报告。

没有行与状态行

  • 没有行的表格使用空状态,说明为什么没有行、接下来该做什么。回应搜索或筛选的空状态应设置 live,这样结果才会被朗读。
  • 状态行只用来说明表格现在显示的内容:共有多少行,显示的是其中哪些。对行执行操作的按钮应放在表格上方。

推荐

  • 给每个表格一个名称:人人都看得到的说明文字;如果上方的标题已经说明了它是什么,则用 label。
  • 只要第一列说明了每一行是什么,就把它写成行标题(<th scope="row"> 加 row-headers)。
  • 没什么可说的单元格就留空。如果“没有”本身就是信息(“不可用”),就把它写出来。

避免

  • 不要用表格给屏幕排版。
  • 不要指望靠条纹或颜色来传达含义。单元格里的状态应是一个词,或者是一个用标签文字写明这个状态的 Tag。
  • 不要为了填满空单元格而在里面放一个破折号。
  • 不要让行可以被按下,也不要在列标题里写你自己的排序标记或顺序。列标题只有按上面的 HTML 写法才变得可以按下,别无他法。行靠它的复选框或单选按钮来选择,可以打开的行则不属于表格。
  • 不要在表格加载期间把它变淡或禁用。

无障碍

角色
table
标注
The table is named by its caption, or by label where no caption is drawn (TBL-23). Header cells name their columns and, while rowHeaders is true, first cells name their rows; every cell is associated with both (TBL-24). While the table scrolls in its own box, that box is a region with the same name, so that a person who reaches it by keyboard hears what scrolls. A sortable column's button is named by the column's header, and the header cell carries the order (TBL-41). A row's checkbox or radio button is named by selectLabel, or by the row's header; the header row's checkbox by selectAllLabel (TBL-46). The radio buttons of a table are not wrapped in a radio group: each is exposed with its position among them (TBL-61). The status line is the product's sentence, told politely when it changes (TBL-52).
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab Moves focus to the table's box while the table scrolls in it; then to each sortable column's header button, in reading order; then, row by row, to the row's checkbox where rows can be chosen — where one row at most is chosen, once to the chosen row's radio button, or the first row's while none is — and to whatever in its cells takes focus. A table that does not scroll, sorts nothing and selects nothing is not a stop.
Enter / Space On a sortable column's header: asks for the rows in that column's order — ascending, or descending where they already ascend by it. Where the header is a link, Enter alone.
Space On a row's checkbox: chooses the row or lets it go. On the header row's checkbox: chooses every row, or lets every row go where all are chosen. On a row's radio button: chooses the row where it is not chosen; on the chosen row's, nothing.
Arrow keys On a row's radio button: move to the next row's radio button (Down, and Right in a left-to-right layout) or the previous one's, wrapping, and choose that row. While the box has focus: scroll it. Left and right move the view left and right in either layout direction.
Page Up / Page Down, Home / End While the box has focus: scroll it by a page, or to its beginning or end, as the platform scrolls a region.

备注

  • The table, its cells and every row that cannot be chosen take no input (TBL-16). What does — a sortable column's header, the checkboxes, a row that can be chosen (for a pointer), the scroll box — is a control, a larger target for one, or a region: a header's hit area is its whole cell and a checkbox's its whole selection cell, each at least the touch target high for touch at both densities (TBL-40, TBL-47); the compact density is lower than the pointer target, as a compact tree row is. What is pressable inside a cell (a link, a button) meets the minimum target by its own spec.
  • A table is for data that has rows and columns. Do not use it to place things side by side: assistive technology announces rows, columns and headers for every cell, which is noise around content that is not tabular. Use the layout.
  • Give every table a name (WCAG 1.3.1, 4.1.2): a caption that everyone sees is best; label is for a table whose heading already names it. A table that scrolls and has no name is a focus stop a person cannot tell from any other, so debug builds report it (TBL-23).
  • Header cells are structure, not style (WCAG 1.3.1). Every column has a header cell unless the table has no header row at all; rowHeaders is true whenever the first column says what each row is. A table with two levels of column headers, or cells that span columns or rows, is outside this spec: split it.
  • A scrolling box must be reachable and operable by keyboard (WCAG 2.1.1) and its focus must be visible (2.4.7): TBL-22. It is a stop only while it scrolls, so short tables add nothing to the focus order.
  • Reflow (WCAG 1.4.10) exempts data tables from two-dimensional scrolling, and the table uses that exemption carefully: only its own box scrolls sideways, never the page (TBL-17, TBL-29), and a first column that stays in view is released on a narrow window (TBL-20).
  • Meaning never rests on color: stripes are an aid to the eye and say nothing; a status in a cell is a word, or a tag whose label says it. An empty cell is exposed as empty (TBL-14); where the absence itself is information ("not available", "none"), write it in the cell.
  • Contrast in the default system: cell text is on-surface and header text on-surface-variant, each held to 4.5:1 on every surface and container step by the token invariants, the stripe included. The lines are decorative: rows, headers and the region are exposed without them.
  • Alignment end for numbers is for sighted comparison only; it changes nothing that is exposed. Text is never justified and never centred except in a column the product sets to center.
  • A table with checkboxes stays a table (TBL-46). The grid pattern — one stop for the whole table, arrow keys from cell to cell — is for a sheet whose cells are edited or acted on; it takes over the keys a screen reader uses to read a table, it would replace the roles of the product's own table on the web, and it cannot work without script. The price of staying a table is one Tab stop for each row's checkbox, which pagination keeps to a page of rows.
  • Where a person chooses one record — the plan to change to, the address to ship to, the build to compare against — selection is single, and the selection column holds a radio button, as the single selection of Carbon's data table and of Ant Design's table does: a radio says that the choices exclude each other, and that one is answered once it is (TBL-60). A radio is never cleared by its own activation (HTML, WAI-ARIA APG radio group), so the table offers no way back to none: a product that needs it gives a control of its own. The radio buttons share one name and so behave as one radio group — one Tab stop, the arrow keys move the choice (TBL-61) — which also keeps a page of rows to one stop where checkboxes would be one each. They are not wrapped in an element of the radio group role, which cannot hold the rows of a table; each radio is heard with its row's name, its state and its position ("Select Kettle, radio button, checked, 3 of 4"), inside the table's name. The grid alternative, rows that carry aria-selected (React Aria's table, single selection mode), is declined for the reasons above.
  • A press anywhere on a row that can be chosen presses its checkbox or radio button (TBL-58), as a press on a card or on a list item that can be chosen does: a larger target for a pointer (WCAG 2.5.8), not a second control. The row gets no role, no tab index and no selected state — ARIA allows aria-selected on a row only inside a grid or a treegrid — so that the keyboard and assistive technology meet the one control the row has, its named checkbox or radio button, and lose nothing. What a cell holds that answers a press (a link, a button, a field) keeps it, and a drag that selects text chooses nothing, so the text of a table can still be selected and copied.
  • Sorting is asked for and shown, never done, by the table (TBL-33, TBL-34): the header cell says the order (WCAG 1.3.1, 4.1.2) and the mark repeats it to the eye without relying on color (1.4.1). One column is sorted at a time.
  • A chosen row is said by its checkbox or radio button, checked; the fill is the same fact for the eye and is not relied on (WCAG 1.4.1), which is also what is left in forced colors (TBL-54). In the default system the text of a chosen row is on-secondary-container on a layer of that color over the surface, the pair a selected list item and a selected tree row are drawn in, held to 4.5:1 by the token invariants, and the fill stands 1.1:1 at least from a hovered row; the contrast of what a cell holds beside plain text on that fill is its own component's.
  • Status messages (WCAG 4.1.3): the table announces nothing itself (TBL-25) and gives the product one polite place to say what changed — rows found, rows chosen, loading and loaded (TBL-52). An Empty state in the empty slot that answers a search or a filter is live by its own spec.
  • While loading, the table is busy and placeholder rows are hidden (TBL-49, TBL-50), so that a person does not walk through rows of nothing. Do not take the rows a person was reading away for a spinner where they can stay.
  • Focus is never lost to a change of rows (TBL-25) and a focused control is never left under a part kept in view (TBL-36; WCAG 2.4.11).

生成自 specs/table.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 原生表格始终是表格:行、列标题、行标题和单元格都是浏览器自己的,无论有没有条纹、采用哪种密度、是否正在滚动、是否固定了某一列或表头。
  • 说明文字为表格命名。没有说明文字时,label 成为表格的 aria-label,不绘制。两者都有时,名称是说明文字。
  • 表格在自己的框内滚动时,这个框是焦点顺序中的一个停靠点,排在单元格里的任何东西之前,同时也是一个以表格名称命名的区域。方向键、Page Up、Page Down、Home 和 End 可以滚动它。不滚动的表格不增加停靠点。
  • 焦点环画在框的边缘以内,所以不会被裁掉。
  • 如果表格每行的第一个单元格是普通的 <td>(渲染出来的 Markdown),并且设置了 row-headers,元素会给它们加上行标题角色。
  • 在从右到左的布局中,各列从右侧排起,滚动从右侧开始,固定的第一列位于右侧,每个单元格的文本保持自己的方向:数字绝不会被颠倒。
  • 文本放大到 200% 时,列宽会重新计算;放不下的地方,表格在自己的框内滚动。
  • 在强制颜色模式下,每条线都是文本颜色;带条纹的表格无论采用哪种分隔方式都会画出行线,因为这个模式会去掉填充。
  • 可排序列的列标题单元格是带有顺序(aria-sort)的列标题,其中你的按钮以列标题的文字命名。标记对辅助技术隐藏。
  • 排序按钮是焦点顺序中的一个停靠点。Enter 和 Space 按下它;它是链接时,只有 Enter。按下之后焦点仍留在它上面。如果你把表头行连同各行一起替换了,焦点会移到同一列的新按钮上。
  • 排序按钮的焦点指示画在它的列标题单元格内部;用键盘聚焦的按钮如果被固定的第一列或状态行挡住,会被带到看得见的地方。
  • 空状态所在的那一行是只有一个单元格的一行。表格不给它添加任何角色和文字。
  • 状态行的段落是一个状态:它的文本变化时会以礼貌(polite)的方式朗读,不移动焦点;首次绘制时什么都不朗读。
  • 在强制颜色模式下,排序按钮的标签和标记采用按钮的文本颜色,已排序的列只靠它的箭头来表明。
  • 没有脚本时,排序按钮、它们的标记、空状态所在的那一行和状态行照样绘制,排序链接也能工作。sort 事件需要脚本。
  • 可以选择行的表格仍然是表格。它不获得 grid 角色,行不获得选中状态,也没有任何按键把焦点从一个单元格移到另一个单元格。行的复选框或单选按钮的选中状态说明这一行已被选中。
  • 表头行的复选框会说明自己是选中还是未选中,半选时则说明是混合;没有可以选择的行时是禁用的。
  • 每个复选框都是焦点顺序中单独的一个停靠点:先是表头行的复选框和排序按钮,然后是每一行的复选框。Space 选中或取消选中它。
  • 行的复选框或单选按钮没有自己的名称时,以这一行的行标题单元格命名。
  • 在 single 下,单选按钮合起来是一个停靠点:Tab 停在已选中行的单选按钮上(一行都没选中时停在第一行的上),再按一次 Tab 就离开表格。方向键移到下一行或上一行的单选按钮并选中那一行,到头后从另一端继续。每个单选按钮会连同行的名称、状态以及它在其中的位置一起读出,例如“选择团队版,单选按钮,已选中,第 2 个,共 3 个”。它们不包在单选组里,因为单选组无法容纳表格的行。
  • 在选择单元格里的任何位置按下,都是按下其中的复选框。某个复选框有焦点时,如果你替换了各行或重新排列了各行,焦点会移到 data-value 相同的那一行的复选框上。
  • 对指针来说,可以选择的行是其复选框的更大的目标。行不会得到角色、焦点停靠点或选中状态,所以键盘和屏幕阅读器面对的仍是那个已命名的复选框,行里的文本也仍然可以选择和复制。
  • 在强制颜色模式下,选中行的填充消失,复选框或单选按钮仍然说明这一行已被选中。
  • 设置了 loading 期间,表格以忙碌状态暴露给辅助技术(aria-busy)。占位行对辅助技术隐藏,所以用户听到的是表格正忙,而不是一行行空无一物的内容。加载开始或结束时,表格自己什么都不朗读。
  • 没有脚本时,选择列、选中的行(data-selected,或者已选中的原生复选框)和占位行照样绘制,原生复选框也能在你的表单里工作。让表头行的复选框保持一致,以及 selectionchange 事件,都需要脚本。没有脚本时,行不显示悬停状态和指针光标,在复选框旁边按下也不会改变任何东西。
  • 没有名称的表格、未知的密度、分隔方式或选择方式、写在表体之前的表尾行、单元格数多于或少于列数的行、没有列标题的可排序列、同时排序的两列、没有名称的行复选框、同时被标为选中的多行、single 下种类不对的控件,以及不在加载中的表格里的占位行,会在调试构建中报告。

需要你做的事

  • 给表格命名。会滚动却没有名称的表格是一个焦点停靠点,用户无法把它与别的停靠点区分开。
  • 给每一列一个列标题单元格,并把行标题标记为行标题。列标题和行标题是结构,不是样式。
  • 需要两级列标题或合并单元格的表格,请尽量保持简单。它们按 HTML 的方式工作,但这个组件没有任何部分是围绕它们设计的;拆分表格通常更清楚。
  • 单元格里可以按下的东西(链接、按钮)保留自己的名称、焦点和目标尺寸。请给它留出空间。
  • 为各行排序之后,在列标题单元格上写上新的 aria-sort:其中一个写 ascending 或 descending,其他可排序的列写 none。不要把这个属性留在没有按钮的列标题上。
  • 状态行的段落没有文本时,也要把它留在页面里,这样你下一次给它的文本才会被朗读。
  • 用 select-all-label 为表头行的复选框命名;表格没有行标题时,用各行复选框的 label 为它们命名。
  • 加载开始和结束时,都在状态行里说明。这是这一变化唯一会被朗读的地方。

文字

Tanrei 不自带任何文字。表格只向你要其中一段,即表头行复选框的名称:元素上的 select-all-label,或者用 setWords() 给出的 selectAllLabel。另外两段由你写在自己的 HTML 里。

文字 在哪里给出
表头行复选框的名称(“选择所有商品”) 元素上的 select-all-label,或者用 setWords({ selectAllLabel: '选择所有行' }) 为所有表格只给一次。
行的复选框或单选按钮的名称(“选择手摇磨豆机”) 这一行的 <tanrei-checkbox> 或 <tanrei-radio-button> 上的 label,或者原生控件上的 aria-label。有行标题时可以不写:行标题会为复选框命名。
状态行的句子(“已选择 2 件,共 4 件”“正在加载”) 状态行的段落;你不写段落时,用元素上的 status。

没有为表头行的复选框命名时,它照样绘制、照样能用,但没有无障碍名称,调试构建会报告缺少这段文字。