Angular CDK a11y 无障碍工具包实战指南:键盘导航、焦点管理、实时播报与高对比度样式
2026/9/13 2:41:44 网站建设 项目流程

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-managerListKeyManagerFocusKeyManagerActiveDescendantKeyManagerTreeKeyManager
焦点陷阱focus-trapCdkTrapFocus指令、FocusTrap/ConfigurableFocusTrap
可交互性检测interactivity-checkerInteractivityChecker服务
屏幕阅读器播报live-announcerLiveAnnouncer服务
焦点来源监控focus-monitorFocusMonitor服务及两个监控指令
ARIA 描述aria-describerAriaDescriber服务
输入方式检测input-modalityInputModalityDetector
样式工具_index.scssa11y-visually-hiddenhigh-contrastmixin
高对比度模式high-contrast-modeHighContrastModeDetector
ID 生成器id-generator.ts唯一 ID 生成工具

二、ListKeyManager:列表/菜单/下拉框的键盘导航

2.1 基本用法:三步接入

ListKeyManager用于基于键盘交互管理列表中的活动项,适合实现role="menu"role="listbox"模式的组件。使用它的组件通常做三件事:

  1. @ViewChildren查询被管理的选项;
  2. 初始化ListKeyManager并传入选项;
  3. 把组件收到的键盘事件转发给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/ENDPAGE_UP/PAGE_DOWNTAB触发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 属性显式声明焦点区域:

  • cdkFocusRegionStartcdkFocusRegionEnd定义焦点循环的起止范围;
  • 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>

重要提示:如果cdkFocusInitialcdkTrapFocus一起使用,除非同时启用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):

  1. politeness——'off' | 'polite' | 'assertive',对应aria-live属性值(类型定义见 live-announcer-tokens.ts);
  2. 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?)监听元素焦点变化,checkChildrentrue时只要任一后代获得焦点就视为该元素"聚焦",默认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-overviewfocus-monitor-focus-viafocus-monitor-directives三个官方示例。

八、无障碍样式工具(Sass mixins)

8.1 视觉隐藏:a11y-visually-hidden

屏幕阅读器等辅助技术会跳过display: nonevisibility: hiddenopacity: 0height: 0width: 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媒体查询的值,只能是activenone(默认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只需实现FocusableOptionfocus()方法)即可被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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询