跳到正文
册页

数据

Table - 表格

一张数据表——对齐、排序、边框都可以按列配置。

用法

什么时候用它

对齐按列设置,数字靠尾边,这样每一位才能对齐。排序按列开启:每个表头都是按钮,等于在邀请读者去排一个数据本来就排不了的列。
TSX
import { Table } from '@misoto22/folio'

说明

一张有线的数据表。

包在它自己的横向滚动容器里,因为表格是唯一一种正当地超出版心的块——整页横向滚动是布局 bug,而表格横向滚动就是表格。

那个容器是可聚焦的,而且必须如此。一个内容本身不可聚焦的滚动区域,键盘是够不着的:没有元素可以 Tab 过去,也就没法对它按方向键,于是对不用鼠标的人来说,折线之外的那些列压根不存在。

它同时也是定位过的,这不是装饰。sr-only 是 position: absolute,所以单元格里一个视觉隐藏的标签,是相对最近的那个定位祖先来解析的——而在它和文档之间一个定位祖先都没有时,它会逃出滚动容器、逃出外面每一层 overflow-hidden,并且按表格当前恰好滚了多远,把页面撑宽那么多。

任何边框设置下都没有斑马纹。在单色系统里,一条带底色的行是在和页面底色竞争的第二块表面,而行与行之间的细线已经足够让人横着追一行了。

结构

Table anatomy
部件说明
滚动区域必填包在表格外面、可聚焦的那个 <div role="region">,名字来自 caption。边框设置和密度属性都长在它身上,横向滚动的也是它——所以越出行宽的是表格,不是页面。它同时还是定位过的,正是这一点让单元格里那个 sr-only 的标签不会相对文档去定位,把整个页面横向拖宽。
表名必填一个真正的 <caption>,默认视觉隐藏,除非 showCaption 把它当眉题印在表格上方。同一个字符串也是滚动区域的可访问名称,所以进去时听到一次,从表格那边再听到一次。
列标题必填TH——等宽大写,所以它永远不会被读成数据;对齐按列设置;除非调用处覆盖,否则一律输出 scope="col"。
排序控件只出现在可排序的表头上:th 里面的一个 <button>,标签旁边是 ArrowUp、ArrowDown 或者一个压暗的 ChevronsUpDown,th 上的 aria-sort 由 sortDirection 设置。
单元格必填TD——顶对齐、--ink-2,和表头共用 --table-pad-x,所以列能对齐;行高来自 --table-pad-y,density 会把它从 14px 减半到 8px。

实践建议

推荐

  • 用 stickyHeader 就要从外面把高度框住:className 和其余每一个属性都落在 <table> 上,不是落在外面那个滚动的 div 上,所以只有一个真会施加约束的父容器——一列有高度的 flex——才能给那个 div 一个可粘的范围。普通包装元素上的 max-height 做不到,表头只会跟着页面一起走。
  • 行首那个单元格要传 scope="row"——TH 先写 scope="col",你的属性是在这之后展开的,所以覆盖得上;不传的话,每个行头都在声称自己领着一列,顺着单元格回溯就会走到错的标签上。
  • 排序换列时,其他列要重置成 sortDirection="none":每个表头各带各的 aria-sort,没有任何东西在协调它们,所以一张表可能同时播报两列都已排序。
  • TD 要和它的 TH 用同一个 align——对齐是按单元格设的,不会沿着列继承下来,而贴尾边的数字配上贴首边的标签,就是一列数字和自己的表头对不齐。

避免

  • aria-sort 是在读者走到表头时才告诉他这张表是怎么排的;按下按钮那一刻它什么都不播报。一张在读屏用户眼皮底下重排的表,必须在读者当下所在的位置把这件事说出来,否则就是每一行都悄悄换了、而没有一句交代。
  • 不要指望它在手机上重排:没有任何东西会堆叠,区域是在一条细线滚动条后面横向滚动,而折线之外的列,只有想明白它会滚的读者才够得到。375px 上的八列要的是另一种呈现方式,不是更小的字号。

示例

可排序的列

排序是逐列选择开启的,因为一张每个表头都是按钮的表格,会招人去排一列数据根本没法排序的东西。一个可排序的表头会在 th 里面渲染一个真的 button——单元格上的点击处理器不可聚焦、也不会被播报,那样排序就只对鼠标存在、对别的什么都不存在——并且根据 sortDirection 设置 aria-sort,而那是读屏软件得知这张表被排过序的唯一途径。每个表头各带各的 aria-sort,没有任何东西在协调它们,所以排序换列时,记得把其余的重置成 none。caption 是必填的,且默认隐藏:它既命名这张表,也命名那个滚动区域。

Recent deploys
CommitBranchState
a1b2c3dmain2m 14spassed
9f8e7d6codex/ui-library2m 41spassed
4c5b6a7codex/photo-cache1m 02sfailed
77aa2b1main2m 20spassed

边框

四种边框设置用在同样两行上,于是变的只有那些线。rows 是默认值,也是沿着一列往下读时的正确答案:记录之间一条细线,没有任何东西跟眼睛正在追踪的那条线抢。grid 加上一张密集数字表所需要的竖线;bordered 在整体外面画一圈边,给那种散落在页面上、而不是待在一张本来就有边框的卡片里的表格用。任何一档设置下都没有斑马纹——在一套单色系统里,一条带底色的行是第二片表面,在跟页面的底争。density 用 compact 会把行内边距从 14px 减半到 8px,而那正是一张几乎全是数字的表格想要的。

rows — the default — read down a column

rows example
RegionOrdersRevenue
Australia1,204$48,210
Japan862$31,940

grid — dense numbers, columns kept apart

grid example
RegionOrdersRevenue
Australia1,204$48,210
Japan862$31,940

bordered — loose on a page, not inside a card

bordered example
RegionOrdersRevenue
Australia1,204$48,210
Japan862$31,940

bordered-grid — both

bordered-grid example
RegionOrdersRevenue
Australia1,204$48,210
Japan862$31,940

吸顶表头

stickyHeader 需要一个有界高度,而那个容器并不归这个组件所有。每一个属性,包括 className,都落在 table 元素本身上——不是落在包着它的那个会滚动的 div 上——所以只有一个施加约束的父元素,才能给那个 div 一个可以粘住的范围。这就是行得通的那种排布:一根定高的 flex 列。滚动区域既是一个 flex 项、又是一个滚动容器,所以它的自动最小尺寸是零,会缩到给它的那个高度。在一个普通块级容器上写 max-height 做不到这件事,而表头就只会跟着别的一切一起往页面上方跑掉。

Every run on this branch
CommitBranchDuration
9a2f41cmain0m 48s
9a3130bcodex/step-11m 25s
9a331facodex/step-22m 02s
9a350e9codex/step-32m 39s
9a36fd8main0m 56s
9a38ec7codex/step-51m 33s
9a3adb6codex/step-62m 10s
9a3cca5codex/step-72m 47s
9a3eb94main1m 04s
9a40a83codex/step-91m 41s
9a42972codex/step-102m 18s
9a44861codex/step-112m 55s
9a46750main1m 12s
9a4863fcodex/step-131m 49s
9a4a52ecodex/step-142m 26s
9a4c41dcodex/step-153m 03s
9a4e30cmain1m 20s
9a501fbcodex/step-171m 57s
9a520eacodex/step-182m 34s
9a53fd9codex/step-190m 51s
9a55ec8main1m 28s
9a57db7codex/step-212m 05s
9a59ca6codex/step-222m 42s
9a5bb95codex/step-230m 59s

七列

七列、印出来的 caption,以及行表头。showCaption 会把那个隐藏的 caption 变成表格上方的引题;无论哪种,同一个字符串都是滚动区域的无障碍名称,所以进去的路上会听到一次,从表格那儿又会听到一次。在每一行的第一个单元格上传 scope="row"——TH 写的是 scope="col",而你的属性是在它之后摊开的,所以这个覆盖会生效;没有它,每个行表头都在宣称自己是一列的表头。这里没有任何东西会在手机上重排:这个区域在一条细线栏后面横向滚动,而首屏之外的那些列,只有想明白了它会滚的读者才够得着。把这块画布收窄看看。375px 下的七列需要的是另一种呈现方式,不是更小的字号。

Fleet at a glance
HostRegionVersionCPUMemoryp95Uptime
apiap-southeast-20.4.012%1.4 GB84 ms31d
webap-southeast-20.4.07%0.9 GB41 ms31d
workerap-northeast-10.3.138%2.1 GB—6d

组成部分

在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。

THead

表头分组。它下面那条线由表格的边框设置决定。

这个组件没有自己的属性。

TBody

这个组件没有自己的属性。

TR

这个组件没有自己的属性。

TH

一个列标签。等宽而且大写,所以它绝不会被读成数据。

可排序时,成为 <button> 的是 <th> 里面的那个标签,而不是让 <th> 自己可点:一个挂了点击处理器的单元格既不可聚焦,也不会被播报成控件,于是这个排序只对鼠标存在。

排序是按列的,而且要显式开启。一个每个表头都是按钮的表格,是在邀请读者去排一列数据根本没法排序的东西。

TH props
属性类型默认值说明
alignTableAlign'start'这一列的内容靠哪一边。数字属于 `end`。
onSort() => void—
sortablebooleanfalse把标签变成按钮,并显示排序标记。
sortDirectionSortDirection'none'这一列按哪个方向排。同时设为 `aria-sort`,那是读屏软件得知“这张表是排过序的”的唯一途径——表头里画一个小三角对它什么都没说。

同时接受 Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。

TD

一个表体单元格。

顶对齐,而这是在挑「哪一种表格在这里更常见」:一个装着段落的单元格。顶对齐才能让一行里各列的首行彼此对齐,而把两个词的术语对着四行描述居中,是把那个术语摆在一片虚无的正中间。

对另一种表格它就是错的默认——一行单行的值,中间夹着一个控件。一个 36px 的按钮挨着 16px 的文字,行就是 52px,于是除了那个按钮以外的每一格都吊在行顶,底下空着二十个像素,读起来像某一列滑掉了。那种行请传 className="align-middle";数据表模板就是这么做的。

TD props
属性类型默认值说明
alignTableAlign'start'这个单元格的内容靠哪一边。和该列的表头保持一致。

同时接受 Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。

类型

TSX
export type TableAlign = 'start' | 'center' | 'end'
export type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'
export type SortDirection = 'ascending' | 'descending' | 'none'

键盘操作

Table keyboard interactions
按键作用
Tab走到滚动区域,以及每一个可排序的表头。
←→区域拿到焦点后,横向滚动表格。

无障碍

  • caption 必填:一个页面上三张表,其中没有名字的那张是没法导航的。
  • 列标题是 <th scope="col">,所以一个单元格能被追溯回它的表头。
  • 可排序的表头是 th 里面的 button,而不是给单元格挂 onClick——带 onClick 的单元格既不可聚焦也不会被播报,那个排序就只对鼠标存在。
  • aria-sort 由 sortDirection 设置,这是读屏用户得知这张表已被排序的唯一途径。
  • 任何边框设置下都没有斑马纹:在单色系统里,一条被染色的行是又一个和页面底色抢注意力的表面。
  • 滚动区域是一个包含块,所以单元格里视觉隐藏的标签会留在表格内部,而不是跑出去把页面撑宽。