Angular ARIA Listbox 组件 API 完全指南:从 ngListbox/ngOption 到键盘导航与焦点管理
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
本文以官方 API 报告 goldens/aria/listbox/index.api.md 为核心骨架,结合本仓库(Angular Components,即 Material Design components for Angular)中ngListbox与ngOption指令的完整源码实现,系统讲解该无障碍 Listbox 组件的公共 API、输入参数、事件模型、键盘交互、焦点策略与测试手段。读完本文,你将能独立实现一个符合 WAI-ARIA Listbox 模式、支持单/多选、roving tabindex 或 activedescendant 焦点管理、typeahead 搜索的 Angular 列表选择组件,并能在单元测试中通过官方 Harness 进行交互断言。
一、关联文档概览:一份自动生成的公共 API 快照
goldens/aria/listbox/index.api.md 是由 API Extractor 针对@angular/aria_listbox包自动生成的 API 报告,它精确刻画了该包的对外公共面。其核心结论有三:
- 包对外只导出三个符号:指令类
Listbox<V>、指令类Option(源码中为Option_2,导出别名Option)、以及注入令牌LISTBOX; Listbox<V>是一个泛型指令(<V>即选项 value 的泛型类型),它通过大量InputSignal/ModelSignal暴露声明式输入,例如id、orientation、multi、focusMode、selectionMode、value等;- 报告中的
ɵdir字段揭示了真实的指令元数据:选择器分别为[ngListbox](exportAsngListbox)与[ngOption](exportAsngOption),且value与valueChange组成双向绑定的一对。
API 报告中的“undocumented”标记仅表示该成员缺少 JSDoc 注释,不代表其不可用。例如scrollActiveItemIntoView、gotoFirst、gotoIndex等命令式方法,以及LISTBOX令牌,都属于稳定公共 API。
二、核心指令与模板用法:ngListbox + ngOption
从源码 src/aria/listbox/listbox.ts 的@Directive装饰器可以看到:
Listbox指令选择器为[ngListbox],宿主元素被赋予role="listbox",并动态绑定id、tabindex、aria-readonly、aria-disabled、aria-orientation、aria-multiselectable、aria-activedescendant等 ARIA 属性;Option指令选择器为[ngOption],宿主元素被赋予role="option",并绑定aria-selected、aria-disabled、data-active等属性(见 src/aria/listbox/option.ts)。
官方 JSDoc 给出的最小完整模板如下:
<ul ngListbox [(value)]="selectedItems" [multi]="true" orientation="vertical"> @for (item of items; track item.id) { <li ngOption [value]="item.id" [label]="item.name" [disabled]="item.disabled"> {{item.name}} </li> } </ul>要点说明:
[(value)]是双向绑定,value的类型为V[](见readonly value: ModelSignal<V[]>),任何时刻都能取到当前选中项的值数组;ngOption的value是必填输入(input.required<V>()),它决定该项选中后写入value数组的元素;label输入作为可访问名称与 typeahead 搜索文本(见searchTerm: () => this.label() ?? '');disabled输入使单个选项不可交互。
指令内部还通过providers: [{provide: LISTBOX, useExisting: Listbox}]将自身注册到LISTBOX注入令牌(src/aria/listbox/tokens.ts),Option构造时通过inject(LISTBOX)反向拿到父级 Listbox,并在ngOnInit/ngOnDestroy中向父级的SortedCollection注册/注销自己,从而维护有序的选项集合。
三、Listbox 输入参数全解(附默认值与取值范围)
结合 API 报告与 src/aria/listbox/listbox.ts 的input()声明,各输入如下:
| 输入 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id | string | 由_IdGenerator生成(形如ng-listbox-0) | 宿主元素 id,也是aria-activedescendant引用的根 |
orientation | 'vertical' \| 'horizontal' | 'vertical' | 列表方向,决定上下/左右方向键映射 |
multi | boolean(booleanAttribute 变换) | false | 是否允许多选,开启后渲染aria-multiselectable |
wrap | boolean(booleanAttribute 变换) | true | 焦点导航是否循环(范围选择时会临时关闭) |
softDisabled | boolean(booleanAttribute 变换) | true | 为true时禁用项仍可获得焦点但不可交互;为false时导航直接跳过禁用项 |
focusMode | 'roving' \| 'activedescendant' | 'roving' | 焦点管理策略(详见第四节) |
selectionMode | 'follow' \| 'explicit' | 'follow' | follow表示焦点跟随即自动选中;explicit表示用户必须显式确认(空格/回车/点击) |
typeaheadDelay | number(毫秒) | 500 | typeahead 输入缓冲重置时间,源码注释标注“Picked arbitrarily”,可按需调整 |
disabled | boolean(booleanAttribute 变换) | false | 整体禁用,禁用时键盘/点击事件被忽略(见onKeydown/onClick的if (!this.disabled())守卫) |
readonly | boolean(booleanAttribute 变换) | false | 只读模式:仍可导航与搜索,但不改变选中状态 |
tabindex(别名tabindex) | number \| undefined | undefined | 自定义容器 tabindex;未设置时由_pattern.tabIndex()(即-1或0)决定 |
value | V[](ModelSignal) | [] | 当前选中值数组,支持[(value)]双向绑定,并通过valueChange输出 |
注意tabindex输入在源码中使用了别名alias: 'tabindex',因此模板里应写成[tabindex]="...",这也是 API 报告中"tabIndex": { "alias": "tabindex" }的由来。所有布尔输入均通过booleanAttribute变换,因此支持<ul ngListbox multi wrap>这种“裸属性”写法。
四、焦点管理:roving tabindex 与 activedescendant 两种策略
focusMode决定键盘焦点落在哪里(见 src/aria/listbox/listbox.ts 的 JSDoc 与宿主绑定):
roving(默认):焦点随导航移动到当前激活的ngOption上,各选项的tabindex由ListPattern统一调度(_pattern.tabIndex()),列表容器本身不可聚焦;activedescendant:焦点始终停留在 listbox 容器上,当前项通过宿主属性aria-activedescendant(绑定_pattern.activeDescendant())指示,选项自身保持tabindex="-1"。
对应地,Listbox对外暴露了两个命令式 API:
gotoFirst():把激活项移到列表第一项(内部调用listBehavior.first());gotoIndex(index):按索引导航,索引越界时被钳制到[0, length-1](见Math.min(Math.max(index, 0), patterns.length - 1));scrollActiveItemIntoView(options?):调用激活项元素的scrollIntoView,默认{block: 'nearest'},适合长列表滚动场景。
从源码结构看(src/aria/private/behaviors/list/list.ts),底层List行为聚合了四个子行为:ListFocus(焦点与 tabindex 分配)、ListNavigation(方向键/Home/End 导航与 wrap)、ListSelection(单/多选、范围选择与 anchor 锚点)、ListTypeahead(按键搜索)。这解释了为何一个看似简单的列表组件拥有如此完整的键盘语义。
五、键盘与鼠标交互矩阵
键盘与点击的完整映射实现在 src/aria/private/listbox/listbox.ts 的keydown与clickManager两个 computed 中,可归纳如下:
键盘导航(所有模式)
ArrowUp/ArrowDown(垂直方向,水平方向为ArrowLeft/ArrowRight,RTL 下自动镜像,见prevKey/nextKey对textDirection的判断);Home/End:跳到首/末项;- 单字符按键:触发 typeahead(正则
typeaheadRegexp = /^.$/匹配任意单字符)。
选择语义(selectionMode)
follow模式:方向键移动即同时{selectOne: true}选中该项;explicit模式:Space/Enter切换选中状态;多选时Ctrl/Meta + A全选、Ctrl/Meta + 点击单独切换某一项。
多选范围选择(multi = true 时额外生效)
- 按下
Shift记录 anchor 锚点;Shift + 方向键、Shift + Home/End、Shift + Space、Shift + Enter执行范围选择({selectRange: true}); - 范围选择期间自动关闭 wrap,避免循环定位错乱。
只读模式(readonly = true)
- 只保留导航、Home/End 与 typeahead,所有选择类快捷键被移除,点击仅移动焦点(
goto)。
点击行为
- 单选 + follow:点击即
{selectOne: true};单选 + explicit:点击{toggle: true};多选 + follow:普通点击选中、Ctrl/Command + 点击切换;多选 + explicit:点击即切换。所有点击都通过target.closest('[role="option"]')定位选项。
此外softDisabled=true时禁用项仍可聚焦、不可选中;整个组件disabled时onKeydown与onClick直接短路,仅onFocusIn仍记录交互状态。
六、内置一致性校验(开发模式)
ListboxPattern.validate()(见 src/aria/private/listbox/listbox.ts)在开发模式下(ngDevMode)通过afterRenderEffect自动检测三类错误并在控制台reportViolations:
- 单选框(
multi=false)却选中了多个值; - 选项
value重复(Duplicate option value '...' detected inside ngListbox.); - 选项
id重复。
这能帮助开发者在开发期尽早发现数据配置问题,生产构建(无ngDevMode)自动跳过该开销。
七、测试支持:Listbox Harness
仓库为测试提供了完整的 Component Harness(见 src/aria/listbox/testing/listbox-harness.ts):
ListboxHarness(宿主选择器[ngListbox]):支持getOrientation()、isMulti()、isDisabled()、getActiveDescendantId()、focus()/blur()、getOptions(filters?);ListboxOptionHarness(宿主选择器[ngOption]):支持isSelected()、isDisabled()、getText()、click(),其static with()谓词可按text、selected、disabled过滤选项;- 过滤参数定义于 src/aria/listbox/testing/listbox-harness-filters.ts,完整的交互测试示例见 src/aria/listbox/testing/listbox-harness.spec.ts 与 src/aria/listbox/listbox.spec.ts。
典型测试片段:
const listbox = await loader.getHarness(ListboxHarness); const options = await listbox.getOptions({selected: true}); expect(await options[0].getText()).toBe('Option 1');八、与其他 ARIA 组件的关系
Listbox是仓库@angular/aria系列无障碍组件族的基石之一。源码 JSDoc 明确指向同族的 Autocomplete(guide/aria/autocomplete)、Select(guide/aria/select)与 Multiselect(guide/aria/multiselect);同时ListboxPattern/OptionPattern及其底层List、ListNavigation、ListSelection、ListTypeahead等行为类被提取到 src/aria/private 目录,作为可复用的 UI 行为层供整个 ARIA 组件族共享。理解ngListbox的输入/事件契约,是深入阅读这些上层组件源码的最佳切入点。
九、实践清单与注意事项
- 双向绑定:始终使用
[(value)]绑定选中值数组,组件会主动将value与现存选项同步(源码中afterRenderEffect会过滤掉已不存在的值); - 必填 value:每个
ngOption必须提供唯一的value,否则开发模式会收到重复值告警; - 按需调整默认值:
typeaheadDelay(默认 500ms)与softDisabled(默认 true)、wrap(默认 true)、focusMode(默认 roving)均应按产品交互修改,例如触屏/视障场景更推荐activedescendant; - 长列表:可调用
scrollActiveItemIntoView()在程序化导航(gotoFirst/gotoIndex)后把激活项滚入视野; - 测试:优先使用
ListboxHarness/ListboxOptionHarness断言aria-selected、aria-activedescendant与点击、键盘交互,避免直接操作 DOM。
通过本文对 API 报告逐符号的解读、与 listbox.ts、option.ts、私有行为层 的源码对照,你已经掌握了@angular/aria_listbox从声明式输入、双向模型、键盘/鼠标语义到焦点策略、开发期校验与测试 Harness 的完整知识闭环,可以放心在无障碍要求严格的生产项目中使用或在其基础上二次开发。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考