Angular CDK a11y 无障碍工具包实战指南:键盘导航、焦点管理、实时播报与高对比度样式
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
本文以 Angular CDK 的 a11y 包为骨架,系统讲解其提供的无障碍基础设施:ListKeyManager/TreeKeyManager键盘导航、cdkTrapFocus焦点陷阱、InteractivityChecker可交互性检测、LiveAnnouncer屏幕阅读器播报、FocusMonitor焦点来源追踪以及可访问性 Sass 样式工具。读完本文,你将掌握这些 API 的调用方式、接口约束与底层实现原理,能直接为菜单、列表、树、对话框等组件构建符合 WAI-ARIA 规范的键盘与焦点体验。
一、包结构与核心能力总览
a11y包位于仓库 src/cdk/a11y,由 index.ts 和 public-api.ts 统一导出,其目录组织与能力对应如下:
| 能力 | 源码位置 | 核心导出 |
|---|---|---|
| 列表键盘导航 | key-manager | ListKeyManager、FocusKeyManager、ActiveDescendantKeyManager、TreeKeyManager |
| 焦点陷阱 | focus-trap | CdkTrapFocus指令、FocusTrap/ConfigurableFocusTrap类 |
| 可交互性检测 | interactivity-checker | InteractivityChecker服务 |
| 屏幕阅读器播报 | live-announcer | LiveAnnouncer服务 |
| 焦点来源监控 | focus-monitor | FocusMonitor服务及两个监控指令 |
| ARIA 描述 | aria-describer | AriaDescriber服务 |
| 输入方式检测 | input-modality | InputModalityDetector |
| 样式工具 | _index.scss | a11y-visually-hidden、high-contrastmixin |
| 高对比度模式 | high-contrast-mode | HighContrastModeDetector |
| ID 生成器 | id-generator.ts | 唯一 ID 生成工具 |
二、ListKeyManager:列表/菜单/下拉框的键盘导航
2.1 基本用法:三步接入
ListKeyManager用于基于键盘交互管理列表中的活动项,适合实现role="menu"或role="listbox"模式的组件。使用它的组件通常做三件事:
- 用
@ViewChildren查询被管理的选项; - 初始化
ListKeyManager并传入选项; - 把组件收到的键盘事件转发给
ListKeyManager。
每个被管理的选项需要实现ListKeyManagerOption接口(见 list-key-manager.ts):
interface ListKeyManagerOption { disabled?: boolean; getLabel?(): string; }disabled:为true时该项会被跳过。默认的跳过谓词_skipPredicateFn = (item) => item.disabled(见 list-key-manager.ts),也可以用skipPredicate()自定义跳过逻辑。getLabel():仅在启用 typeahead(输入跳转)时才必需。
ListKeyManager的构造函数支持三种数据源(见 list-key-manager.ts):
new ListKeyManager(items: QueryList<T> | T[] | readonly T[]) new ListKeyManager(items: Signal<T[]>, injector: Injector)- 传入
QueryList时,管理器会自动订阅其changes事件,动态同步新增/移除的选项; - 传入普通数组适用于拿不到
QueryList的场景; - 传入
Signal时必须同时提供Injector,管理器内部通过effect()追踪信号变化。
2.2 方向键处理与 wrapping
onKeydown(event)是核心入口(见 list-key-manager.ts)。它按keyCode分派:DOWN_ARROW/UP_ARROW处理垂直移动、RIGHT_ARROW/LEFT_ARROW处理水平移动(RTL 下左右方向自动互换)、HOME/END、PAGE_UP/PAGE_DOWN、TAB触发tabOut流、其余字符键交给 typeahead。导航键命中后会自动调用event.preventDefault(),避免页面滚动等默认行为。
this.keyManager = new FocusKeyManager(...).withWrap();withWrap()开启环绕模式,到达列表末尾后继续按方向键会回到另一端(见 list-key-manager.ts)。底层实现(见_setActiveInWrapMode)用取模运算(activeIndex + delta * i + length) % length循环查找下一个未禁用的项。
除withWrap外,其余常用链式配置方法:
| 方法 | 作用 | 默认值 |
|---|---|---|
withVerticalOrientation(enabled) | 启用/禁用上下方向键 | true |
withHorizontalOrientation('ltr'\|'rtl'\|null) | 启用水平导航,null禁用 | null |
withAllowedModifierKeys(keys) | 允许按住哪些修饰键时仍响应方向键 | 不允许任何修饰键 |
withTypeAhead(debounceInterval) | 开启输入跳转(typeahead),需选项实现getLabel() | 200ms |
withHomeAndEnd(enabled) | Home/End 跳到首/末项 | false |
withPageUpDown(enabled, delta) | PageUp/PageDown 按delta步进跳转 | delta = 10 |
skipPredicate(predicate) | 自定义跳过谓词 | 跳过disabled项 |
2.3 两种变体:FocusKeyManager 与 ActiveDescendantKeyManager
ListKeyManager有两个子类,对应两种不同的"激活"语义(见 a11y.md):
FocusKeyManager(focus-key-manager.ts)——选项直接接收浏览器焦点,每个项必须实现FocusableOption:
interface FocusableOption extends ListKeyManagerOption { focus(origin?: FocusOrigin): void; }其setActiveItem在激活新项时调用item.focus(this._origin)让浏览器焦点真正落在该项上(见 focus-key-manager.ts)。_origin默认是'program',可通过setFocusOrigin(origin)指定焦点来源,让FocusMonitor能正确记录"由键盘导航触发"的焦点变化。
ActiveDescendantKeyManager(activedescendant-key-manager.ts)——选项不接收真实焦点,而是通过aria-activedescendant标记活动项,每个项必须实现Highlightable:
interface Highlightable extends ListKeyManagerOption { setActiveStyles(): void; setInactiveStyles(): void; }激活新项时,旧项调用setInactiveStyles()清除高亮、新项调用setActiveStyles()应用高亮(见 activedescendant-key-manager.ts)。此外每个项还必须有绑定到菜单/列表框aria-activedescendant的 ID——这也是 CDK 提供 id-generator.ts 的原因。
选择哪种:如果选项本身是"可聚焦的真实 DOM 元素"(如<button role="menuitem">),用FocusKeyManager;如果选项是"由容器统一管理虚拟焦点"的轻量项(如 autocomplete 面板),用ActiveDescendantKeyManager。
2.4 监听活动项变化与资源清理
管理器暴露两个可观察对象:
change: Subject<number>——活动项索引变化时发出;tabOut: Subject<void>——按下 Tab 键(焦点离开列表)时发出,常用于关闭菜单/弹出层。
调用方应在组件销毁时调用destroy()取消订阅并完成流(见 list-key-manager.ts)。
三、TreeKeyManager:树形视图的键盘导航
TreeKeyManager(tree-key-manager.ts)用于实现role="tree"模式的组件,它除了移动活动项,还会正确处理树的展开/折叠、层级移动和 typeahead(见 a11y.md)。
使用步骤与ListKeyManager类似:用@ViewChildren查询树节点 → 初始化管理器 → 通过onKeydown转发键盘事件。每个树节点实现TreeKeyManagerItem接口,构造函数支持QueryList、普通数组或Observable<T[]>三种数据源,并接收TreeKeyManagerOptions配置(见 tree-key-manager.ts),可配置项包括:
shouldActivationFollowFocus——聚焦时是否同时激活(选中)该项;horizontalOrientation——水平方向(ltr/rtl),影响左右方向键语义;skipPredicate——跳过谓词,默认不跳过任何节点(含禁用项),以符合 ARIA 关于禁用控件可聚焦的建议;trackBy——判定节点相等的函数;typeAheadDebounceInterval——typeahead 防抖间隔。
焦点管理:管理器会在键盘交互时自动聚焦合适的项。当活动项变化时,通过change属性发出通知,组件应据此更新tabindex:
- 没有活动项时,树本身保留
tabindex;有活动项时树本身不设tabindex; - 只有活动项对应的 HTML 节点设置
tabindex="0",其余项一律-1。
这样保证 Tab 进入树时焦点落在正确的节点上,同时避免破坏 tab 键的自然遍历顺序。
四、FocusTrap:把 Tab 焦点困在指定区域内
4.1 cdkTrapFocus 指令
cdkTrapFocus指令将 Tab / Shift+Tab 的焦点循环约束在元素内部,适合模态对话框等需要约束焦点的场景(对应 WAI-ARIA 的 dialog-modal 模式)。在组件中导入CdkTrapFocus即可使用:
<div class="my-inner-dialog-content" cdkTrapFocus> <!-- Tab 与 Shift + Tab 都不会离开这个元素 --> </div>注意两点:
- 该指令不阻止鼠标交互导致的焦点移出;
- 它默认不在初始化时自动捕获焦点,因此仅用
cdkTrapFocus时页面初次打开不会自动把焦点移入区域。
底层实现见 focus-trap.ts:FocusTrap类在区域首尾插入两个不可见的锚点元素,首锚点获得焦点时跳转到区域内最后一个可 Tab 元素,尾锚点获得焦点时跳转到第一个可 Tab 元素,从而形成循环;其构造函数还接收InteractivityChecker,用于兜底判断区域本身是否可聚焦。此外它还通过enabled属性开关锚点的tabIndex,支持动态启用/禁用陷阱(见 focus-trap.ts)。
4.2 显式焦点区域:cdkFocusRegionStart / cdkFocusRegionEnd / cdkFocusInitial
当区域内不止一组焦点时,可以用三个 DOM 属性显式声明焦点区域:
cdkFocusRegionStart与cdkFocusRegionEnd定义焦点循环的起止范围;cdkFocusInitial指定区域初始化时接收焦点的元素。
<a mat-list-item routerLink cdkFocusRegionStart>Focus region start</a> <a mat-list-item routerLink>Link</a> <a mat-list-item routerLink cdkFocusInitial>Initially focused</a> <a mat-list-item routerLink cdkFocusRegionEnd>Focus region end</a>重要提示:如果cdkFocusInitial与cdkTrapFocus一起使用,除非同时启用cdkTrapFocusAutoCapture选项,否则初始聚焦不会发生——因为CdkTrapFocus默认不在初始化时捕获焦点。
五、InteractivityChecker:元素可交互性检测
InteractivityChecker是用于检测元素可交互性的服务,覆盖四种状态(见 a11y.md):
- disabled:是否被禁用(
disabled属性或aria-disabled); - visible:是否可见;
- tabbable:是否可被 Tab 键到达;
- focusable:是否可聚焦。
它内部被FocusTrap(判断兜底聚焦目标)、FocusMonitor等模块复用,例如焦点陷阱的锚点监听回调中就用this._checker.isFocusable(this._element)判断区域本身能否回退聚焦(见 focus-trap.ts)。实现位于 interactivity-checker 目录,详细 API 参见 API 文档。
六、LiveAnnouncer:向屏幕阅读器播报消息
LiveAnnouncer通过aria-live区域向屏幕阅读器用户播报消息(如"已保存""3 项已选择"等)。其实现位于 live-announcer.ts:服务创建一个隐藏的aria-live元素(或复用通过LIVE_ANNOUNCER_ELEMENT_TOKEN注入的自定义元素),在 Angular zone 外更新其内容以避免频繁触发变更检测。
6.1 基本用法(基于注入函数)
@Component({...}) export class MyComponent { private liveAnnouncer = inject(LiveAnnouncer); announceMessage() { this.liveAnnouncer.announce("Hey Google"); } }announce方法有两个可重载参数(见 live-announcer.ts):
politeness——'off' | 'polite' | 'assertive',对应aria-live属性值(类型定义见 live-announcer-tokens.ts);duration——消息加入 DOM 后清除的毫秒数。
方法返回Promise<void>,在消息真正写入 DOM 后 resolve。
6.2 全局默认配置
通过LIVE_ANNOUNCER_DEFAULT_OPTIONS注入令牌(见 live-announcer-tokens.ts)可以配置全局默认值:
{ provide: LIVE_ANNOUNCER_DEFAULT_OPTIONS, useValue: { politeness: 'assertive', duration: 5000 } }LIVE_ANNOUNCER_ELEMENT_TOKEN则允许自定义承载消息的aria-live元素(默认为null,即由服务自行创建)。
七、FocusMonitor:追踪焦点来源
FocusMonitor是可注入服务,比监听原生focus/blur事件更强大:它能告诉你元素是经由什么方式聚焦的(鼠标、键盘、触摸或程序化),也支持监听子树焦点。
7.1 monitor 方法与 FocusOrigin
用monitor(element, checkChildren?)监听元素焦点变化,checkChildren传true时只要任一后代获得焦点就视为该元素"聚焦",默认false。返回一个Observable<FocusOrigin>,取值含义(见 a11y.md 与 focus-monitor.ts):
| FocusOrigin | 含义 |
|---|---|
'mouse' | 鼠标点击聚焦 |
'keyboard' | 键盘聚焦 |
'touch' | 触摸屏触摸聚焦 |
'program' | 程序化调用focus() |
null | 元素失焦 |
实现层面,FocusMonitor依赖InputModalityDetector记录最近的 mousedown/keydown/touchstart 输入方式,并结合窗口聚焦、超时机制把焦点事件归因到对应输入方式(见 focus-monitor.ts)。检测模式可通过FOCUS_MONITOR_DEFAULT_OPTIONS令牌配置为IMMEDIATE(默认,只看当前/上一个 tick 的输入)或EVENTUAL(归因于最后一次对应输入事件,见 focus-monitor.ts)。
7.2 自动附加的 CSS 类
被监控的元素聚焦时会自动获得 CSS 类:
.cdk-focused——元素处于聚焦状态;.cdk-${origin}-focused——按焦点来源附加.cdk-mouse-focused、.cdk-keyboard-focused、.cdk-touch-focused或.cdk-program-focused。
这些类让开发者可以针对不同输入方式定制视觉反馈(如仅键盘导航时显示焦点环、鼠标点击时不显示)。
7.3 资源释放与 focusVia
任何通过monitor监控的元素,最终都应调用stopMonitoring(element)取消监控。
focusVia(element, origin)允许程序化聚焦时"伪造"焦点来源:若目标元素正在被监控,则上报传入的 origin;若未被监控,则像普通focus()一样聚焦(见 a11y.md)。
7.4 两个便捷指令
cdkMonitorElementFocus——等价于对宿主元素调用monitor(el, false);cdkMonitorSubtreeFocus——等价于调用monitor(el, true)。
两者都提供@Output() cdkFocusChange,在FocusOrigin变化时发出新值(实现见 focus-monitor.ts 中的CdkMonitorFocus相关指令)。示例代码可参考仓库中的focus-monitor-overview、focus-monitor-focus-via与focus-monitor-directives三个官方示例。
八、无障碍样式工具(Sass mixins)
8.1 视觉隐藏:a11y-visually-hidden
屏幕阅读器等辅助技术会跳过display: none、visibility: hidden、opacity: 0、height: 0、width: 0的元素。有时你需要视觉上隐藏元素,但保留给辅助技术,这时使用a11y-visually-hiddenmixin,它会产出.cdk-visually-hidden类:
@use '@angular/cdk'; @include cdk.a11y-visually-hidden();<div class="custom-checkbox"> <input type="checkbox" class="cdk-visually-hidden"> </div>典型的应用场景是"视觉上隐藏原生 checkbox/radio,用自定义样式替代,但保留原生控件给辅助技术"。mixin 的实现见 _index.scss:使用clip: rect(0 0 0 0)配合1px尺寸、绝对定位把元素移出可视区域但保留在可访问性树中,并额外处理了 RTL 方向与 Chrome 长文本换行崩溃等边界问题。
注意:使用 Angular Material 时,.cdk-visually-hidden会由其 theming 系统自动包含;其他场景需要手动@include cdk.a11y-visually-hidden();到全局样式表中。
8.2 高对比度模式:high-contrast
部分操作系统提供高对比度模式(High Contrast Mode),high-contrastmixin 让你只为该模式下的用户定义样式,其原理是编译为@media (forced-colors: ...)媒体查询(见 _index.scss):
@use '@angular/cdk'; button { @include cdk.high-contrast { outline: solid 1px; } }mixin 接收可选参数$target,指定forced-colors媒体查询的值,只能是active或none(默认active;传入非法值会在编译期抛出 Sass 错误)。历史值black-on-white/white-on-black仍被接受但会被归一化为active。
九、在真实组件中的组合应用
CDK a11y 的这些能力常常组合使用,形成一个完整的无障碍组件。以"键盘可导航的列表 + 实时播报 + 焦点样式"为例:
import {FocusKeyManager, LiveAnnouncer} from '@angular/cdk/a11y'; @Component({ selector: 'app-option-list', template: ` <div role="listbox" (keydown)="onKeydown($event)"> <div role="option" *ngFor="let opt of options; let i = index" cdkMonitorElementFocus (cdkFocusChange)="onFocusChange(opt, $event)" [class.cdk-keyboard-focused]="lastOrigin === 'keyboard'"> {{opt.label}} </div> </div> `, }) export class OptionListComponent implements OnInit, OnDestroy { private liveAnnouncer = inject(LiveAnnouncer); @ViewChildren(RoleOptionDirective) options!: QueryList<RoleOptionDirective>; keyManager!: FocusKeyManager<RoleOptionDirective>; lastOrigin: FocusOrigin | null = null; ngOnInit() { this.keyManager = new FocusKeyManager(this.options) .withWrap() .withHomeAndEnd() .withTypeAhead(200); } onKeydown(event: KeyboardEvent) { this.keyManager.onKeydown(event); } onFocusChange(opt: any, origin: FocusOrigin) { this.lastOrigin = origin; this.liveAnnouncer.announce(`${opt.label} focused`, 'polite'); } ngOnDestroy() { this.keyManager.destroy(); } }其中RoleOptionDirective只需实现FocusableOption(focus()方法)即可被FocusKeyManager管理。若改用role="menu"的弹出菜单,则需在tabOut事件中关闭菜单,并把整个面板包上cdkTrapFocus以约束焦点;配合AriaDescriber(aria-describer)还可为元素动态维护aria-describedby描述。
十、测试与验证
仓库为每个 a11y 模块提供了完整的单元测试,是理解行为边界的绝佳材料:
- list-key-manager.spec.ts——验证方向键移动、wrap 环绕、跳过禁用项、typeahead、Home/End/PageUp/PageDown 等;
- tree-key-manager.spec.ts——验证树的展开折叠与焦点管理;
- focus-trap.spec.ts 与 configurable-focus-trap.spec.ts——验证焦点循环、初始聚焦与 autoCapture;
- focus-monitor.spec.ts——验证各
FocusOrigin的归因与 CSS 类附加; - live-announcer.spec.ts——验证 politeness 属性、消息清除与默认配置。
结语
CDK a11y 包把 WAI-ARIA 中"键盘可操作性""焦点管理""屏幕阅读器播报""高对比度支持"等抽象规范落地为可直接注入的 Angular 服务与指令。无论是自研菜单、下拉框、树组件,还是为对话框补充焦点约束,都可以先看a11y包是否已有现成方案——它的每个模块(key-manager、focus-trap、focus-monitor、live-announcer)都独立可用,且配套的 API 文档(如 list-key-manager.md、focus-trap.md、live-announcer.md)与单元测试可以帮你快速确认每个 API 的确切行为。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考