如何构建真正可用的可访问自定义下拉框(Combobox)
Source: Dev.to
最初发表于 AccessGuard 博客。
第一步:使用正确的角色
Combobox 不仅仅是一个经过样式化的按钮。WAI‑ARIA 作者实践定义了特定的角色结构:
- 一个 combobox 元素(通常是 button 或 input),其
aria-haspopup="listbox"并且aria-expanded反映打开状态。 aria-controls指向 listbox 的 id。- 弹出层本身使用
role="listbox"。 - 每个选项使用
role="option",并在当前活动的选项上添加aria-selected。
提示: 角色必须与行为相匹配。不要在表现为菜单的 div 上随意加上
role="listbox"。
步骤 2:使触发器可访问且具描述性
- 触发器必须是一个真正可聚焦的元素——可以是原生的
或带有 `tabindex="0"` 的。 - 使用可见标签、
aria-label或aria-labelledby为其提供可访问名称。 - 宣布当前值,使屏幕阅读器用户听到 “Country, combobox, United States”,而不仅仅是 “combobox, collapsed”。
第3步:键盘支持不可妥协
至少需要:
- Enter 或 Space – 打开列表框。
- Arrow Down – 打开(如果已关闭)并移动到第一个或已选中的选项。
- Arrow Up / Arrow Down – 在选项之间移动。
- Home / End – 跳转到第一个和最后一个选项。
- Escape – 关闭并将焦点返回到触发器。
- Tab – 关闭并移动到下一个可聚焦元素。
- Typeahead – 按下字母键可跳转到下一个匹配的选项。
第4步:管理焦点,而不仅仅是视觉高亮
在列表框模式中,DOM 焦点保持在组合框触发器上。
在触发器上使用 aria-activedescendant,指向当前高亮选项的 id。
这使得 typeahead 和键盘处理仍在触发器上,同时向辅助技术告知哪个选项是活动的。
将真实焦点移入列表是常见错误,会破坏 typeahead 并使屏幕阅读器感到困惑。
第5步:在不产生噪音的情况下宣布更改
当列表框打开时,如果 aria-expanded 和 aria-activedescendant 正确连接,屏幕阅读器应自动宣布展开状态和活动选项。
避免使用额外的 aria-live 区域来重复宣布——双重宣布比没有宣布更糟。
Step 6: Do not forget the close behaviors
- 在 Escape 键、点击外部以及组合框失焦时关闭列表框。
- 通过 Escape 关闭时,将焦点恢复到触发器。
- 当用户选择一个选项时:
- 更新触发器的可见文本及其可访问名称。
- 关闭列表框。
- 将焦点返回到触发器。
快速测试清单
- 只能使用键盘就能打开、导航、选择并关闭吗?
- VoiceOver 或 NVDA 是否会朗读角色、状态和当前选项?
- 输入提示(typeahead)是否有效?
- 按 Escape 是否总是将焦点返回到触发元素?
- 移动端的触摸用户是否能获得可用的体验?(提示:在 iOS 上使用 TalkBack 和 VoiceOver 进行测试)
如果你对所有这些都能回答 是,那么你已经超越了网络上 90 % 的自定义下拉菜单。
何时可以跳过所有这些
老实说,只要可能就使用原生的 “ 元素。它具有可访问性,兼容所有平台,且移动浏览器还能免费提供出色的选择器。
仅在确实需要原生 “ 无法提供的功能时才构建自定义组合框——可搜索的选项、丰富的选项内容或异步加载。最好的可访问组件往往是你根本不需要构建的那个。
我们将在后续发布关于可访问自动完成的文章,这会在此模式之上增加另一层复杂性。如果有你希望我们下次讨论的棘手组件,请告诉我们。
在 AccessGuard 上阅读更多内容,访问 getaccessguard.com。