WordPress Gutenberg Composite 组件完全指南:基于 WAI-ARIA 的单一 Tab 停靠点与方向键导航
2026/9/17 4:03:55 网站建设 项目流程

WordPress Gutenberg Composite 组件完全指南:基于 WAI-ARIA 的单一 Tab 停靠点与方向键导航

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

Composite是 Gutenberg 项目(WordPress 块编辑器)中@wordpress/components包提供的核心交互组件,它基于 WAI-ARIA Composite Role 抽象,在页面上提供单一 Tab 停靠点,并允许用户通过方向键在可聚焦的子元素之间导航。本文将从快速上手、完整 API 参考、焦点管理机制到源码级实现原理,系统讲解如何在 Gutenberg 生态中用它构建无障碍、键盘友好的工具栏、菜单、网格等复合组件。

设计动机:为什么需要 Composite

在原生 HTML 中,如果一个容器内有多个可聚焦元素(如一组按钮),Tab 键会逐个访问它们,这要求用户按很多次 Tab 才能遍历完一组控件。对于工具栏、菜单、单选组、网格这类「复合组件」,WAI-ARIA 推荐只保留一个 Tab 停靠点,进入组件后改用方向键在内部成员之间移动焦点。这正是Composite要解决的抽象问题。

从 源码实现 的注释可以看到:

Composite 是一个可能包含可导航项(由 Composite.Item 表示)的组件。它受 WAI-ARIA Composite Role 启发,实现了所有键盘导航机制,确保整个 Composite 元素只有一个 Tab 停靠点。这意味着它可以表现为 roving tabindex 或 aria-activedescendant 容器。

也就是说,无论底层采用哪种焦点管理策略,使用方拿到的 API 都是一致的——这正是该组件「抽象」的价值所在。

快速上手

@wordpress/components导入Composite即可使用:

import { Composite } from '@wordpress/components'; function MyMenu() { return ( <Composite> <Composite.Group> <Composite.GroupLabel>Label</Composite.GroupLabel> <Composite.Item>Item 1</Composite.Item> <Composite.Item>Item 2</Composite.Item> </Composite.Group> </Composite> ); }

最简用法甚至不需要Group

import { Composite } from '@wordpress/components'; <Composite> <Composite.Item>Item 1</Composite.Item> <Composite.Item>Item 2</Composite.Item> <Composite.Item>Item 3</Composite.Item> </Composite>

使用后,Tab 键只会进入组件一次,之后的横向/纵向移动全部交给方向键。

组件 API 全解

Composite通过Object.assign挂载了一组静态子组件(index.tsx),完整清单如下:

子组件作用
Composite.Group渲染一组复合项的容器
Composite.GroupLabel组标签,必须包裹在Composite.Group
Composite.Item复合组件中的可导航项
Composite.Row复合行,包裹 Item 后形成二维复合组件(如网格)
Composite.Hover鼠标悬停时获得焦点、移出时交还基元素的元素
Composite.Typeahead为复合组件增加输入定位(typeahead)能力
Composite.Context复合组件使用的 React Context,可访问 store

下面逐一详解。

Composite根组件

渲染一个复合组件(composite widget),支持的 props 如下。

活动项控制:activeId/defaultActiveId/setActiveId
  • activeId:string | null:当前活动项的id。活动项指复合组件内拥有 DOM 焦点或虚拟焦点(启用了virtualFocus)的元素。
    • null表示基础复合元素(带 composite role 的那个元素),用户可以从它出发用方向键导航进入各项;
    • 若初始设为null,基础复合元素自身获得焦点,用户可以用方向键从中导航。
  • defaultActiveId:string | null:组件渲染时的默认活动项 id。
    • null:基础复合元素获得焦点;
    • undefined:第一个启用的项被聚焦。
  • setActiveId:(activeId: string | null | undefined) => voidactiveId状态变化时的回调,用于受控场景。
焦点循环:focusLoop

类型boolean | 'horizontal' | 'vertical' | 'both',默认false。决定用户到达复合组件末尾时的焦点行为。

一维复合组件(单行或单列)

  • true:从最后一项循环回第一项,反之亦然;
  • horizontal:仅当orientationhorizontal或未设置时循环;
  • vertical:仅当orientationvertical或未设置时循环;
  • activeId初始为null,基础复合元素会夹在最后一项与第一项之间被聚焦。

二维复合组件(使用了Composite.Row

  • true:从某行/列的末项循环回同一行/列的首项;若处于最后一行/列的末项,则跳到第一行/列的首项,反之亦然;
  • horizontal:仅在行内循环(行末项回到该行首项);
  • vertical:仅在列内循环(列末项回到该列首项);
  • activeId初始为null,垂直循环不生效——从最后一行向下或从第一行向上移动都会聚焦到基础复合元素;
  • focusWrapfocusLoop取值一致,则在最后一行/列的末项与第一行/列的首项之间循环。
换行换列:focusWrap

类型boolean | 'horizontal' | 'vertical',默认false仅对二维复合组件生效

  • true:在行与列之间换行;
  • horizontal:仅在行之间换行;
  • vertical:仅在列之间换行;
  • focusLoopfocusWrap一致,则在末行/列末项与首行/列首项之间包裹。

focusLoop的区别:focusLoop处理「同方向到达边界」的循环,focusWrap处理「从一行切到另一行/从一列切到另一列」时的包裹。

位移回退:focusShift

类型boolean,默认false仅对二维复合组件生效。启用后,向上/下移动时若下一个位置没有项或该项被禁用,焦点会回退到当前位置之前的项。典型场景是行内项数不一致的不规则网格。

虚拟焦点:virtualFocus

类型boolean,默认false。启用后,复合元素作为aria-activedescendant容器,DOM 焦点始终停留在复合元素上,各项只获得「虚拟焦点」;默认情况下则采用 roving tabindex(焦点随导航在各项间真实移动)。两种模式下,获得焦点的项都会携带data-active-item属性(测试代码即通过该属性断言活动项,见 test/index.browser.test.tsx)。

方向:orientation

类型'horizontal' | 'vertical' | 'both',默认both。对一维复合组件决定哪些方向键可用:

  • both:所有方向键都可用;
  • horizontal:仅左右方向键;
  • vertical:仅上下方向键。

对二维复合组件无影响(二维下方向键语义由行列结构决定)。

从右到左:rtl

类型boolean,默认值为isRTL()(由 index.tsx 从@wordpress/i18n自动获取当前语言环境方向)。设为true时,store 的next/previous行为反转(左右互换)。注意:它只影响复合组件的行为,HTML/CSS 层面仍需自行设置dir="rtl"

渲染控制:render

类型RenderProp<...> | ReactElement。允许把组件渲染为不同的 HTML 元素或 React 组件,值可以是 React 元素,也可以是接收原始 props 并返回合并后元素的函数。这是 Ariakit 系组件的通用扩展点,常用于把Composite.Item渲染成<button><a>或自定义组件。

焦点可见性:focusable/onFocusVisible
  • focusable:boolean:使组件可聚焦。获得键盘焦点时携带data-focus-visible属性并触发onFocusVisible。非原生可聚焦元素会完全失去可聚焦性,而原生可聚焦元素保留其固有可聚焦性。
  • onFocusVisible:(event: SyntheticEvent<HTMLElement>) => void:元素通过键盘交互获得焦点或聚焦时按键触发的自定义事件处理函数,是data-focus-visible属性的编程等价物。注意:onFocusVisible生效的前提是focusabletrue(若其默认值不是 true)。
禁用状态:disabled/accessibleWhenDisabled
  • disabled:boolean,默认false:设置aria-disabled属性,从而支持所有元素(包括不原生支持disabled的元素)。可与accessibleWhenDisabled组合。
  • accessibleWhenDisabled:boolean:指示元素即使在disabled时仍可聚焦。这对「可发现性」很重要——文档中给出了经典例子:

编辑器中的工具栏包含一组特殊的智能粘贴功能,当剪贴板为空或功能不适用于当前剪贴板内容时它们被禁用。如果这些禁用按钮的功能可发现性主要依赖其在工具栏上的存在,那么让禁用按钮保持可聚焦是有帮助的。

相关规范见 Focusability of disabled controls。

children

类型React.ReactNode,组件内容。

Composite.GroupComposite.GroupLabel

Composite.Group渲染复合项的分组容器;Composite.GroupLabel渲染组标签。后者必须包裹在Composite.Group内部——从 group-label.tsx 的源码可以看到,CompositeGroupLabel会通过useCompositeGroupContext()检查自己是否处于CompositeGroupContext.Provider内,否则直接throw new Error('Composite.GroupLabel can only be rendered inside Composite.Group.')。正确渲染后,aria-labelledby会被正确设置在组元素上,屏幕阅读器用户即可感知分组语义。

两个组件均支持renderchildrenprops。

Composite.Item

渲染一个复合项,是导航的最小单位。支持childrenrender以及accessibleWhenDisabled(语义同根组件)。从 item.tsx 源码可见,CompositeItem内部会从 Context 读取 store,若在Composite之外使用(拿不到 store),会通过@wordpress/warning输出开发警告:'Composite.Item: Missing composite state. Render inside Composite to enable composite keyboard behavior.'——这是排查「方向键不生效」类问题的重要线索。

Composite.Row:构建二维复合组件

Composite.Item包进Composite.Row,即可创建二维复合组件(如网格):

<Composite> <Composite.Row> <Composite.Item>Item 1.1</Composite.Item> <Composite.Item>Item 1.2</Composite.Item> <Composite.Item>Item 1.3</Composite.Item> </Composite.Row> <Composite.Row> <Composite.Item>Item 2.1</Composite.Item> <Composite.Item>Item 2.2</Composite.Item> <Composite.Item>Item 2.3</Composite.Item> </Composite.Row> </Composite>

二维模式下,focusLoopfocusWrapfocusShift的全部语义(见上文)才会生效。

Composite.Hover

渲染一个随鼠标悬停获得焦点、鼠标移出时把焦点交还给复合基础元素的组件,通常与Composite.Item组合使用,实现菜单悬停展开类体验:

<Composite> <Composite.Hover render={ <Composite.Item /> }>Item 1</Composite.Hover> <Composite.Hover render={ <Composite.Item /> }>Item 2</Composite.Hover> </Composite>

Composite.Typeahead

为复合组件增加输入定位能力:按下可打印字符键,焦点移动到下一个以输入字符开头的复合项(类似文件管理器/编辑器的首字母跳转):

<Composite render={ <Composite.Typeahead /> }> <Composite.Item>Item 1</Composite.Item> <Composite.Item>Item 2</Composite.Item> </Composite>

注意示例中的用法是把Composite.Typeahead通过render渲染为Composite的底层元素。

Composite.Context

复合组件间共享的 React Context(context.tsx),可用来访问复合 store,并在复合子组件通过 portal(如SlotFill)渲染、Context 无法自动穿透到Fill子组件时手动转发:

import { Composite } from '@wordpress/components'; import { useContext } from '@wordpress/element'; const compositeContext = useContext( Composite.Context );

焦点管理机制:roving tabindex 与 aria-activedescendant

Composite支持两种被 WAI-ARIA APG 认可的标准键盘交互实现,由virtualFocus切换:

  1. Roving tabindex(默认):容器内始终只有一个元素的tabindex0,其余为-1,DOM 焦点随导航真实移动;
  2. aria-activedescendant(虚拟焦点):DOM 焦点固定在容器上,通过aria-activedescendant指向当前活动项 id,适合虚拟列表、巨型网格等需要频繁移动焦点却不想重排 DOM 的场景。

无论哪种模式,当前活动项都会带有data-active-item="true"属性,前端样式与测试均可据此定位活动项。浏览器测试 验证了「单一 Tab 停靠点」的核心行为:Tab 聚焦 Before → 进入组件聚焦 Item 1 → Tab 直接跳到 After,Shift+Tab 反向亦然。

源码实现剖析:Ariakit 之上的 WordPress 封装

Composite并非从零实现,而是对开源组件库 Ariakit 的二次封装(index.tsx)。其实现要点:

  • store 驱动:根组件调用Ariakit.useCompositeStore({ activeId, defaultActiveId, setActiveId, focusLoop, focusWrap, focusShift, virtualFocus, orientation, rtl })创建 store(index.tsx),所有焦点/导航状态都收敛于此;
  • Context 分发:store 通过useMemo缓存进contextValue,再由CompositeContext.Provider提供给所有子组件(index.tsx);Group额外通过CompositeGroupContext标记分组层级(group.tsx);
  • 默认值策略focusLoop = falsefocusWrap = falsefocusShift = falsevirtualFocus = falseorientation = 'both'rtl = isRTL()disabled = false(index.tsx),其中rtl自动跟随@wordpress/i18nisRTL(),无需手动配置即可适配阿拉伯语、希伯来语等环境;
  • 子组件转发Item/Row/Hover/Typeahead/GroupLabel均为对 Ariakit 对应组件的轻量转发,统一从 Context 取 store(见 item.tsx、row.tsx、hover.tsx、typeahead.tsx);
  • store prop 与 legacy 兼容storeprop 属于未文档化的内部通道,仅供 legacy 兼容层使用,类型被刻意模糊以阻止外部直接使用(源码多处@ts-expect-error注释说明了这一点)。

从类型定义(types.ts)可以看出,所有 props 的类型均直接派生自Ariakit.CompositeStoreProps与对应 Ariakit 组件的 props,保证了与上游能力的同步与类型安全。

在 Gutenberg 中的实际应用

Composite已广泛用于 Gutenberg 内部各组件,例如:

  • alignment-matrix-control:对齐方式选择矩阵,利用Composite.Row构建二维网格导航;
  • circular-option-picker:颜色/图案循环选择器,利用方向键快速切换选项。

构建类似工具栏、菜单、网格选择器等复合控件时,推荐组合方式:

  • 一维工具栏:Composite+Composite.Itemorientation="horizontal",可选focusLoop
  • 分组菜单:外层Composite,内部多个Composite.Group+Composite.GroupLabel
  • 二维网格:Composite+ 多个Composite.Row,按需启用focusWrapfocusLoopfocusShift
  • 长列表/虚拟列表:开启virtualFocus,配合data-active-item做高亮样式。

无障碍最佳实践与注意事项

  1. 不要过度禁用:需要「可发现」的禁用项请使用accessibleWhenDisabled,而不是简单disabled,以避免键盘用户与屏幕阅读器用户「看不见」该功能;
  2. RTL 双重设置rtlprop 只管导航行为,页面级dir="rtl"与相关 CSS 仍需自行处理;
  3. 子组件必须处于CompositeComposite.Item脱离上下文只会得到开发警告,Composite.GroupLabel脱离Group会直接抛错;
  4. 利用data-active-item做样式:活动项的高亮、aria-selected等视觉与语义反馈可统一挂在该属性选择器下,与内部实现解耦;
  5. 二维组件的循环语义focusLoopfocusWrap取值相同时会产生「跨首末行包裹」行为,设计导航时要区分「同方向循环」与「换行包裹」两种意图。

至此,从 API 用法、焦点机制到实现原理,Composite的完整能力已清晰呈现。如需深入交互细节,可继续阅读 composite 目录 下的 stories 与浏览器测试用例,它们覆盖了禁用项跳过、虚拟焦点、二维导航、typeahead 等全部行为。

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询