自定义组件常见的失误是提供了角色却遗漏了对应状态属性
别名: 角色无状态 · role without state · 不完整组件
概念解释
给 div 写上 role="checkbox" 或 role="combobox",阅读器就会按这种控件来介绍它,并期待能读到勾选、展开、按下这类状态。不完整的 ARIA 组件(incomplete ARIA widget)指的是角色已经宣称「我是这种控件」,对应的状态属性却没挂上。用户听到「复选框」,问「勾没勾」时得到的是沉默,不是「未选中」。
失误不在「忘了起名」,也不在「状态改了没同步」——连可同步的属性都没有。角色把预期造出来,状态槽位空着。
机制
角色是一份合同。checkbox 合同里有 checked;combobox 有 expanded 和 hasPopup;tab 有 selected;switch 有 checked;可切换的 button 有 pressed。阅读器按合同决定读什么、用什么按键模型去理解它。属性缺席时,多数阅读器不会猜一个默认值再读出来,而是把那一项从语音里抹掉。于是角色还在制造「这是复选框」的预期,状态通道没有载荷。
第二层是设计系统里的复用。组件库先铺一层角色,好让检查器认出来,状态绑定留给业务侧「有空再接」。检查器看到角色就过了自动规则,漏状态在自动化里经常是绿的。真正暴露发生在用户第一次问「现在是开还是关」。
怎么研究
抽样自定义控件:凡是 role 不是原生默认的,列一张「该角色要求的状态属性」对照表(WAI-ARIA 里每个角色的 required / supported states)。用无障碍检查器和 NVDA 读每个实例,记录:角色是否被说出、状态词是否出现、键盘模型是否按该角色工作。再对比同一功能的原生元素。
自变量:角色种类、是否具备 required 状态、是否具备 supported 但业务上用得到的状态。 因变量:语音是否含状态、用户能否回答「当前是哪一态」、自动检测是否仍报通过。
边界
role="presentation" / none 是在撤销语义,不要求状态,谈「漏状态」没有意义。静态的 role="img" 或 note 也没有展开/选中合同。role="button" 若只是一次性动作,本来就没有 pressed;只有做成开关时才欠这条属性——不能把所有按钮都判成漏状态。阅读器对缺属性的容错不同:有的把 combobox 当普通输入框降级,有的什么都不补,测试必须覆盖组合,不能只信一家的沉默。
怎么落地
- 每引入一个非原生角色,先列出该角色合同里的状态,再写视觉;缺哪条状态就不宣称那个角色。
- 复选、开关、手风琴、选项卡、组合框把状态属性做成和数据模型同一字段,禁止只在故事书里写角色。
- 自动检测通过后,仍用阅读器问一句「现在是什么状态」;问不出来就按漏状态修,不要按「有角色」结案。
- 验证:在检查器里过滤所有自定义角色节点,逐个看必填状态是否存在且有值。再用 NVDA 对每个控件问当前态。角色能读、状态词没有,就是这张合同只签了一半。