CKEditor 4 工具栏 Toggle 按钮的无障碍实现:`aria-pressed` 状态与屏幕阅读器验证指南

原创2026-10-14 21:55:551,755 阅读
文章标签:前端UI组件

CKEditor 4 工具栏 Toggle 按钮的无障碍实现:aria-pressed 状态与屏幕阅读器验证指南

导读

本文以 CKEditor 4 官方仓库中的屏幕阅读器手动测试文档(tests/plugins/button/manual/togglebuttonscreenreader.md)为核心,系统讲解工具栏「切换型按钮」(toggle button)在无障碍可访问性(ARIA)层面的设计与验证方法:包括 isToggle 按钮定义、aria-pressed 属性的动态维护机制,以及如何使用键盘与屏幕阅读器(VoiceOver、NVDA、JAWS)完成端到端验收。读完本文,你将掌握 CKEditor 4 中切换按钮的完整无障碍行为模型,并能复现官方手动测试与自动化测试来验证自己的自定义按钮。

背景:切换型按钮(Toggle Button)与可访问性树

工具栏中的「加粗」「斜体」这类按钮具有两种稳定状态——按下(pressed/on)与未按下(unpressed/off)。这类按钮在 WAI-ARIA 中被定义为 toggle button,需要通过 aria-pressed 属性向辅助技术(屏幕阅读器)暴露当前状态,而不是简单依赖视觉样式(如高亮背景)来传达状态。

在 CKEditor 4.19.0 之前,工具栏按钮在浏览器的可访问性树中只被暴露为普通按钮,屏幕阅读器无法告知用户该按钮是否处于激活状态。该问题由 GitHub issue #2444 追踪,并在 4.19.0 版本中得到修复——官方变更记录明确指出:

#2444:Togglable toolbar buttons are now exposed as toggle buttons in the browser's accessibility tree.(可切换的工具栏按钮现在在浏览器的可访问性树中暴露为切换按钮。)

实现位于 plugins/button/plugin.js:当按钮定义包含 isToggle: true 时,渲染模板会输出 aria-pressed="false" 初始属性,并在状态变化时动态同步该属性。

屏幕阅读器手动测试:官方验收步骤

官方为本次特性提供了专门面向屏幕阅读器的手动测试页面 togglebuttonscreenreader.html 与测试说明文档 togglebuttonscreenreader.md。该页面声明依赖插件:wysiwygarea、toolbar、link(通过 @bender-ckeditor-plugins 标签),并用 @bender-ui: collapsed 控制测试 UI 的展示形态。

测试步骤与预期结果

测试页中注册了一个名为 toggleButton 的切换按钮,绑定命令在 CKEDITOR.TRISTATE_OFF 与 CKEDITOR.TRISTATE_ON 之间往返切换。手动测试的完整步骤与预期如下:

  1. 将焦点移动到编辑器工具栏中的按钮上
    • 预期:屏幕阅读器将该按钮播报为「可切换」(togglable),且当前为关闭/未按下(off/unpressed)。
  2. 通过键盘激活该按钮(回车或空格)
  3. 将焦点移回该按钮
    • 预期:屏幕阅读器播报为「可切换」,且当前为打开/已按下(on/pressed)。
  4. 再次通过键盘激活该按钮
  5. 将焦点移回该按钮
    • 预期:屏幕阅读器播报为「可切换」,且当前回到关闭/未按下(off/unpressed)。

注意测试文档开头标注:This test is intended for screen reader(本测试专为屏幕阅读器设计),即该用例的价值在于验证辅助技术端的实际播报,而非单纯检查 DOM 属性。

各主流屏幕阅读器的示例播报

原文档同时给出了三个主流屏幕阅读器的典型输出,可直接用于验收对照:

屏幕阅读器 未按下(off) 已按下(on)
VoiceOver(macOS) <button name>, toggle button <button name>, toggle button, selected
NVDA <button name>, toggle button, not pressed <button name>, toggle button, pressed
JAWS <button name>, toggle button <button name>, toggle button, pressed

其中 <button name> 为按钮的可访问名称(即 label,通过 aria-labelledby 关联到 cke_button_label 元素,见 plugins/button/plugin.js)。可以看到三家屏幕阅读器均会播报「toggle button」语义,并用各自的措辞区分 pressed 状态——这正是 aria-pressed 正确暴露后的效果。

源码实现:aria-pressed 的渲染与同步

1. 按钮模板的初始输出

在 plugins/button/plugin.js 的 render() 方法中,按钮模板参数包含:

toggleAriaHtml: this.isToggle ? 'aria-pressed="false"' : ''

即:只有当按钮定义中显式声明 isToggle: true 时,生成的 <a class="cke_button ..."> 元素才会带 aria-pressed="false" 初始值。该属性被注入模板中的 {toggleAriaHtml} 占位符(plugins/button/plugin.js)。

同时模板中还固定输出了其他无障碍属性:

role="button"
aria-labelledby="{id}_label"
aria-describedby="{id}_description"
aria-haspopup="{hasArrow}"
aria-disabled="{ariaDisabled}"

2. 状态切换时的动态同步

按钮状态由 setState() 方法维护(plugins/button/plugin.js)。当状态变化时,除了切换 cke_button_on/off/disabled CSS 类之外,还会同步 ARIA 属性:

if ( this.isToggle && !this.hasArrow ) {
    // Note: aria-pressed attribute should not be added to menuButton instances. (ticket #11331)
    // For other buttons, do not remove the attribute, instead set its value (#2444).
    element.setAttribute( 'aria-pressed', state === CKEDITOR.TRISTATE_ON );
}

两个关键设计点:

  • aria-pressed 只在 isToggle 且无下拉箭头(!hasArrow)的按钮上维护。带菜单的按钮(menu button)不应使用 aria-pressed,而是通过 aria-expanded 表达菜单开合状态——该限制源自 ticket #11331,并在代码注释中明确说明。
  • 按 #2444 的要求,状态变化时是「更新值」而非「移除属性」:始终将 aria-pressed 设置为 true(TRISTATE_ON)或 false(其余状态),保证可访问性树中按钮始终以 toggle button 语义呈现。

命令层的状态机定义在 core/command.js:TRISTATE_DISABLED(禁用)、TRISTATE_ON(启用且激活)、TRISTATE_OFF(启用且未激活)。按钮通过监听命令的 state 事件同步自身状态(plugins/button/plugin.js)。

3. 与底层 CKEDITOR.dom.element#setState 的关系

值得区分的是:按钮 UI 的 setState 与 DOM 元素的 setState(core/dom/element.js)职责不同。DOM 层的 setState(state, base, useAria) 在 useAria 为真时会切换 aria-pressed/aria-disabled 属性,但按钮 UI 层为了满足 #2444(不删除属性、仅更新值)以及 #11331(menu button 不加 aria-pressed),在 plugins/button/plugin.js 中先调用 element.setState( state, 'cke_button' ) 完成类名切换,再自行精确控制 aria-disabled 与 aria-pressed 的赋值。

如何定义一个 Toggle 按钮

isToggle 是按钮定义的可选属性,自 4.19.0 版本起支持(见 plugins/button/plugin.js 的 API 文档)。手动测试页 togglebuttonscreenreader.html 给出了最小可复现示例:

CKEDITOR.replace( 'editor', {
    toolbar: [ [ 'toggleButton' ] ],
    on: {
        pluginsLoaded: function( evt ) {
            evt.editor.addCommand( 'toggleButton', {
                exec: function() {
                    if ( this.state === CKEDITOR.TRISTATE_OFF ) {
                        this.setState( CKEDITOR.TRISTATE_ON );
                    } else {
                        this.setState( CKEDITOR.TRISTATE_OFF );
                    }
                }
            } );

            evt.editor.ui.addButton( 'toggleButton', {
                label: 'Sample button',
                icon: 'Link',
                isToggle: true,
                command: 'toggleButton'
            } );
        }
    }
} );

要点:

  • isToggle: true 使按钮在无障碍语义上成为切换按钮;
  • command 指向一个自定义命令,其 exec 在 TRISTATE_OFF 与 TRISTATE_ON 之间切换命令状态;
  • 按钮监听命令状态变化自动刷新自身 UI 与 aria-pressed。

实际产品中,basicstyles 插件(加粗、斜体、下划线、删除线等)正是通过 isToggle: true 声明按钮的,见 plugins/basicstyles/plugin.js:

editor.ui.addButton( buttonName, {
    isToggle: true,
    label: buttonLabel,
    command: commandName,
    toolbar: 'basicstyles,' + ( order += 10 )
} );

其余使用 isToggle 的内置插件还包括 blockquote、justify、list、bidi、maximize、sourcearea、copyformatting 等(可在 plugins 目录下检索 isToggle 确认)。

自动化回归:Bender 测试对 ARIA 的守护

除了手动测试,仓库还提供了自动化测试来防止 aria-pressed 行为回归:

  • tests/plugins/button/aria.js 通过 buttonTools.createAriaPressedTests() 为 toggle_btn 生成 4 个用例:
    1. 初始状态 → aria-pressed="false";
    2. 切换到 TRISTATE_ON → aria-pressed="true";
    3. 切换回 TRISTATE_OFF → aria-pressed="false";
    4. 先 TRISTATE_ON 再 TRISTATE_DISABLED → 仍为 "false"(禁用态不再表达为按下)。
  • 测试辅助工具定义在 tests/plugins/button/_helpers/buttontools.js,assertAttributes() 同时校验 role="button"、aria-disabled、aria-haspopup、aria-labelledby 等标准属性(tests/plugins/button/_helpers/buttontools.js)。

这些用例与手动测试互补:自动化保证 DOM 层 aria-pressed 数值正确,手动测试保证屏幕阅读器实际播报符合预期。

配套手动测试与延伸阅读

围绕按钮无障碍行为,仓库还提供了多组关联手动测试,可一起运行验证:

  • togglebutton.md:单按钮场景下,页面上方实时显示 aria-pressed 值,点击按钮后应依次为 false → true → false;
  • togglebuttons.md:在带完整工具栏(basicstyles、bidi、blockquote、copyformatting、justify、list、sourcearea 等)的编辑器中,用表格实时列出每个按钮的 aria-pressed 值,验证真实环境下状态同步;
  • menubuttonscreenreader.md:验证带菜单按钮(menu button)不误报为 pressed/expanded,涉及 aria-haspopup 与 aria-expanded(对应 4.19.1 的 #5144 修复,见 ariaexpanded.md);
  • label.md:验证按钮标签与菜单开合时的朗读行为(aria-expanded、键盘 alt+f10 聚焦工具栏、space 打开、esc 关闭)。

这些测试均基于 Bender 手动测试框架运行(@bender-tags、@bender-ui 为 Bender 标签语法,测试框架配置见 bender.js 与 package.json)。

常见问题与验收要点

  • 为什么我的自定义按钮屏幕阅读器不播报 toggle 状态? 检查按钮定义是否包含 isToggle: true——没有该声明,渲染模板不会输出 aria-pressed,按钮在可访问性树中只是普通按钮。
  • 带下拉菜单的按钮是否需要 aria-pressed? 不需要。代码注释明确限制 this.isToggle && !this.hasArrow(plugins/button/plugin.js),菜单按钮应使用 aria-expanded 表达展开状态。
  • 按钮禁用时 aria-pressed 应是什么值? 按自动化测试的断言(tests/plugins/button/_helpers/buttontools.js),禁用态下 aria-pressed 保持 "false",同时 aria-disabled="true"。
  • 验收标准:以本文「测试步骤与预期结果」一节为准,至少覆盖 VoiceOver、NVDA、JAWS 三家中你目标用户常用的一款,并核对上表中对应的播报文案。
登录后查看全文
ckeditor4