WordPress Gutenberg 中 MenuGroup 组件解析:@wordpress/components 的菜单分组、可访问性与分隔线实现
2026/9/17 21:03:31 网站建设 项目流程

WordPress Gutenberg 中 MenuGroup 组件解析:@wordpress/components 的菜单分组、可访问性与分隔线实现

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

MenuGroup是 Gutenberg 仓库中@wordpress/components包提供的一个基础 UI 组件,用于把若干相关的MenuItem组织进一个带标题、带分隔线的公共区块,是DropdownMenu下拉菜单体系的核心子组件。本文以仓库内packages/components/src/menu-group/目录的官方文档为主体,结合实现源码、SCSS 样式、Storybook 示例与 jsdom 测试用例,完整讲解MenuGroup的设计准则、Props 定义、无障碍(a11y)实现与在 Gutenberg 块编辑器中的真实应用场景,帮助你在开发自定义块编辑器 UI 时正确使用并深入理解该组件。

MenuGroup 的定位与使用场景

MenuGroup将一组相关的MenuItem组件包裹成一个公共区块(common section)。按照 组件 README 中的设计准则:

  • 当一个菜单中存在两个或以上相互关联的菜单项时,应当使用MenuGroup把它们归为一组,向用户传达"这些操作属于同一类别";
  • MenuGroup上方或下方还存在其他菜单项时,组件会自动在它与相邻项之间呈现一条分隔线(divider line),起到视觉区隔作用;
  • MenuGroup可以选择性地携带一个label,用简短文字描述该组内容的含义(例如 "Settings"、"View"、"Editor")。

这一组件的定位在 Storybook 元数据中也有明确标注:状态为recommended,备注为 "DropdownMenu的子组件(Subcomponent)",见 stories/index.story.tsx 中的componentStatus配置。也就是说,MenuGroup不建议脱离菜单语境单独使用,它的典型宿主是DropdownMenu内部的NavigableMenu菜单容器。

README 还给出了与它配合的三个"相邻组件"及选择依据:

组件用途
MenuItem菜单中的单个按钮式选项
MenuItemsChoice在一组互斥选项间切换(如单选),应放在MenuGroup内部
DropdownMenu承载MenuItem/MenuItemsChoice/MenuGroup的可展开菜单容器

基本用法:README 示例与组件 Props

官方用法示例

README 中给出的最小用法如下,直接继承自官方文档,可复制使用:

import { MenuGroup, MenuItem } from '@wordpress/components'; const MyMenuGroup = () => ( <MenuGroup label="Settings"> <MenuItem>Setting 1</MenuItem> <MenuItem>Setting 2</MenuItem> </MenuGroup> );

组件从@wordpress/components包统一导出,导出入口见 packages/components/src/index.ts。

Props 完整说明

MenuGroup的 Props 类型定义在 types.ts 中,共 4 个可选属性:

Prop类型说明
childrenReactNode分组内部渲染的菜单项子元素
classNamestring附加到根容器元素的 CSS class
labelstring显示在分组顶部的标题文本(可选)
hideSeparatorboolean隐藏容器顶部分隔线(border-top)

注意两个容易忽略的细节:

  1. label不是必填的——无 label 的纯分组也是合法用法;
  2. hideSeparator只影响"顶部分隔线",且只有在存在兄弟MenuGroup时才有意义(分隔线由相邻选择器绘制,见下文样式分析)。

源码实现:从 children 计数到 ARIA group

完整实现见 index.tsx,共 50 余行,核心逻辑分四步。

1. 无子元素时整体返回 null

const { children, className = '', label, hideSeparator } = props; const instanceId = useInstanceId( MenuGroup ); if ( ! Children.count( children ) ) { return null; }

组件在没有任何 children 时直接渲染null,即 DOM 中不会留下空分组。这一行为由 jsdom 测试显式验证(test/index.jsdom.test.tsx):

test( 'should render null when no children provided', () => { render( <MenuGroup /> ); expect( screen.queryByRole( 'group' ) ).not.toBeInTheDocument(); } );

值得留意的是,仅return null还不够稳妥:当子元素是"空" ReactNode(例如条件渲染cond && <MenuItem/>求值为false)时,Children.count可能仍统计到占位节点。为此 SCSS 中补了一道 CSS 兜底(见下一节),双保险确保空分组绝不显示。

2. 通过 useInstanceId 生成唯一 label 锚点

const labelId = `components-menu-group-label-${ instanceId }`;

useInstanceId来自@wordpress/compose,为每个组件实例生成递增的唯一 ID,从而保证同一菜单中多个MenuGroup的 label 锚点互不冲突。

3. 渲染结构:label 锚点 + role="group" 容器

组件最终输出的 JSX 结构(index.tsx#L34-L49):

return ( <div className={ classNames }> { label && ( <div className="components-menu-group__label" id={ labelId } aria-hidden="true" > { label } </div> ) } <div role="group" aria-labelledby={ label ? labelId : undefined }> { children } </div> </div> );

这里有两处无障碍细节值得注意:

  • 可见的 label 文本节点标记aria-hidden="true",把它从无障碍树中排除,避免文本被读屏器重复朗读;
  • 真正的语义承载者是内层role="group"的 div,通过aria-labelledby指向 label 的 id,让读屏软件把"分组名称"与"分组内容"正确关联;
  • 当未提供label时,aria-labelledbyundefined,即渲染一个无名分组,不会引用到不存在的 id。

这正是测试用例should render with an accessible label所验证的行为(test/index.jsdom.test.tsx#L23-L31):以screen.queryByRole('group', { name: 'My group' })断言分组可以通过 ARIA 名称被查询到,证明aria-labelledby链路完整生效。

4. className 合成

const classNames = clsx( className, 'components-menu-group', { 'has-hidden-separator': hideSeparator, } );

hideSeparator属性并不会移除任何元素,而是给根节点追加has-hidden-separatorclass,由 CSS 决定隐藏顶部分隔线——这是典型的"React 管结构、SCSS 管外观"的职责分离。

样式实现:分隔线由 CSS 相邻选择器绘制

样式定义在 style.scss,仅 27 行,却精确实现了 README 中"与相邻项之间应有分隔线"的设计准则:

.components-menu-group + .components-menu-group { padding-top: $grid-unit-10; border-top: $border-width solid $gray-900; &.has-hidden-separator { border-top: none; margin-top: 0; padding-top: 0; } } .components-menu-group:has(> div:empty) { display: none; } .components-menu-group__label { padding: 0 $grid-unit-10; margin-top: $grid-unit-05; margin-bottom: $grid-unit-15; color: $gray-700; text-transform: uppercase; font-size: 11px; font-weight: var(--wpds-typography-font-weight-emphasis); white-space: nowrap; }

三条规则对应三个设计决策:

  1. .components-menu-group + .components-menu-group:只有当某个分组"紧跟在另一个分组之后"时才绘制border-top分隔线。从源码结构看,菜单中最常见的分隔线场景(分组与分组之间)由此自动覆盖;第一个分组没有顶部分隔线,视觉上与菜单顶部自然衔接。
  2. .has-hidden-separator:当开发者显式传入hideSeparator时,移除分隔线以及为分隔线预留的上下间距(margin-top/padding-top一并归零),避免留下空白断层。
  3. .components-menu-group:has(> div:empty):利用 CSS:has()选择器配合空元素检测,作为 React 层return null的兜底——即便某个分组因条件渲染导致内层 div 为空,也会整体display: none。这与 React 层的 children 计数逻辑构成双重防护。

label 的视觉样式遵循 Gutenberg 设计系统规范:灰色($gray-700)、大写、11px、加粗(引用 WordPress Design System 的--wpds-typography-font-weight-emphasis变量)、不换行,作为"分组小标题"而非正文出现。

Storybook 示例:MenuGroup 在真实菜单中的组合方式

stories/index.story.tsx 提供了两个可交互 Story,展示了MenuGroup的标准组合姿势:包裹在NavigableMenu(可方向键导航的菜单容器)中,与MenuItemMenuItemsChoice协作。

WithSeparatorStory(stories/index.story.tsx#L41-L82)完整还原了块编辑器中"More"菜单的形态:

return ( <NavigableMenu> <MenuGroup label="View"> <MenuItem>Top Toolbar</MenuItem> <MenuItem>Spotlight Mode</MenuItem> <MenuItem>Distraction Free</MenuItem> </MenuGroup> <MenuGroup { ...args }> <MenuItemsChoice choices={ choices } value={ mode } onSelect={ ( newMode: string ) => setMode( newMode ) } onHover={ () => {} } /> </MenuGroup> </NavigableMenu> );

该 Story 的注释与 README 设计准则完全一致:"When other menu items exist above or below a MenuGroup, the group should have a divider line between it and the adjacent item."(当 MenuGroup 上下存在其他菜单项时,分组与相邻项之间应有分隔线。)args中设置了hideSeparator: falselabel: 'Editor',可直接在 Storybook 中切换hideSeparator观察分隔线的有无。

与 DropdownMenu 的集成关系

MenuGroupDropdownMenu内容区的三种合法子元素之一(MenuItemMenuItemsChoiceMenuGroup)。DropdownMenu 的 README 给出了children函数渲染模式的标准写法,MenuGroup在其中承担"把平铺选项切成多个视觉区块"的职责:

import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; import { more, arrowUp, arrowDown, trash } from '@wordpress/icons'; const MyDropdownMenu = () => ( <DropdownMenu icon={ more } label="Select a direction"> { ( { onClose } ) => ( <> <MenuGroup> <MenuItem icon={ arrowUp } onClick={ onClose }> Move Up </MenuItem> <MenuItem icon={ arrowDown } onClick={ onClose }> Move Down </MenuItem> </MenuGroup> <MenuGroup> <MenuItem icon={ trash } onClick={ onClose }> Remove </MenuItem> </MenuGroup> </> ) } </DropdownMenu> );

从 dropdown-menu/index.tsx 的renderContent实现看,children函数的返回值被直接渲染进role="menu"NavigableMenu容器内,MenuGrouprole="group"子层嵌套其中,形成menu > group > menuitem的完整 ARIA 层级。此外DropdownMenu还支持controls数组(DropdownOption[] | DropdownOption[][])声明式写法:当传入嵌套数组时,内部会为每组的首项追加has-separatorclass(index.tsx#L170-L179)模拟分组分隔效果——可以推断,controls的二维分组正是对MenuGroup语义的"数据化"等价实现,而children函数模式则允许更灵活的分组(例如带 label 的分组、混用MenuItemsChoice)。

DropdownMenu的其余 Props(iconlabelcontrolspopoverPropstogglePropsmenuPropsdisableOpenOnArrowDownopen/defaultOpen/onToggle等)在 DropdownMenu README 中有完整说明,实际开发MenuGroup所在菜单时通常需要一并了解。

在 Gutenberg 块编辑器中的真实应用

MenuGrouppackages/block-editor中被广泛使用,检索可知它出现在块设置菜单、块转换菜单、插入器筛选、颜色/双色调节器、全局样式控制面板等十余处组件中,典型用例包括:

  • 块设置下拉菜单:把"移动/复制/删除"等编辑操作与"样式/设置"等功能入口分成不同MenuGroup
  • 块转换菜单:将"转换为其他块类型"的选项按类别分组;
  • 块样式菜单、双色调节器、全局样式状态控制 等:均以MenuGroup组织多组选项。

阅读这些文件可以看到统一的使用模式:在DropdownMenu的 children 渲染函数内,按业务语义拆出若干MenuGroup(部分带label如 "Move"、"Transform"),组内放MenuItem,互斥选择场景放MenuItemsChoice,组与组之间的分隔线则由样式自动处理。

测试与验证

单元测试位于 test/index.jsdom.test.tsx,基于 Vitest + Testing Library 覆盖三条核心断言,可运行仓库根目录的标准测试流程验证:

测试断言验证的源码行为
should render null when no children providedqueryByRole('group')不存在Children.count(children)为空时return null
should render childrenrole="group"与子文本均可见正常渲染出分组容器与子项
should render with an accessible labelqueryByRole('group', { name: 'My group' })可见aria-labelledby使分组获得可访问名称

小结

MenuGroup虽然实现只有五十余行,却完整体现了 Gutenberg 组件库的工程取向:

  • 语义先行role="group"+aria-labelledby的 ARIA 分组结构,使辅助技术能识别菜单中的逻辑区块;
  • 样式即约定:README 中"相邻项之间必须有分隔线"的设计准则,由 CSS 相邻兄弟选择器自动实现,开发者无需手工管理分隔线元素,hideSeparator提供逃生口;
  • 空态双保险:React 层Children.count判空 + CSS:has(> div:empty)兜底,确保空分组不会在 UI 中留下残影;
  • 组件生态协同:作为DropdownMenu的推荐子组件,与MenuItemMenuItemsChoiceNavigableMenu共同构成 Gutenberg 编辑器"More"菜单的完整能力。

在开发自定义块编辑器插件时,只要需要在下拉菜单中呈现"多个类别的操作",直接按本文的 Props 与组合模式使用MenuGroup即可获得与核心编辑器一致的分组体验。相关源码与文档入口:MenuGroup README、实现源码、Props 类型、样式、DropdownMenu 文档。

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

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

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

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

立即咨询