CKEditor 4 工具栏开关按钮与 aria-pressed 无障碍状态:从手动测试到源码实现
CKEditor 4 工具栏开关按钮与 aria-pressed 无障碍状态:从手动测试到源码实现
本文围绕 CKEditor 4 中开关型(toggle)工具栏按钮的无障碍属性
aria-pressed展开,以仓库中对应的 Bender 手动测试文档 togglebutton.md 为主线,结合 button 插件源码、配套测试页面与单元测试,完整讲解这一特性(4.19.0 新增,issue #2444)的背景、验证方法、实现原理与真实应用。读完本文,你将掌握如何运行该手动测试、如何在自定义按钮中启用isToggle、以及aria-pressed在按钮生命周期内如何随状态切换变化。
特性背景:为什么工具栏按钮需要 aria-pressed
CKEditor 4 的工具栏按钮(如“加粗 Bold”“斜体 Italic”“引用 Blockquote”等)在视觉上存在“按下/未按下”的两种形态,帮助用户判断当前格式是否生效。但对于依赖屏幕阅读器的用户,仅靠视觉变化无法感知状态差异——屏幕阅读器只会把按钮读作“按钮”,而不会告知用户该功能当前是“开”还是“关”。
在 CHANGES.md 的 4.19.0 版本记录中,这一痛点被明确修复:
- 新特性(#2444):可切换的工具栏按钮现在会在浏览器的无障碍树(accessibility tree)中暴露为 toggle button(CHANGES.md)。
- 缺陷修复(#4543):修复了屏幕阅读器因缺少“该功能当前是开还是关”的信息而无法正确播报工具栏按钮切换状态的问题(CHANGES.md)。
实现方式正是 WAI-ARIA 标准中的 aria-pressed 属性:它为按钮提供 true(按下/开启)、false(未按下/关闭)两种状态值,辅助技术据此向用户播报切换状态。本文关联的手动测试文档 togglebutton.md,正是专门用于验证这一行为的人工测试用例。
关联文档解读:togglebutton.md 手动测试
仓库中的手动测试文档 tests/plugins/button/manual/togglebutton.md 全文如下:
@bender-tags: 4.19.0, 2444, feature, button
@bender-ui: collapsed
@bender-ckeditor-plugins: wysiwygarea, toolbar, link
1. Wait for editor to fully load.
1. Check the info about `[aria-pressed]` attribute's value that is above the editor.
**Expected** The value equals `false`.
**Unexected** The value equals `true` or is not set.
1. Click the button in the toolbar.
**Expected** The value equals `true`.
**Unexected** The value equals `false` or is not set.
1. Click the button once more.
**Expected** The value equals `false`.
**Unexected** The value equals `true` or is not set.
这段文档由三部分构成,下面逐一拆解其含义。
Bender 元数据头(front matter)
@bender-tags: 4.19.0, 2444, feature, button:标记该测试关联的版本(4.19.0)、GitHub issue 编号(#2444)、类型(feature)与涉及模块(button),用于 Bender 测试框架的筛选与归组。@bender-ui: collapsed:声明该测试的 UI 面板默认折叠,仅展示精简界面,便于专注被测内容。@bender-ckeditor-plugins: wysiwygarea, toolbar, link:指定测试页面中编辑器需要加载的插件——wysiwygarea提供所见即所得编辑区,toolbar提供工具栏容器,link则用于为测试按钮复用其图标(见下文 HTML 剖析)。
测试步骤与预期
文档的验证逻辑是一个“开→关”的完整循环,核心断言是:编辑区上方实时显示的 aria-pressed 值必须始终与按钮的按下状态一致。
| 步骤 | 操作 | 预期 aria-pressed 值 |
非预期 |
|---|---|---|---|
| 1 | 等待编辑器完全加载 | false(初始状态) |
true 或未设置 |
| 2 | 单击工具栏中的按钮 | true(按下/开启) |
false 或未设置 |
| 3 | 再次单击按钮 | false(弹起/关闭) |
true 或未设置 |
注意文档中“非预期”的判据包含 “未设置” 这一情况:若按钮缺少 aria-pressed 属性,或属性值与状态不符,均视为测试失败。这正对应 4.19.0 之前的历史行为——当时的开关按钮在无障碍树中并不具备切换语义。
配套测试页面:togglebutton.html 的完整实现
手动测试文档对应的运行页面是 tests/plugins/button/manual/togglebutton.html,它完整实现了上述所有步骤的支撑环境,可作为自定义验证页面的直接模板:
<p><code>[aria-pressed]</code> value: <span id="aria-pressed-value">editor is loading…</span></p>
<div id="editor">
<p>Whatever</p>
</div>
<script>
( function() {
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 );
}
renderButtonState();
}
} );
evt.editor.ui.addButton( 'toggleButton', {
label: 'Toggle button',
icon: 'Link',
isToggle: true,
command: 'toggleButton'
} );
},
instanceReady: renderButtonState
}
} );
function renderButtonState() {
var button = CKEDITOR.document.findOne( '.cke_button__togglebutton' ),
stateIndicator = CKEDITOR.document.getById( 'aria-pressed-value' ),
currentState = button.getAttribute( 'aria-pressed' )
stateIndicator.setHtml( currentState );
}
} )();
</script>
页面设计要点如下:
- 状态指示器:编辑区上方放置
<span id="aria-pressed-value">,renderButtonState()通过CKEDITOR.document.findOne( '.cke_button__togglebutton' )定位工具栏按钮 DOM 节点,读取其aria-pressed属性值并写入该 span,实现文档所述“编辑区上方的值实时显示”。 - 自定义命令:
toggleButton命令在exec中依据当前状态在CKEDITOR.TRISTATE_OFF与CKEDITOR.TRISTATE_ON之间翻转,模拟真实开关按钮(如 Bold)的行为。 - 按钮定义:
editor.ui.addButton( 'toggleButton', { ... isToggle: true, command: 'toggleButton' } )——isToggle: true是本次特性的关键开关,它让按钮进入“可切换”模式;icon: 'Link'表示直接复用 link 插件的图标(这也是元数据头声明加载link插件的原因)。 - 渲染时机:
instanceReady时先渲染一次初始值(预期false),每次单击命令执行后再次渲染,从而让测试人员肉眼比对false → true → false的完整变化。
源码实现原理:button 插件如何维护 aria-pressed
手动测试验证的行为,由 plugins/button/plugin.js 中的两处核心逻辑实现。
渲染模板:初始化即写入 aria-pressed="false"
按钮 HTML 模板中预留了 {toggleAriaHtml} 占位符(plugins/button/plugin.js#L19),在渲染参数阶段按需填充:
toggleAriaHtml: this.isToggle ? 'aria-pressed="false"' : ''
(plugins/button/plugin.js#L316)
也就是说:只有当按钮定义了 isToggle 时,aria-pressed="false" 才会作为初始值写入按钮元素;普通按钮不包含该属性。这解释了文档中“属性未设置”为何会被判定为非预期——它只可能出现在未启用 isToggle 的按钮上。
setState:状态切换时同步属性
当命令状态变化触发 setState 时(plugins/button/plugin.js#L333-L355),插件在更新视觉状态的同时同步无障碍属性:
setState: function( state ) {
if ( this._.state == state )
return false;
this._.state = state;
var element = CKEDITOR.document.getById( this._.id );
if ( element ) {
element.setState( state, 'cke_button' );
element.setAttribute( 'aria-disabled', state == CKEDITOR.TRISTATE_DISABLED );
if ( this.isToggle && !this.hasArrow ) {
// Note: aria-pressed attribute should not be added to menuButton instances. (https://dev.ckeditor.com/ticket/11331).
// For other buttons, do not remove the attribute, instead set its value (#2444).
element.setAttribute( 'aria-pressed', state === CKEDITOR.TRISTATE_ON );
}
return true;
} else {
return false;
}
},
(plugins/button/plugin.js#L345-L349)
这里有三个值得注意的实现细节:
- 布尔映射:
aria-pressed的值被设置为state === CKEDITOR.TRISTATE_ON,即状态为TRISTATE_ON时写true,否则写false——与手动测试文档的预期完全对应。 - 只更新不删除:源码注释明确说明,即使按钮从按下回到弹起状态,也不移除属性,而是把值重置为
false。这与文档中“值等于false”而非“未设置”的预期保持一致。 - 排除菜单按钮:
if ( this.isToggle && !this.hasArrow )表明带下拉箭头(hasArrow)的菜单/面板按钮不设置aria-pressed。这源自历史问题 #11331:菜单按钮选中时应改为更新标签文本,而不是使用aria-pressed属性(相关记录见 CHANGES.md#L2000)。
isToggle 的默认值与 API 文档
在构造函数中,isToggle 默认值为 false(plugins/button/plugin.js#L86),即所有按钮默认都不是开关型,行为向后兼容。API 文档将其描述为(plugins/button/plugin.js#L439-L440):
@param {Boolean} [definition.isToggle=false]Indicates if the button should be treated as a toggle one (button that can be switched on and off, e.g. the "Bold" button). This option is supported since the 4.19.0 version.
即:isToggle 自 4.19.0 起可用,值为 true 时按钮可被反复按下/弹起(典型例子就是“加粗”按钮)。
单元测试佐证:四种状态的自动化断言
除手动测试外,仓库还提供了自动化单元测试来锁定该行为。核心测试用例生成器位于 tests/plugins/button/_helpers/buttontools.js#L51-L93 的 createAriaPressedTests(),它为每个开关按钮生成四组断言:
| 测试用例 | 触发方式 | 期望 aria-pressed |
|---|---|---|
| 初始状态 | 不执行任何状态变更 | false |
| 切换开启后 | 执行 setState( CKEDITOR.TRISTATE_ON ) |
true |
| 切换关闭后 | 依次执行 TRISTATE_ON、TRISTATE_OFF |
false |
| 开启状态下被禁用 | 依次执行 TRISTATE_ON、TRISTATE_DISABLED |
false |
其中“开启后被禁用”这一用例颇具深意:按钮虽处于 TRISTATE_DISABLED,但 aria-pressed 回落到 false,说明禁用态不会让按钮“卡死”在按下语义上。
这些用例被注入到 tests/plugins/button/aria.js#L98:
CKEDITOR.tools.extend( tests, buttonTools.createAriaPressedTests( 'test_editor', [ 'toggle_btn' ] ) );
测试页面在 pluginsLoaded 阶段通过 editor.ui.addButton( 'toggle_btn', { label: 'toggle button', isToggle: true } ) 注册了开关按钮(tests/plugins/button/aria.js#L29-L32),随后逐一验证属性值。断言辅助函数 assertAttributes(tests/plugins/button/_helpers/buttontools.js#L14-L34)会检查属性是否存在并比对期望值。
真实插件中的应用:哪些按钮启用了 isToggle
在 CKEditor 4 官方插件中,isToggle: true 已被广泛采用,可以直接在源码中检索到(search isToggle 可全量枚举)。代表性示例:
- basicstyles(加粗、斜体、下划线等):在注册每个按钮时设置
isToggle: true,与对应的 style 命令状态联动(plugins/basicstyles/plugin.js#L39-L44); - blockquote(引用块):
isToggle: true(plugins/blockquote/plugin.js#L245); - justify(左对齐/居中/右对齐/两端对齐):四个按钮均声明
isToggle: true(plugins/justify/plugin.js#L229-L247); - list(有序/无序列表)、bidi(文字方向)、copyformatting(格式刷)、maximize(全屏)等同样启用。
这些按钮的共同特征是:命令状态可随光标位置、选区内容或用户操作在“开/关”间切换,且切换状态需要被屏幕阅读器感知。isToggle 的存在让这些插件无需各自手写 aria-pressed 维护逻辑,统一由 button 插件的 setState 集中处理。
如何运行该手动测试
该测试属于 Bender 测试框架的手动测试(manual test)。CKEditor 4 的测试套件配置在 bender.js 中,其中:
manualBrowsers与manualTestTimeout分别控制可运行手动测试的浏览器列表与超时时长(bender.js#L15-L16);Plugins测试组将tests/plugins/**纳入扫描范围(bender.js#L106-L113),因此tests/plugins/button/manual/togglebutton.md会被自动识别为手动测试。
运行方式概括如下:
- 在仓库根目录执行
npm install安装依赖(benderjs及其插件已在 package.json 的 devDependencies 中声明,例如benderjs^0.4.6、benderjs-yui^0.3.2 等); - 执行
bender init初始化测试环境; - 运行
bender run manual --browsers Chrome(或项目约定的手动测试启动命令)启动测试服务器; - 在打开的页面中定位 button 插件的手动测试,选择
togglebutton用例; - 按文档步骤操作:等待编辑器加载 → 观察
aria-pressed初始值 → 单击按钮两次,逐步核对显示值与预期一致。
若需在发布包中携带测试,可参考 dev/builder/build.sh#L101-L114:构建流程会复制 tests/ 目录及 bender.js 配置文件到 release 目录,并执行 npm install && bender init 完成测试环境就绪。
小结:从手动测试到无障碍闭环
togglebutton.md 虽是一份篇幅精简的手动测试文档,却完整覆盖了 CKEditor 4 开关按钮无障碍特性的验收闭环:
- 标准语义:开关按钮通过
aria-pressed在无障碍树中表达“按下/未按下”,解决屏幕阅读器无法感知切换状态的问题(#2444、#4543,4.19.0 引入); - 单一开关:
isToggle: true是启用该行为的唯一入口,普通按钮不输出该属性,菜单按钮(hasArrow)除外(#11331); - 状态同步:渲染时初始化
aria-pressed="false",setState时按state === TRISTATE_ON映射为true/false,禁用态回落为false; - 双重验证:手动测试(togglebutton.md + togglebutton.html)验证真实点击链路,单元测试(aria.js + buttontools.js)锁定四种状态断言,两者互为印证。
如果你正在开发自定义 CKEditor 4 插件并需要开关型工具栏按钮,只需在 addButton 定义中加入 isToggle: true,并确保绑定命令的状态在 TRISTATE_ON/TRISTATE_OFF 之间正确切换,即可免费获得与官方插件一致的无障碍支持。