无障碍与键盘规格
无障碍在 XiHan.UI 里不是「尽量做到」,它是判据:每个组件都有一份机读的键盘规格表,它同时是测试的分母。
键盘规格表
export const accordionKeyboard: KeyboardTable = {
component: 'accordion',
source: 'https://www.w3.org/WAI/ARIA/apg/patterns/accordion/#keyboardinteraction',
rows: [
{
id: 'accordion.kbd.toggle',
keys: ['Space', 'Enter'],
when: 'focus in trigger, not disabled',
does: '展开/收起该条目的 content',
},
{
id: 'accordion.kbd.next',
keys: ['ArrowDown', 'ArrowRight'],
when: 'focus in trigger, 按键与 orientation 同轴(dir=rtl 时左右键语义互换)',
does: '焦点移到下一个 trigger,末条不回绕',
},
// …
],
}每一行有稳定的 id、触发按键、生效前置条件、以及效果。全库共 375 条,散落在 102 个组件上。
规格表由三方共同消费:
- 测试——一致性用例通过
covers字段反查行 id,缺一行即套件失败; - 文档——组件参考里每个组件的键盘表就是它渲染出来的;
- 校验——
source字段指向规格出处(W3C APG 模式、WAI-ARIA、HTML 标准或 WCAG 技术),改行为时能追回依据。
分母外化
想让键盘测试通过,只能去写用例,不能去改分母。规格表是先立的契约,不是事后补的记录。
ARIA 上的几处刻意选择
读组件源码时会反复见到这几种处理,它们都是权衡过的:
aria-disabled 而非 disabled——当禁用项仍需可聚焦时用它。原生 disabled 会让元素丢掉焦点,读屏用户就再也 Tab 不到它,也读不到「为什么不能点」。手风琴的禁用条目、加载中的按钮都走这条。
该不做 roving tabindex 的地方就不做——手风琴的每个触发器都是独立的 Tab 停靠点,这是 APG 对该模式的规定。组件不输出 tabindex 是刻意的,不是遗漏。
方向键的轴向与书写方向——垂直列表不响应左右键;RTL 下左右键语义互换。这两件事收在 navIntentFromKey 一处,所有列表型组件共享同一份判断。
不归导航管的按键绝不 preventDefault——否则会吃掉输入法、浏览器快捷键和默认行为。
输入法组合期不误判——isComposingEvent() 用于识别输入法组合中的按键,避免把候选词确认的 Enter 当成提交。
背景失活与焦点
模态浮层打开时按固定顺序装配四件事:
- 注册层,压入层栈;
- 建消隐层(Esc、点外面、焦点移出);
- 建焦点域(陷住 + 回绕 + 归还);
- 锁滚动;
- 推迟一帧给背景加
inert。
顺序不能乱,细节见行为原语。
浏览器里的自动扫描
一致性套件的 fixture 会被挂进真实 Chromium,对初始态与各用例终态跑 axe。终态按形态签名去重,避免同一形态重复扫。
必须在真浏览器里跑:jsdom 没有布局,对比度、目标尺寸、翻面与避让一概演不出来。
pnpm exec playwright install chromium # 首次
pnpm test:browser存量违规登记表
扫出的存量问题登记在 tooling/testing/src/a11y/known.ts 里。命中已登记的规则不判失败,但一条都不再命中时判登记过期——修好了就必须从表里删掉,不许留着一份早已不成立的豁免。
两个适配器共用的那张表已经清空,全局登记也是空的。当前只剩 Web Components 适配器自己的一条:steps 的必需子节点由作者手写,可能缺角色要求的直接子节点。
WARNING
这份表是当前状态的诚实记录,不是「已知可接受」。在它清空之前,XiHan.UI 不适合用于对无障碍有合规要求的场景。
步骤重放豁免
只剩一个组件在真机里推不到用例终态,两个适配器共用这条登记:
breadcrumb——扫描必须拦下跨文档跳转,否则测试宿主被导航走;而用例断言的正是「不拦下」,同一文档里不可兼得。
