首页
/ Angular ARIA Toolbar 深入解析:可访问工具栏的键盘导航、Widget 分组与 RTL 支持

Angular ARIA Toolbar 深入解析:可访问工具栏的键盘导航、Widget 分组与 RTL 支持

2026-09-06 14:26:59作者:秋泉律Samson

导读

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 一节):

  1. 软禁用(soft-disabled,默认):widget 仍然可聚焦,但视觉上表示不可用;
  2. 硬禁用(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 变体同样提供 MaterialRetro 两种皮肤,证明方向切换与主题样式正交、互不干扰。

七、测试:使用 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 三套皮肤,方便直接对照抄录到你的项目中。

落地检查清单

  1. ngToolbar 包裹容器并给出 aria-label
  2. 每个可操作控件加 ngToolbarWidget,按需配 valuedisabled
  3. 互斥选项用 ngToolbarWidgetGroup + role="radiogroup" + role="radio",多选场景加 [multi]="true"
  4. selected() 信号绑定 aria-pressed / aria-checked 呈现选中态;
  5. 需要垂直布局时加 orientation="vertical";RTL 应用加 dir="rtl"
  6. 需要禁用项跳过头部焦点时设置 [softDisabled]="false"
  7. ToolbarHarness 编写键盘/点击/选中状态测试。
登录后查看全文
热门项目推荐
相关项目推荐