CKEditor 4 工具栏开关按钮与 aria-pressed 无障碍状态:从手动测试到源码实现

原创2026-10-14 21:52:311,711 阅读
文章标签:前端UI组件

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>

页面设计要点如下:

  1. 状态指示器:编辑区上方放置 <span id="aria-pressed-value">,renderButtonState() 通过 CKEDITOR.document.findOne( '.cke_button__togglebutton' ) 定位工具栏按钮 DOM 节点,读取其 aria-pressed 属性值并写入该 span,实现文档所述“编辑区上方的值实时显示”。
  2. 自定义命令:toggleButton 命令在 exec 中依据当前状态在 CKEDITOR.TRISTATE_OFF 与 CKEDITOR.TRISTATE_ON 之间翻转,模拟真实开关按钮(如 Bold)的行为。
  3. 按钮定义:editor.ui.addButton( 'toggleButton', { ... isToggle: true, command: 'toggleButton' } ) —— isToggle: true 是本次特性的关键开关,它让按钮进入“可切换”模式;icon: 'Link' 表示直接复用 link 插件的图标(这也是元数据头声明加载 link 插件的原因)。
  4. 渲染时机: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)

这里有三个值得注意的实现细节:

  1. 布尔映射:aria-pressed 的值被设置为 state === CKEDITOR.TRISTATE_ON,即状态为 TRISTATE_ON 时写 true,否则写 false——与手动测试文档的预期完全对应。
  2. 只更新不删除:源码注释明确说明,即使按钮从按下回到弹起状态,也不移除属性,而是把值重置为 false。这与文档中“值等于 false”而非“未设置”的预期保持一致。
  3. 排除菜单按钮: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 会被自动识别为手动测试。

运行方式概括如下:

  1. 在仓库根目录执行 npm install 安装依赖(benderjs 及其插件已在 package.json 的 devDependencies 中声明,例如 benderjs ^0.4.6、benderjs-yui ^0.3.2 等);
  2. 执行 bender init 初始化测试环境;
  3. 运行 bender run manual --browsers Chrome(或项目约定的手动测试启动命令)启动测试服务器;
  4. 在打开的页面中定位 button 插件的手动测试,选择 togglebutton 用例;
  5. 按文档步骤操作:等待编辑器加载 → 观察 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 之间正确切换,即可免费获得与官方插件一致的无障碍支持。

登录后查看全文
ckeditor4