ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Angular CDK FocusKeyManager 深入解析:用键盘焦点管理器构建可访问的菜单与列表组件

Angular CDK FocusKeyManager 深入解析:用键盘焦点管理器构建可访问的菜单与列表组件 Angular CDK FocusKeyManager 深入解析用键盘焦点管理器构建可访问的菜单与列表组件【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/componentsFocusKeyManager是 Angular CDKangular/cdk/a11y包中用于基于键盘交互管理列表项焦点的核心工具类它继承自ListKeyManager是rolemenu、rolelistbox等 WAI-ARIA 组件模式在 Angular 中的标准实现基础。本文将从接口契约、继承能力、按键映射、配置方法、焦点来源FocusOrigin到仓库内真实源码与测试用例完整讲解如何在你的组件中落地一个键盘可导航的焦点管理体系。一、FocusKeyManager 在 CDK 无障碍体系中的定位FocusKeyManager位于 focus-key-manager.ts是整个angular/cdk/a11y包中键盘导航能力链上的一环。它解决的问题非常具体当用户按方向键、Home/End、PageUp/PageDown 时如何决定哪个子项应该获得焦点并把这些键盘语义翻译成对真实 DOM 焦点focus()的调用。在 CDK 的无障碍工具矩阵中它与其他机制分工明确工具职责FocusKeyManager直接移动浏览器焦点到列表中的某一项本文主角ActiveDescendantKeyManager通过aria-activedescendant标记当前活动项焦点始终留在宿主元素上TreeKeyManager管理roletree树形视图的活动节点FocusMonitor监听元素焦点来源鼠标/键盘/触摸/程序化并给出FocusOrigin其中FocusKeyManager与ActiveDescendantKeyManager共享同一个父类ListKeyManager区别仅在于激活一项时的副作用前者调用该项的focus()后者调用setActiveStyles()/setInactiveStyles()。完整的功能对照可参考 a11y.md 中的 Types of list key managers 一节。二、核心接口契约FocusableOption任何交给FocusKeyManager管理的项都必须满足一个接口——FocusableOption。它的定义在 focus-key-manager.tsexport interface FocusableOption extends ListKeyManagerOption { /** Focuses the FocusableOption. */ focus(origin?: FocusOrigin): void; }它继承自 list-key-manager.ts 中定义的ListKeyManagerOptionexport interface ListKeyManagerOption { /** Whether the option is disabled. */ disabled?: boolean; /** Gets the label for this option. */ getLabel?(): string; }三个成员的语义分别是focus(origin?: FocusOrigin)让该项自身获得浏览器焦点。origin参数用于告知该项被聚焦的方式默认值为program程序化聚焦。disabled?可选布尔值表示该项是否禁用。禁用项会被键盘导航自动跳过默认行为可用skipPredicate自定义。getLabel?()可选方法返回该项的文本标签。仅在使用 typeahead按键首字母跳转功能时必须实现源码在 list-key-manager.ts 中会在开发模式下校验若开启 typeahead 而项未实现getLabel会直接抛出ListKeyManager items in typeahead mode must implement the getLabel method.。三、继承自 ListKeyManager 的能力全览FocusKeyManager本体非常精简约 60 行其全部肌肉都来自父类ListKeyManager约 470 行见 list-key-manager.ts。使用前先完整了解这些继承能力。3.1 只读状态属性属性类型说明activeItemIndexnumber \| null当前活动项的索引无活动项时为null初始值为-1activeItemT \| null当前活动项本身isTyping()boolean用户是否正处于 typeahead 连续输入过程中内部实现上这两个状态以 Angular 的signal存储_activeItemIndex signal(-1)因此是响应式的。3.2 可订阅事件流流触发时机tabOut: Subjectvoid每当按下Tab键时发出组件可据此得知焦点即将离开列表change: Subjectnumber活动项索引发生变化时发出新索引两个流在destroy()中都会被complete()。3.3 三种初始化方式ListKeyManager构造函数支持三种数据源list-key-manager.tsconstructor(items: QueryListT | T[] | readonly T[]); constructor(items: SignalT[] | Signalreadonly T[], injector: Injector);QueryListT最常见配合ViewChildren/ContentChildren使用并自动订阅changes以响应子项增删。普通数组当项不是通过模板查询收集例如来自服务或动态渲染时使用。SignalT[]Injector信号驱动模式下使用内部通过effect()跟踪数组变化。注意信号模式必须传入injector否则开发模式下会抛错见源码第 75-77 行。3.4 键盘事件处理onKeydownonKeydown(event: KeyboardEvent)是整个键盘导航的入口按键映射逻辑位于 list-key-manager.ts可用如下矩阵概括按键生效前提行为Tab无条件发出tabOut不阻止默认行为↓垂直模式已启用默认激活下一项↑垂直模式已启用默认激活上一项→withHorizontalOrientation已设置LTR 激活下一项RTL 激活上一项←withHorizontalOrientation已设置LTR 激活上一项RTL 激活下一项HomewithHomeAndEnd()已启用激活第一项EndwithHomeAndEnd()已启用激活最后一项PageUpwithPageUpDown()已启用向前跳delta默认 10项越界取第 0 项PageDownwithPageUpDown()已启用向后跳delta默认 10项越界取最后一项其他可打印字符已开启 typeahead 且修饰键允许交给Typeahead做首字母匹配两点关键实现细节修饰键拦截默认情况下任何修饰键alt/ctrl/meta/shift被按住时方向键导航都会被忽略isModifierAllowed为 false只有通过withAllowedModifierKeys显式放行的修饰键组合才会生效。测试用例should not do anything for arrow keys if the alt key is held down等验证了这一点见 list-key-manager.spec.ts。默认行为处理对成功处理的方向键、Home/End、PageUp/PageDown会调用event.preventDefault()阻止浏览器默认滚动等行为而对 Tab 和未匹配的按键则显式return不去阻止默认动作测试用例should not prevent the default keyboard action when pressing tab。3.5 活动项跳转方法方法行为setFirstItemActive()激活第一个可用项setLastItemActive()激活最后一个可用项setNextItemActive()激活下一个可用项无活动项时等价于setFirstItemActivesetPreviousItemActive()激活上一个可用项无活动项且开启 wrap 时跳到最后一项setActiveItem(index \| item)按索引或按引用激活指定项updateActiveItem(index \| item)仅更新活动项状态不产生任何副作用FocusKeyManager 下即不调用focus可用项的判定由_skipPredicateFn决定默认实现是item item.disabled。跳过逻辑在_setActiveItemByIndex中通过 while 循环实现list-key-manager.ts可使用skipPredicate(predicate)自定义例如跳过不可见的项。四、FocusKeyManager 自己的两个能力4.1 重写 setActiveItem激活即聚焦FocusKeyManager唯一重写的方法是setActiveItemfocus-key-manager.tsoverride setActiveItem(item: any): void { super.setActiveItem(item); if (this.activeItem) { this.activeItem.focus(this._origin); } }它先委托父类完成活动项状态更新与change流发出再对新的活动项调用focus(this._origin)将焦点真实移动到该项上。这也解释了为什么updateActiveItem与setActiveItem必须并存前者只改状态不聚焦适合需要在状态更新后由组件自行处理聚焦的精细化场景测试should allow setting the focused item without calling focus专门验证了这一点。4.2 setFocusOrigin携带焦点来源setFocusOrigin(origin: FocusOrigin): this { this._origin origin; return this; }_origin字段默认值为program每次focus调用都会把它作为参数传给FocusableOption.focus()。FocusOrigin类型来自 focus-monitor.ts取值包括mouse | keyboard | touch | program | null。该项在收到 origin 后可以做差异化处理——例如MatMenuItem据此决定是否渲染键盘导航的视觉样式。该方法返回this支持链式调用。对应的测试验证list-key-manager.spec.tssetFocusOrigin(mouse)后按方向键断言项的focus被以mouse调用切换为keyboard后focus以keyboard调用。五、配置方法详解链式 APIListKeyManager的配置方法统一返回this可一口气链式组合this.keyManager new FocusKeyManager(this.items) .withWrap() // 到边界后循环 .withVerticalOrientation() // 允许 ↑/↓默认开启 .withHorizontalOrientation(ltr) // 允许 →/←支持 ltr | rtl | null .withHomeAndEnd(true) // 允许 Home/End 跳到首尾 .withPageUpDown(true, 5) // 允许 PageUp/PageDown步长 5 .withAllowedModifierKeys([shiftKey]) // 放行 Shift方向键 .withTypeAhead(200) // 开启首字母跳转防抖 200ms .skipPredicate(item item.disabled) // 自定义跳过规则 .setFocusOrigin(keyboard); // 指定焦点来源各方法逐一说明方法默认说明withWrap(shouldWrap true)关开启后在列表两端继续导航会环绕。实现上在 wrap 模式用取模运算(activeIndex delta * i length) % length循环扫描可用项见_setActiveInWrapModelist-key-manager.tswithVerticalOrientation(enabled true)开控制 ↑/↓ 是否参与导航withHorizontalOrientation(direction)null关传入ltr或rtl启用 →/←并自动处理 RTL 下方向反转withAllowedModifierKeys(keys)[]不允许任何修饰键允许按住指定修饰键altKey \| ctrlKey \| metaKey \| shiftKey时仍响应方向键withHomeAndEnd(enabled true)关启用 Home/End 跳到首尾可用项withPageUpDown(enabled true, delta 10)关启用 PageUp/PageDown 大跨度跳转delta为步长withTypeAhead(debounceInterval 200)关开启首字母跳转debounceInterval为按键防抖毫秒数要求所有项实现getLabel()skipPredicate(predicate)item item.disabled自定义跳过项判定setFocusOrigin(origin)program设置后续focus()调用携带的FocusOrigin六、实战三步接入你的组件按照 a11y.md 的 Basic usage 说明使用FocusKeyManager的组件通常做三件事查询子项 → 初始化 manager → 转发键盘事件。下面是一个可运行的简化示例import {Component, ContentChildren, QueryList, AfterContentInit, HostListener} from angular/core; import {FocusKeyManager, FocusableOption} from angular/cdk/a11y; // 1. 每个被管理的项实现 FocusableOption Component({ selector: app-menu-item, template: div classmenu-item [class.active]active{{label}}/div, host: {role: menuitem}, }) export class MenuItem implements FocusableOption { active false; label Item; disabled false; focus(_origin?: string) { // 这里拿到真实 DOM 元素并调用 focus() // 也可以根据 origin 决定是否应用键盘导航样式 } } // 2. 宿主组件查询 初始化 转发键盘事件 Component({ selector: app-menu, template: ng-content/ng-content, host: {role: menu, (keydown): onKeydown($event)}, }) export class Menu implements AfterContentInit { ContentChildren(MenuItem) items!: QueryListMenuItem; private keyManager!: FocusKeyManagerMenuItem; ngAfterContentInit() { // 3. 初始化传入 QueryList链式配置 this.keyManager new FocusKeyManager(this.items) .withWrap() .withHomeAndEnd() .setFocusOrigin(keyboard); } HostListener(keydown, [$event]) onKeydown(event: KeyboardEvent) { this.keyManager.onKeydown(event); } }要点回顾键盘事件必须由宿主容器统一捕获并转发给keyManager.onKeydown(event)而不是由每个子项单独处理withWrap()让菜单在按↓越过最后一项时回到第一项setFocusOrigin(keyboard)告知子项是键盘驱动了焦点移动子项可以据此正确呈现键盘导航样式避免鼠标悬停与键盘导航样式冲突。七、仓库中的真实应用与测试佐证FocusKeyManager不是纸面 API它在仓库的 Material 与 CDK 组件中被广泛使用可作为最直接的参考实现MatMenuMatMenu通过ViewChildren收集直接子菜单项并构造new FocusKeyManager(this._directDescendantItems)见 menu.ts 与 menu.ts其MatMenuItem类正是FocusableOption接口的实现者。MatChipSet / MatSelectionList / MatExpansionPanel / MatTabHeader分别在 chip-set.ts、selection-list.ts、accordion.ts、paginated-tab-header.ts 中用于管理 chips、列表项、手风琴头部、分页 tab 的键盘焦点。CDK 侧CdkMenu的 menu-base.ts 与CdkStepper的 stepper.ts 同样以它为键盘导航核心。测试方面list-key-manager.spec.ts 中用FakeFocusable模拟可聚焦项覆盖了FocusKeyManager的核心行为契约连续按↓会依次调用第 1、2 项的focus且不会重复聚焦已聚焦项L1048-L1059↑恢复上一项焦点L1061-L1071updateActiveItem更新索引但不触发focusL1073-L1083setFocusOrigin后focus收到正确的FocusOrigin参数L1085-L1098。八、与其他 KeyManager 的选型对比在动手前先确认哪个 KeyManager 适合你的场景场景使用哪个子项需实现子项各自持有 DOM 元素焦点真实移动到子项菜单、tab 列表FocusKeyManagerFocusableOptionfocus()焦点始终留在宿主仅通过aria-activedescendant标记活动项combobox 建议列表ActiveDescendantKeyManagerHighlightablesetActiveStyles()/setInactiveStyles()见 activedescendant-key-manager.ts树形结构需要展开/收起与层级导航roletreeTreeKeyManager相关实现见 tree-key-manager.tsTreeKeyManagerItem两个关键区分点焦点移动的方式真实聚焦 vsaria-activedescendant与视觉高亮的方式由焦点自然带出 vs 显式setActiveStyles。若项在激活时需要以激活态样式呈现而无需移动焦点请选择ActiveDescendantKeyManager。九、总结FocusKeyManager用一个极简的继承架构父类负责状态与按键语义子类只重写激活副作用解决了列表型组件的键盘导航这一高复杂度问题ListKeyManager提供 wrap、typeahead、修饰键放行、自定义跳过谓词、方向键/Home/End/PageUp/PageDown 的完整按键矩阵与tabOut/change事件流FocusKeyManager在此基础上将激活与聚焦绑定并支持通过setFocusOrigin传递焦点来源。配合 a11y.md 中其他无障碍工具FocusTrap、FocusMonitor、LiveAnnouncer你可以为任意列表型组件构建出符合 WAI-ARIA 规范、键盘可完整操作的体验——这也是MatMenu、MatChipSet等 Material 组件无障碍能力的地基。【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表