数据
Table - 表格
一张数据表——对齐、排序、边框都可以按列配置。
用法
什么时候用它
import { Table } from '@misoto22/folio'说明
一张有线的数据表。
包在它自己的横向滚动容器里,因为表格是唯一一种正当地超出版心的块——整页横向滚动是布局 bug,而表格横向滚动就是表格。
那个容器是可聚焦的,而且必须如此。一个内容本身不可聚焦的滚动区域,键盘是够不着的:没有元素可以 Tab 过去,也就没法对它按方向键,于是对不用鼠标的人来说,折线之外的那些列压根不存在。
它同时也是定位过的,这不是装饰。sr-only 是 position: absolute,所以单元格里一个视觉隐藏的标签,是相对最近的那个定位祖先来解析的——而在它和文档之间一个定位祖先都没有时,它会逃出滚动容器、逃出外面每一层 overflow-hidden,并且按表格当前恰好滚了多远,把页面撑宽那么多。
任何边框设置下都没有斑马纹。在单色系统里,一条带底色的行是在和页面底色竞争的第二块表面,而行与行之间的细线已经足够让人横着追一行了。
结构
| 部件 | 说明 |
|---|---|
| 滚动区域必填 | 包在表格外面、可聚焦的那个 <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 是必填的,且默认隐藏:它既命名这张表,也命名那个滚动区域。
| Commit | Branch | State | |
|---|---|---|---|
| a1b2c3d | main | 2m 14s | passed |
| 9f8e7d6 | codex/ui-library | 2m 41s | passed |
| 4c5b6a7 | codex/photo-cache | 1m 02s | failed |
| 77aa2b1 | main | 2m 20s | passed |
边框
四种边框设置用在同样两行上,于是变的只有那些线。rows 是默认值,也是沿着一列往下读时的正确答案:记录之间一条细线,没有任何东西跟眼睛正在追踪的那条线抢。grid 加上一张密集数字表所需要的竖线;bordered 在整体外面画一圈边,给那种散落在页面上、而不是待在一张本来就有边框的卡片里的表格用。任何一档设置下都没有斑马纹——在一套单色系统里,一条带底色的行是第二片表面,在跟页面的底争。density 用 compact 会把行内边距从 14px 减半到 8px,而那正是一张几乎全是数字的表格想要的。
rows — the default — read down a column
| Region | Orders | Revenue |
|---|---|---|
| Australia | 1,204 | $48,210 |
| Japan | 862 | $31,940 |
grid — dense numbers, columns kept apart
| Region | Orders | Revenue |
|---|---|---|
| Australia | 1,204 | $48,210 |
| Japan | 862 | $31,940 |
bordered — loose on a page, not inside a card
| Region | Orders | Revenue |
|---|---|---|
| Australia | 1,204 | $48,210 |
| Japan | 862 | $31,940 |
bordered-grid — both
| Region | Orders | Revenue |
|---|---|---|
| Australia | 1,204 | $48,210 |
| Japan | 862 | $31,940 |
吸顶表头
stickyHeader 需要一个有界高度,而那个容器并不归这个组件所有。每一个属性,包括 className,都落在 table 元素本身上——不是落在包着它的那个会滚动的 div 上——所以只有一个施加约束的父元素,才能给那个 div 一个可以粘住的范围。这就是行得通的那种排布:一根定高的 flex 列。滚动区域既是一个 flex 项、又是一个滚动容器,所以它的自动最小尺寸是零,会缩到给它的那个高度。在一个普通块级容器上写 max-height 做不到这件事,而表头就只会跟着别的一切一起往页面上方跑掉。
| Commit | Branch | Duration |
|---|---|---|
| 9a2f41c | main | 0m 48s |
| 9a3130b | codex/step-1 | 1m 25s |
| 9a331fa | codex/step-2 | 2m 02s |
| 9a350e9 | codex/step-3 | 2m 39s |
| 9a36fd8 | main | 0m 56s |
| 9a38ec7 | codex/step-5 | 1m 33s |
| 9a3adb6 | codex/step-6 | 2m 10s |
| 9a3cca5 | codex/step-7 | 2m 47s |
| 9a3eb94 | main | 1m 04s |
| 9a40a83 | codex/step-9 | 1m 41s |
| 9a42972 | codex/step-10 | 2m 18s |
| 9a44861 | codex/step-11 | 2m 55s |
| 9a46750 | main | 1m 12s |
| 9a4863f | codex/step-13 | 1m 49s |
| 9a4a52e | codex/step-14 | 2m 26s |
| 9a4c41d | codex/step-15 | 3m 03s |
| 9a4e30c | main | 1m 20s |
| 9a501fb | codex/step-17 | 1m 57s |
| 9a520ea | codex/step-18 | 2m 34s |
| 9a53fd9 | codex/step-19 | 0m 51s |
| 9a55ec8 | main | 1m 28s |
| 9a57db7 | codex/step-21 | 2m 05s |
| 9a59ca6 | codex/step-22 | 2m 42s |
| 9a5bb95 | codex/step-23 | 0m 59s |
七列
七列、印出来的 caption,以及行表头。showCaption 会把那个隐藏的 caption 变成表格上方的引题;无论哪种,同一个字符串都是滚动区域的无障碍名称,所以进去的路上会听到一次,从表格那儿又会听到一次。在每一行的第一个单元格上传 scope="row"——TH 写的是 scope="col",而你的属性是在它之后摊开的,所以这个覆盖会生效;没有它,每个行表头都在宣称自己是一列的表头。这里没有任何东西会在手机上重排:这个区域在一条细线栏后面横向滚动,而首屏之外的那些列,只有想明白了它会滚的读者才够得着。把这块画布收窄看看。375px 下的七列需要的是另一种呈现方式,不是更小的字号。
| Host | Region | Version | CPU | Memory | p95 | Uptime |
|---|---|---|---|---|---|---|
| api | ap-southeast-2 | 0.4.0 | 12% | 1.4 GB | 84 ms | 31d |
| web | ap-southeast-2 | 0.4.0 | 7% | 0.9 GB | 41 ms | 31d |
| worker | ap-northeast-1 | 0.3.1 | 38% | 2.1 GB | — | 6d |
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
THead
表头分组。它下面那条线由表格的边框设置决定。
这个组件没有自己的属性。
TBody
这个组件没有自己的属性。
TR
这个组件没有自己的属性。
TH
一个列标签。等宽而且大写,所以它绝不会被读成数据。
可排序时,成为 <button> 的是 <th> 里面的那个标签,而不是让 <th> 自己可点:一个挂了点击处理器的单元格既不可聚焦,也不会被播报成控件,于是这个排序只对鼠标存在。
排序是按列的,而且要显式开启。一个每个表头都是按钮的表格,是在邀请读者去排一列数据根本没法排序的东西。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| align | TableAlign | 'start' | 这一列的内容靠哪一边。数字属于 `end`。 |
| onSort | () => void | — | |
| sortable | boolean | false | 把标签变成按钮,并显示排序标记。 |
| sortDirection | SortDirection | 'none' | 这一列按哪个方向排。同时设为 `aria-sort`,那是读屏软件得知“这张表是排过序的”的唯一途径——表头里画一个小三角对它什么都没说。 |
同时接受 Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
TD
一个表体单元格。
顶对齐,而这是在挑「哪一种表格在这里更常见」:一个装着段落的单元格。顶对齐才能让一行里各列的首行彼此对齐,而把两个词的术语对着四行描述居中,是把那个术语摆在一片虚无的正中间。
对另一种表格它就是错的默认——一行单行的值,中间夹着一个控件。一个 36px 的按钮挨着 16px 的文字,行就是 52px,于是除了那个按钮以外的每一格都吊在行顶,底下空着二十个像素,读起来像某一列滑掉了。那种行请传 className="align-middle";数据表模板就是这么做的。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| align | TableAlign | 'start' | 这个单元格的内容靠哪一边。和该列的表头保持一致。 |
同时接受 Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
类型
export type TableAlign = 'start' | 'center' | 'end'
export type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'
export type SortDirection = 'ascending' | 'descending' | 'none'键盘操作
| 按键 | 作用 |
|---|---|
| Tab | 走到滚动区域,以及每一个可排序的表头。 |
| ←→ | 区域拿到焦点后,横向滚动表格。 |
无障碍
- caption 必填:一个页面上三张表,其中没有名字的那张是没法导航的。
- 列标题是 <th scope="col">,所以一个单元格能被追溯回它的表头。
- 可排序的表头是 th 里面的 button,而不是给单元格挂 onClick——带 onClick 的单元格既不可聚焦也不会被播报,那个排序就只对鼠标存在。
- aria-sort 由 sortDirection 设置,这是读屏用户得知这张表已被排序的唯一途径。
- 任何边框设置下都没有斑马纹:在单色系统里,一条被染色的行是又一个和页面底色抢注意力的表面。
- 滚动区域是一个包含块,所以单元格里视觉隐藏的标签会留在表格内部,而不是跑出去把页面撑宽。