Angular ARIA Toolbar 深入解析:可访问工具栏的键盘导航、Widget 分组与 RTL 支持
导读
Angular 的 @angular/aria/toolbar 为常见的"文本格式化工具栏""命令面板"等场景提供了符合 ARIA Toolbar 模式的可访问工具栏实现:方向键在控件间移动焦点、Enter/Space 激活控件、Tab 键整体进出工具栏。本文基于仓库中的 Toolbar 官方指南 与 配套示例工程,完整讲解其模板用法、ngToolbar / ngToolbarWidget / ngToolbarWidgetGroup 三个指令的属性与行为,以及软禁用/硬禁用、RTL、组件测试 harness 等进阶能力,读完即可在自己的 Angular 应用中落地一个键盘与读屏完全可用的工具栏。
一、Toolbar 是什么,什么时候该用
Toolbar 是用于分组相关控件与操作并提供键盘导航的容器,典型应用是文本编辑器格式化栏、设计工具命令面板。官方指南中给出的使用判断标准(来自 toolbar.md):
适合使用 Toolbar 的场景
- 多个相关操作:一组控件执行相关功能(例如文本格式化按钮);
- 键盘效率重要:用户希望通过方向键快速在控件间跳转;
- 分组需求:需要用分隔符把控件组织成若干逻辑区段;
- 高频访问:控件在工作流中被反复使用。
不适合的场景
- 只有 2–3 个不相关操作时,普通按钮即可;
- 控件彼此不相关——Toolbar 暗示逻辑分组,把不相关控件放一起会让用户困惑;
- 深层嵌套导航——复杂层级更适合 Menu 或导航组件。
它提供的核心能力包括:方向键导航(Enter/Space 激活)、内置 ARIA 属性支持读屏器、Widget 分组(如互斥单选组、多选开关组)、水平/垂直两种布局、基于 Angular Signals 的响应式状态、RTL(从右到左)语言自动适配,以及可配置的边缘焦点行为(回环或硬性停止)。
二、基本用法:ngToolbar + ngToolbarWidget
2.1 组件导入
工具栏由三个可复用指令/组件组成,均从 @angular/aria/toolbar 导入(见 basic 示例 app.ts):
import {Component} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {}
2.2 模板结构:一个完整的水平工具栏
下面的结构完整复现了 basic 示例的 app.html,包含三个区段:撤销/重做、加粗/斜体/下划线(多选开关组)、对齐方式(互斥单选组),区段之间用 role="separator" 的分隔元素隔开:
<div ngToolbar aria-label="Text Formatting Tools">
<div class="group">
<button ngToolbarWidget value="undo" type="button" aria-label="undo"
class="material-symbols-outlined" translate="no">
undo
</button>
<button ngToolbarWidget value="redo" type="button" aria-label="redo"
class="material-symbols-outlined" translate="no">
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button ngToolbarWidget value="bold" type="button" aria-label="bold"
#bold="ngToolbarWidget"
[aria-pressed]="bold.selected()"
class="material-symbols-outlined" translate="no">
format_bold
</button>
<button ngToolbarWidget value="italic" type="button" aria-label="italic"
#italic="ngToolbarWidget"
[aria-pressed]="italic.selected()"
class="material-symbols-outlined" translate="no">
format_italic
</button>
<button ngToolbarWidget value="underlined" type="button" aria-label="underlined"
#underlined="ngToolbarWidget"
[aria-pressed]="underlined.selected()"
class="material-symbols-outlined" translate="no">
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button ngToolbarWidget role="radio" type="button" value="align left"
aria-label="align left" #leftAlign="ngToolbarWidget"
[aria-checked]="leftAlign.selected()"
class="material-symbols-outlined" translate="no">
format_align_left
</button>
<button ngToolbarWidget role="radio" type="button" value="align center"
aria-label="align center" #centerAlign="ngToolbarWidget"
[aria-checked]="centerAlign.selected()"
class="material-symbols-outlined" translate="no">
format_align_center
</button>
<button ngToolbarWidget role="radio" type="button" value="align right"
aria-label="align right" #rightAlign="ngToolbarWidget"
[aria-checked]="rightAlign.selected()"
class="material-symbols-outlined" translate="no">
format_align_right
</button>
</div>
</div>
配套样式(basic 示例 app.css)仅负责视觉呈现,说明 Toolbar 指令本身不强制样式,外观由你自己的 CSS 决定——这也是同一示例可以派生出 Material 与 Retro 两套皮肤的原因:
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
2.3 关键属性与信号
从示例结构可以归纳出各元素的职责划分:
| 元素/属性 | 作用 |
|---|---|
ngToolbar |
工具栏容器,承载键盘导航;建议提供 aria-label 让读屏器识别区域名称 |
orientation="vertical" |
切换为垂直布局(默认水平),键盘方向键随之切换为上下 |
ngToolbarWidget |
标记一个可聚焦的控件,参与方向键导航;value 标识控件、disabled 表示禁用 |
#bold="ngToolbarWidget" |
通过模板引用拿到 widget 实例,bold.selected() 是信号,返回该控件是否被选中 |
ngToolbarWidgetGroup |
Widget 分组容器;配合 role="radiogroup" 构成互斥单选组,[multi]="true" 允许多选 |
role="separator" |
视觉分隔条,将控件组织为逻辑区段 |
选中的呈现方式遵循 ARIA 惯例:普通开关按钮绑定 [aria-pressed]="widget.selected()";radio 组内控件绑定 role="radio" 与 [aria-checked]="widget.selected()"。
三、垂直工具栏
垂直工具栏把控件自上而下堆叠,适用于侧边栏面板或垂直命令面板,上下方向键负责控件间跳转。开启方式是在容器上加 orientation="vertical"(见 vertical 示例 app.html):
<div ngToolbar orientation="vertical" aria-label="Text Formatting Tools">
<div class="group">
<button ngToolbarWidget value="undo" type="button" aria-label="undo">undo</button>
<button ngToolbarWidget value="redo" type="button" aria-label="redo">redo</button>
</div>
<div class="separator" role="separator"></div>
<!-- 其余控件区段与水平布局写法完全一致 -->
</div>
其余部分(ngToolbarWidget、分组、分隔符)与水平布局写法一致,Material 皮肤(vertical/material)与 Retro 皮肤(vertical/retro)同样是仅改样式、不改结构。
四、Widget 分组:互斥单选与多选
Widget 分组用于承载"共同协作"的控件集合,例如文本对齐方式或列表格式选项。分组维护自身内部状态,同时整体参与工具栏导航。
官方指南中的核心示例:
<!-- 单选模式(radio 组) -->
<div ngToolbarWidgetGroup role="radiogroup" aria-label="Alignment">
<button ngToolbarWidget value="left">Left</button>
<button ngToolbarWidget value="center">Center</button>
<button ngToolbarWidget value="right">Right</button>
</div>
<!-- 多选模式(toggle 组) -->
<div ngToolbarWidgetGroup [multi]="true" aria-label="Formatting">
<button ngToolbarWidget value="bold">Bold</button>
<button ngToolbarWidget value="italic">Italic</button>
<button ngToolbarWidget value="underline">Underline</button>
</div>
multi 输入决定组内是否允许同时选中多个 widget:
- 不设置
multi(默认)时,配合role="radiogroup"形成互斥选择——选中一个会取消另一个; [multi]="true"时形成开关组(toggle group)——每个 widget 可独立开/关。
在 basic 示例的对齐区段中,正是 <div ngToolbarWidgetGroup role="radiogroup" ... aria-label="Text alignment options"> 包裹三个 role="radio" 按钮实现了"左/中/右对齐互斥"这一语义(见 basic/app/app.html)。
五、禁用控件:软禁用 vs 硬禁用
Toolbar 支持两种禁用模式(见 toolbar.md 的 Disabled widgets 一节):
- 软禁用(soft-disabled,默认):widget 仍然可聚焦,但视觉上表示不可用;
- 硬禁用(hard-disabled):widget 被完全移出键盘导航序列。
softDisabled 默认值为 true。若想启用硬禁用模式,在工具栏上设置 [softDisabled]="false"。
disabled 示例 演示了带禁用控件的工具栏——redo 按钮加上原生 disabled 属性,在默认软禁用模式下它仍可被方向键聚焦(读屏器会播报"已禁用"),而在硬禁用模式下方向键会直接跳过它:
<button
disabled
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
选择建议:当"暂不可用"的原因需要让用户感知(例如"撤销"在剪贴板为空时不可用但用户仍可能期待按到它)时保留软禁用;当禁用项纯粹是干扰(例如与当前文档类型无关的控件)时使用硬禁用,避免方向键导航到"死胡同"。
六、RTL(从右到左)语言支持
Toolbar 自动支持 RTL 语言:把 dir="rtl" 放在工具栏容器上即可同时反转视觉布局与键盘导航方向——左方向键移动到下一个控件、右方向键移动到上一个(见 rtl 示例 app.html):
<div ngToolbar dir="rtl" aria-label="Text Formatting Tools">
<!-- 控件结构与其他示例完全一致,无需任何额外改动 -->
</div>
RTL 变体同样提供 Material 与 Retro 两种皮肤,证明方向切换与主题样式正交、互不干扰。
七、测试:使用 ToolbarHarness 编写组件测试
@angular/aria/toolbar/testing 提供组件 harness,让测试直接以"用户视角"操作工具栏。官方指南给出的完整示例如下(harness API 与 Angular CDK testing 体系一致):
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ToolbarHarness} from '@angular/aria/toolbar/testing';
import {MyToolbarComponent} from './my-toolbar'; // 你的组件
describe('MyToolbarComponent', () => {
let fixture: ComponentFixture<MyToolbarComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MyToolbarComponent],
});
fixture = TestBed.createComponent(MyToolbarComponent);
await fixture.whenStable();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should have widgets and allow selection', async () => {
// 加载工具栏 harness
const toolbar = await loader.getHarness(ToolbarHarness);
// 获取全部 widget
const widgets = await toolbar.getWidgets();
expect(widgets.length).toBe(3);
// 点击第一个 widget
await widgets[0].click();
// 校验选中状态
expect(await widgets[0].isSelected()).toBe(true);
});
});
harness 覆盖了测试工具栏最常见的三类断言:widget 数量、模拟点击、选中状态查询,使测试不依赖具体 DOM 细节,只围绕 Toolbar 的交互语义编写。
八、API 参考与延伸阅读
三个核心符号的完整 API 文档见 /api/aria/toolbar/Toolbar、/api/aria/toolbar/ToolbarWidget、/api/aria/toolbar/ToolbarWidgetGroup 对应的 API Reference 页面,键盘交互细节可对照 W3C 官方的 Toolbar ARIA pattern(即文档头部 docs-pill 引用的模式规范)。
仓库内所有可运行示例统一位于 adev/src/content/examples/aria/toolbar 目录,按 basic(水平)、vertical(垂直)、disabled(禁用)、rtl(从右到左)四类场景组织,每类均附 Basic / Material / Retro 三套皮肤,方便直接对照抄录到你的项目中。
落地检查清单
- 用
ngToolbar包裹容器并给出aria-label; - 每个可操作控件加
ngToolbarWidget,按需配value、disabled; - 互斥选项用
ngToolbarWidgetGroup+role="radiogroup"+role="radio",多选场景加[multi]="true"; - 用
selected()信号绑定aria-pressed/aria-checked呈现选中态; - 需要垂直布局时加
orientation="vertical";RTL 应用加dir="rtl"; - 需要禁用项跳过头部焦点时设置
[softDisabled]="false"; - 用
ToolbarHarness编写键盘/点击/选中状态测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00