展示
Tag - 标签
主题标签——一个话题、一项技术、一个筛选面——用 onClick 做筛选,用 onRemove 关掉。
用法
什么时候用它
import { Tag } from '@misoto22/folio'说明
一个主题标签——一个话题、一项技术、一个筛选维度——可以用 onClick 去做筛选,也可以用 onRemove 被关掉。
和 Badge 不同,后者承载的是状态或计数。标签说的是某个东西关于什么,所以好几个会挨在一行里让读者扫;而徽章是关于某一条记录的某一个事实。
这里本来会有一个 Token 组件。它没有被造出来:token 就是一个带移除按钮的 tag,两者之间差的是一个 prop,不是一个组件。这套系统已经发了三个长得像的东西——Badge、Tag、StatusPill——再来第四个、而它全部的区别只是尾巴上多一个 X,那就成了调用处靠掷硬币来挑的那一个。
在拿到一个 handler 之前是纯展示的;而两种可交互的情形都由这个组件自己拥有,不再把其中一种丢给调用处的一层包裹。这不是图省事:给一个已经装着移除 <button> 的 chip 再套一层 <button>,就是按钮套按钮——没有哪个解析器会原样保留它,也没有哪棵无障碍树会按写下来的样子把它报出去。两个都给了,chip 会把标签和那个 X 渲染成并列的两个按钮——标签把前面那段内边距一起带走,所以它是从头一直到 X 的整个 chip,而不是一串字外面裹着一圈点不动的空白。
结构
| 部件 | 说明 |
|---|---|
| 标记盒必填 | 那个 <span>:和 Badge 同一个 --radius-sm 圆角、同一份 10px × 4px 内边距、同一种 12px 等宽字,但它自己没有边框。 |
| 底色必填 | 静止时是 --stone,active 之后是 --accent,按 --duration-fast 交叉淡入。accent 是这套系统唯一指向“某个选择”的记号,所以选中就画在它身上。 |
| 标签文字必填 | children,用 --ink-3-aa——那是 AA 的下限,不是某种浅灰——active 之后换成 --accent-foreground。 |
| 移除按钮 | 只在传了 onRemove 时出现:标签后面一个真正的 <button type="button">,里面是 12px 的 X,名字由 removeLabel 给。画出来的盒子只有 16px,低于 WCAG 2.5.8 的 24px 下限,所以一个内缩的伪元素把命中区域撑到 24——画面不变,标记之间也不会被撑开。 |
| 筛选控件 | 只在传了 onClick 时出现:标记盒本身变成那个 <button>,并把 active 带成 aria-pressed。同时给了 onRemove 时,标签会拆成 X 旁边它自己的按钮——两个兄弟节点,绝不会一个套进另一个——并且把前导内边距一并带走,所以点击目标是一直到 X 为止的整块标记,而不只是那几个字。 |
实践建议
推荐
- 做筛选就用 onClick,不要在外面包一层。标记盒本身就变成那个按钮,所以内边距是点击目标的一部分,焦点圈画在读者看得见的那个形状上——可移除的标记也不会落成一个按钮套着另一个按钮。
- 会切换的标记要传 active,不会切换的就别传。aria-pressed 读的正是同一个值,所以 accent 的填充和辅助技术听到的状态不可能各说各话;不传就什么都不播报,而这对一个只做跳转、不做切换的标记来说恰好是对的。
- 筛选栏要留一个能回去的关闭态——active 就是那抹 accent,而一排全都 active 的 tag,把“就是这一个”这个记号平摊给了所有人。
- removeLabel 里要把对象说出来——「移除 Rust 筛选」,不是「移除」。它和 onRemove 一起是必填的,因为八个标记的控件全叫 Remove,就是八个读屏软件分不清的控件。
避免
- 它根本没有 tone,所以一个 tag 说不出成功或危险。用 className 给它上色,是往系统里手工塞进一个色相,而“被选中”这件事,就只能指望 accent 一个人还读得出来。
- 孤零零一个 tag,是一个丢了 tone 的 Badge。这个组件是为一排一排地扫读而造的,而一条记录旁边的单个标记,说的是关于这条记录的一个事实。
- 别自己在外面套一个 button。套在可移除的标记外面就是按钮里嵌按钮——这是非法标记,解析器会把它拆成兄弟节点,留下一份作者和无障碍树都没预料到的 DOM。那一层包装本来要做的事,就是 onClick。
示例
default
一行分面,其中一个被选中了。accent 的填充是这套系统唯一那个指向「所选」的手段,所以一行需要一个可以退回去的关闭态——每个标签都激活,等于把那个意思是「就是这个」的记号花在了全部身上。这里没有任何东西可点:没有 onRemove 时,Tag 就是一个普通的 span,而按钮、焦点环,以及那个让选中状态可被听见的 aria-pressed,都属于调用处,下一个例子会展示。
a filter row
同一行,做筛选。按钮在标签外面:那个 span 自己带着 10px 的内边距,所以嵌在它里面的按钮,是一个只有文字那么大的目标,外面围着一圈死掉的、却看起来仍然可点的边距。aria-pressed 要和 active 同一口气设上,因为 active 只负责上 accent 那个色,此外再没有别的途径够到辅助技术——没有它,被选中的那个分面就成了一个只有看得见的读者才能看见的状态。
tag badge or pill
三枚长得几乎一模一样的芯片,摆在同一条记录上,各自都有它是正确答案的那一处。Tag 命名的是这条记录关于什么,所以好几个会一起坐着、供人扫读;Badge 是关于某一条记录的一个事实,这也是为什么它承载状态色而标签一个都不承载;StatusPill 是视图自身的实时状态,最多一枚,用的是这套系统里最响的小字号。当你会想在它旁边再放一个的时候,就拿标签。
a removable chip
可移除的那种形态:给读者已经选好的东西用的芯片。这里本来是一个独立的 Token 组件会去的位置,而两者的差别是一个属性,不是一个组件。removeLabel 必须和 onRemove 一起给,而不是默认成「移除」,因为八个都叫「移除」的控件,是八个读屏软件分不出彼此的控件——把主语写进去。
无障碍
- 没给处理函数之前都是纯展示。onClick 会把标记盒变成一个真正的 button,并把 active 带成 aria-pressed,所以焦点圈和按下状态都留在真正画出它们的那个元素上。
- 两个控件都是真正的 <button type="button">,而且是兄弟节点,所以 Tab 各自走得到,Enter 和 Space 各自都能触发,两个都不会去提交碰巧套着它们的那个表单。
- 传了 onRemove 就必须传 removeLabel,它是这个按钮全部的可访问名称——那个 X 本身是 aria-hidden 的。