Angular Components 无障碍 Tabs 组件@angular/aria_tabs公开 API 完全解析
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
本文以仓库中 API Extractor 生成的黄金报告 goldens/aria/tabs/index.api.md 为骨架,深入解析 Angular Components 项目中@angular/aria无障碍标签页组件(ARIA Tabs Pattern)的完整公开 API 表面。这份报告并非手写文档,而是由 API Extractor 自动生成的类型级契约,逐条列出了一个指令模块对外暴露的全部类、信号输入、输出与注入令牌。阅读本文后,你将能够:理解ngTabs/ngTabList/ngTab/ngTabPanel/ngTabContent五个指令的职责划分与协作方式;掌握每个输入信号(orientation、focusMode、selectionMode、wrap、softDisabled、selectedTab等)的取值与默认值;并透过 src/aria/private/tabs/tabs.ts 的 UI Pattern 实现看清无障碍交互(焦点管理、键盘导航、选区)在底层是如何被封装与驱动的。
文档定位:一份自动生成的 API 契约
goldens/aria/tabs/index.api.md的文件头明确声明:
Do not edit this file. It is a report generated by API Extractor.
这意味着该文档是对src/aria/tabs/包公开导出内容的机器可读快照,任何对公开 API 的改动都会使该黄金报告与源码不一致,从而在 CI(approve-api-golden类检查)中暴露。因此这份文档本身就是"公开 API 边界"的最权威索引:凡是在其中出现的符号,都是消费者可以稳定依赖的接口;凡是标注(undocumented)的成员(如ngOnInit、ngOnDestroy、_pattern等),则属于框架生命周期或内部实现细节,不应作为公共契约使用。
对照 src/aria/tabs/public-api.ts 可以看到,该包公开导出了 7 个符号:5 个指令(Tabs、TabList、Tab、TabPanel、TabContent)与 2 个注入令牌(TABS、TAB_LIST),另外通过ɵɵDeferredContent/ɵɵDeferredContentAware复导出内部的延迟内容基础指令。下文按报告中的出现顺序逐一展开。
指令矩阵:五个指令如何分工
从 API 报告与源码看,标签页交互被拆分为五个互相配合的指令,形成三层结构:
| 指令 | 选择器 | 导出声明 | 职责 |
|---|---|---|---|
Tabs | [ngTabs] | ngTabs | 顶层容器,协调 TabList 与 TabPanel 的注册与配对 |
TabList | [ngTabList] | ngTabList | 管理 Tab 集合、焦点移动、选择策略、方向与键盘导航 |
Tab | [ngTab] | ngTab | 单个可选中标签,暴露选中/激活状态与open() |
TabPanel | [ngTabPanel] | ngTabPanel | 存放标签对应内容的面板,负责可见性与inert |
TabContent | ng-template[ngTabContent] | ngTabContent | 结构型指令,实现内容的懒加载渲染 |
从源码结构推断,三者的注入关系是:Tabs通过TABS令牌暴露自身(src/aria/tabs/tabs.ts),TabList注入TABS并在ngOnInit时调用_register(this)注册自己;TabList又通过TAB_LIST令牌暴露自身,Tab注入TAB_LIST完成注册。TabPanel直接注入TABS,在ngOnInit时向 Tabs 的内容集合注册。TABS与TAB_LIST两个令牌定义于 src/aria/tabs/tab-tokens.ts。
Tabs(ngTabs):容器与配对枢纽
Tabs类本身只实现OnDestroy,公开成员非常精简:
element: HTMLElement—— 宿主元素引用;_collection: SortedCollection<TabPanel>—— 按 DOM 顺序维护的 TabPanel 有序集合;_tabList: WritableSignal<TabList | undefined>—— 当前注册的 TabList;_panelMap/_tabMap/_tabPatterns/_tabPanelPatterns—— 一组computed信号,把"值 → 底层 Pattern"的映射关系响应式地暴露给子指令;_register(child: TabList)/_unregister()—— 由 TabList 在生命周期中调用,维护_tabList信号。
从源码可以看到,Tabs通过afterNextRender启动SortedCollection的 DOM 观察(startObserving),并维护_panelMap——这是Tab校验"自己的 value 是否有对应面板"的数据来源(src/aria/tabs/tab.ts)。典型的模板用法如下(示例源自 src/aria/tabs/tabs.ts):
<div ngTabs> <ul ngTabList [(selectedTab)]="selectedTabValue"> <li ngTab value="tab1">Tab 1</li> <li ngTab value="tab2">Tab 2</li> <li ngTab value="tab3">Tab 3</li> </ul> <div ngTabPanel value="tab1"> <ng-template ngTabContent>Content for Tab 1</ng-template> </div> <div ngTabPanel value="tab2"> <ng-template ngTabContent>Content for Tab 2</ng-template> </div> <div ngTabPanel value="tab3"> <ng-template ngTabContent>Content for Tab 3</ng-template> </div> </div>TabList(ngTabList):输入信号最密集的控制核心
TabList是输入选项最多的指令,全部输入均为 Angular 信号输入(input()/model()),在 API 报告中体现为InputSignal/ModelSignal类型。下表汇总每个输入的含义、类型与默认值(依据 src/aria/tabs/tab-list.ts 与 API 报告):
| 输入 | 类型 | 默认值 | 说明 |
|---|---|---|---|
orientation | 'horizontal' \| 'vertical' | 'horizontal' | 标签列表方向,映射为宿主aria-orientation |
wrap | boolean(经booleanAttribute转换) | true | 焦点移动是否在两端循环 |
softDisabled | boolean(转换) | true | true时禁用项仍可聚焦但不可交互;false时导航直接跳过禁用项 |
focusMode | 'roving' \| 'activedescendant' | 'roving' | 焦点策略:roving通过tabindex移动焦点到激活标签;activedescendant焦点停留在容器,用aria-activedescendant指示 |
selectionMode | 'follow' \| 'explicit' | 'follow' | 选择策略:follow聚焦即选中;explicit需用户显式操作(点击或空格)才选中 |
selectedTab | string \| undefined(model) | undefined | 双向绑定的当前选中标签 value,输出别名为selectedTabChange |
disabled | boolean(转换) | false | 是否整体禁用标签列表 |
报告中selectedTab的声明带有输出{ "selectedTab": "selectedTabChange" },即模板中应写作[(selectedTab)]="value"或[selectedTab]="value" (selectedTabChange)="handler($event)"。
公开方法:open与findTab
open(value: string): boolean—— 按 value 打开对应标签(面板),返回是否成功;findTab(value?: string): Tab | undefined—— 在有序集合中按 value 查找Tab。
宿主行为
TabList的宿主绑定将交互事件转发给底层的TabListPattern(src/aria/tabs/tab-list.ts):
role="tablist"、aria-disabled、aria-orientation、aria-activedescendant、tabindex均由_pattern计算得出;(keydown)、(click)、(focusin)统一交由_pattern.onKeydown/onClick/onFocusIn处理,实现方向键导航、Home/End、空格/回车选择等无障碍键盘交互。
值得注意的实现细节:selectedTab模型与内部选中 Pattern 之间通过linkedSignal双向同步,并在afterRenderEffect的write阶段回写(src/aria/tabs/tab-list.ts),这保证了"外部绑定 value ↔ 内部选中项"始终一致。
Tab(ngTab):可选项的输入与状态
Tab的公开 API(src/aria/tabs/tab.ts):
| 成员 | 类型 | 说明 |
|---|---|---|
id | InputSignal<string> | 全局唯一标识,默认由 CDK 的_IdGenerator生成(前缀ng-tab-) |
disabled | InputSignalWithTransform<boolean, unknown> | 是否禁用,经booleanAttribute转换 |
value | InputSignal<string>(必填) | 唯一值,用于关联对应ngTabPanel;API 报告中required: true |
active | Signal<boolean>(只读) | 是否为当前焦点项,映射到宿主data-active |
selected | Signal<boolean>(只读) | 是否被选中,映射到aria-selected |
element | HTMLElement | 宿主元素引用 |
open() | 方法 | 打开该标签(等价于调用底层TabPattern.open()) |
宿主属性role="tab"、tabindex、aria-controls、aria-disabled均由_pattern派生:aria-controls指向关联 TabPanel 的 id,aria-disabled与禁用状态同步。
两个工程细节值得注意:
- 按钮防表单提交:构造函数中若宿主是
<button>且未显式设置type,会自动补上type="button",避免误触发表单提交(src/aria/tabs/tab.ts)。 - 开发期校验:在
ngDevMode下通过afterRenderEffect检查"ngTab的 value 是否有对应ngTabPanel",无匹配时向控制台报告违规(reportViolations)。
TabPanel(ngTabPanel):可见性与延迟内容
TabPanel(src/aria/tabs/tab-panel.ts):
| 成员 | 类型 | 说明 |
|---|---|---|
id | InputSignal<string> | 全局唯一标识,默认由_IdGenerator生成(前缀ng-tab-panel-) |
value | InputSignal<string>(必填) | 与ngTab的 value 匹配 |
visible | Signal<boolean>(只读) | 面板是否可见,computed(() => !this._pattern.hidden()) |
宿主绑定体现了 ARIA 与隐藏语义的结合(src/aria/tabs/tab-panel.ts):
role="tabpanel"、tabindex、aria-labelledby(指向控制它的标签 id);- 隐藏时设置
inert属性:'[attr.inert]': '!visible() ? true : null',将隐藏面板从可访问性树中彻底移除(源码注释明确说明视觉隐藏仍需额外 CSS 配合)。
TabPanel还通过hostDirectives挂载了DeferredContentAware(输入preserveContent),并在afterRenderEffect的write阶段把visible()同步给延迟内容机制——这正是"懒加载内容"的接入点。开发期同样有两条违规校验:面板内必须存在ngTabContent结构指令;面板的 value 必须有对应ngTab。
TabContent(ng-template[ngTabContent]):懒加载内容
TabContent是纯声明式指令(src/aria/tabs/tab-content.ts),选择器为ng-template[ngTabContent],内部仅通过hostDirectives: [DeferredContent]复用 CDK/私有包中的延迟内容机制:
内容只有在标签首次激活时才会渲染(lazy loading),激活后渲染结果被缓存复用。
配合TabPanel的preserveContent输入,开发者可以控制内容在切换后是否保留在 DOM 中。
注入令牌:TABS 与 TAB_LIST
API 报告中的两个常量:
TABS: InjectionToken<Tabs>—— 向子指令暴露 Tabs 容器(Tabs指令在providers中useExisting提供);TAB_LIST: InjectionToken<TabList>—— 向子指令暴露 TabList(TabList指令的providers中提供)。
这套令牌体系(src/aria/tabs/tab-tokens.ts)允许Tab以inject(TAB_LIST)拿到父级列表、再经_tabsParent间接访问 Tabs 的映射,形成清晰的依赖方向,也便于测试中替换。
底层原理:UI Pattern 架构与 Behavior 组合
API 报告中所有_pattern成员都指向src/aria/private/tabs/tabs.ts中的三个纯 TypeScript 类(不依赖 Angular 运行时):
TabPattern—— 维护id、disabled、active、selected、tabIndex、controls(关联面板 id)等派生信号,expanded用linkedSignal与 TabList 的选中项同步;TabPanelPattern——hidden、tabIndex(隐藏时为 -1)、labelledBy;TabListPattern—— 组合ListFocus、ListNavigation、ListExpansion等 Behavior 类,实现焦点管理、方向键导航(含 wrap/softDisabled 语义)、选中与展开联动。
这正是 src/aria/private/ui-pattern-rules.md 描述的架构理念:无障碍模式(Accessibility Patterns)→ Behavior 类(封装导航、选择等通用行为)→ UI Pattern 类(组合 Behavior 实现完整模式)。可以推断,aria/private/tabs/tabs.spec.ts即是对这些 Pattern 行为的单元测试,而指令层只是将信号输入与宿主 DOM/ARIA 绑定接到 Pattern 上。
测试与 Harness 支持
src/aria/tabs/目录提供了完整测试设施:
- src/aria/tabs/tabs.spec.ts —— 指令级组件测试;
- src/aria/tabs/testing/tabs-harness.ts 与 tabs-harness.spec.ts、tabs-harness-filters.ts —— 基于 CDK Component Harness 的测试工具,供使用者在自己的测试中通过
TabsHarness定位标签、读取选中状态、触发打开操作,无需直接操作 DOM 细节。这与 Angular Components 一贯的 harness 测试风格一致(@angular/cdk/testing体系)。
组合实战示例
综合全部 API,一个可运行的最小完整示例(含显式选择模式与回调):
<div ngTabs> <ul ngTabList [(selectedTab)]="current" orientation="horizontal" focusMode="roving" selectionMode="explicit" wrap="true" (selectedTabChange)="onTabChanged($event)" > <li ngTab value="overview">概览</li> <li ngTab value="docs" [disabled]="docsDisabled">文档</li> </ul> <div ngTabPanel value="overview"> <ng-template ngTabContent> <p>概览内容——首次激活时才渲染。</p> </ng-template> </div> <div ngTabPanel value="docs" preserveContent> <ng-template ngTabContent> <p>文档内容——切换后保留在 DOM。</p> </ng-template> </div> </div>对应组件类:
import {Component} from '@angular/core'; @Component({...}) export class TabsDemo { current: string | undefined = 'overview'; docsDisabled = false; onTabChanged(value: string | undefined) { console.log('selected:', value); } }要点回顾:selectedTab是唯一支持双向绑定的输入(输出名selectedTabChange);value是ngTab与ngTabPanel建立关联的键,必须唯一且相互匹配,否则开发模式下会收到reportViolations输出的控制台警告;键盘与 ARIA 属性(role、aria-selected、aria-controls、aria-activedescendant、inert等)全部由指令自动管理,无需手写。
小结
@angular/aria_tabs的公开 API 表面在 goldens/aria/tabs/index.api.md 中被完整、精确地固化为契约:五个指令各司其职,两个注入令牌完成层级通信,信号输入覆盖方向、焦点、选择、禁用与换行等全部 WAI-ARIA Tabs Pattern 关键维度;底层则以 src/aria/private/tabs/tabs.ts 的 UI Pattern 类承载所有交互逻辑,将无障碍实现从 Angular 指令层解耦,兼顾了可测试性与可复用性。对于希望构建无障碍标签页界面的开发者,直接依据上文表格与示例使用ngTabs系列指令,即可获得符合 ARIA 规范的开箱即用能力。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考