在 Gutenberg 中使用 ResponsiveBlockControl 构建按视口区分的区块控件
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
本文围绕 WordPress Gutenberg(块编辑器)中@wordpress/block-editor包提供的__experimentalResponsiveBlockControl组件展开,讲解如何为标准化的"按视口/屏幕尺寸分别设置"的区块控件提供统一接口。你将掌握该组件的全部 Props 用法、默认渲染行为、自定义响应式渲染方案、可访问性实现原理,并结合仓库源码与测试用例理解其底层工作机制,最终能够在自己的区块edit实现中落地一套可复用的响应式设置界面。
组件定位:为什么需要按视口区分的区块设置
在开发自定义区块时,一个常见需求是:某个布局属性(例如区块的内边距padding、外边距margin)在"大屏"上表现良好,但在"小屏"上可能过大或过小。此时就需要为每种屏幕尺寸分别提供一套设置值,即"响应式设置"。
ResponsiveBlockControl正是 Gutenberg 为这类场景提供的标准化组件:它把"默认(All,所有视口统一)"与"按视口分别设置(Small / Medium / Large)"两种模式的切换界面统一封装起来,开发者只需专注于渲染具体的值控件(如SelectControl、NumberControl),而把切换开关、分组渲染、可访问性标签等公共逻辑交给组件处理。
它的核心特点可以概括为三点:
- 渲染完全可控:通过
renderDefaultControl与可选的renderResponsiveControls两个渲染函数,开发者可以完全掌控控件输出; - 视口完全可定制:通过
viewports属性可以自定义任意数量的视口配置,不局限于 Small / Medium / Large 三档; - 纯"受控组件":
ResponsiveBlockControl本身不负责持久化任何控件值,你传入的控件需要自行处理值的保存(例如写入区块 attributes)。
从源码看,该组件定义在 packages/block-editor/src/components/responsive-block-control/index.jsx,并通过 packages/block-editor/src/components/index.js 以__experimentalResponsiveBlockControl的名称对外导出(组件名含__experimental前缀,表明其 API 仍在实验阶段,可能在后续版本调整)。
基本用法:在区块 edit 中接入
在区块的edit实现中,将<ResponsiveBlockControl />放入InspectorControls(设置侧栏)内即可。你需要提供:
renderDefaultControl函数——渲染具体的界面控件;isResponsive布尔状态——标记当前是否处于"响应式模式";onIsResponsiveChange回调——切换模式时更新状态。
默认情况下,renderDefaultControl返回的控件既用于渲染"All"默认状态,也(在未提供renderResponsiveControls时)用于逐个渲染每个视口的响应式控件。
以下是最小可运行的示例(来自组件 README.md):
import { useState } from 'react'; import { registerBlockType } from '@wordpress/blocks'; import { InspectorControls, __experimentalResponsiveBlockControl as ResponsiveBlockControl, } from '@wordpress/block-editor'; registerBlockType( 'my-plugin/my-block', { // ... edit( { attributes, setAttributes } ) { const [ isResponsive, setIsResponsive ] = useState( false ); const { paddingSize } = attributes; // 你的自定义控件可以是任何你想要的界面元素。 const paddingControl = ( labelComponent, viewport ) => { return ( <input type="number" label={ viewport.label } onChange={ /* 在此处理 padding 值更新 */ } value={ paddingSize } /> ); }; return ( <> <InspectorControls> <ResponsiveBlockControl title='Block Padding' property='padding' renderDefaultControl={ paddingControl } isResponsive={ isResponsive } onIsResponsiveChange={ () => { setIsResponsive( ! isResponsive ); } } /> </InspectorControls> <div> { /* 你的区块渲染 */ } </div> </> ); } } );需要注意:上面示例仅为演示renderDefaultControl的签名。在实践中,若要让每个视口分别保存各自的值,更常见的做法是把viewport.id并入属性键(例如paddingSize与paddingSizeSmall/paddingSizeMedium/paddingSizeLarge分开存储),并在onChange中根据viewport.id写入对应的 attributes 字段。
Props 全解:从必填到可选的每一项
title
- 类型:
String - 默认值:
undefined - 必填:
true
整个控件组的标题,被用作<fieldset>中<legend>元素的文本,用来标记整组控件。从测试用例可见,渲染后会被辅助技术识别为role="group"(名称为 title 文本)——详见 test/index.jsdom.test.jsx。
property
- 类型:
String - 默认值:
undefined - 必填:
true
用于构建控件组的可访问标签与 ARIA 角色,应表示该组件控制的布局属性(如padding、margin等)。在源码中,property被传入ResponsiveBlockControlLabel,最终拼装成"Controls the padding property for All viewports."这类完整的可访问描述文本。
isResponsive
- 类型:
Boolean - 默认值:
false - 必填:
false
决定组件显示默认控件还是响应式控件,同时驱动切换开关(ToggleControl)的勾选状态。源码中切换开关的checked值为! isResponsive(即勾选代表"所有屏幕使用相同值"),onChange直接绑定onIsResponsiveChange。当isResponsive为false时渲染默认控件;为true时渲染响应式控件组。
onIsResponsiveChange
- 类型:
Function - 默认值:
undefined - 必填:
true
切换开关值变化时被调用的回调函数,用于把isResponsive状态更新为最新值,使组件成为标准的"受控"组件。
renderDefaultControl
- 类型:
Function - 默认值:
undefined - 必填:
true - 参数:
- labelComponent:(
Function)一个已渲染好的ResponsiveBlockControlLabel组件,可直接作为你控件的<label>; - viewport:(
Object)表示当前视口属性的对象(含id与label)。
- labelComponent:(
用于渲染需要按视口展示的控件。例如一个控制 padding 大小的SelectControl,把它作为renderDefaultControl传入后,组件会在"All"状态用它渲染默认控件;若未提供renderResponsiveControls,还会在响应式模式下用它自动渲染每个视口的控件。
组件会为你的控件传入一个预先创建好的、可访问的<label>,你还可以利用viewport参数中的上下文信息(如当前渲染的是All还是某个响应式视口)来调整控件输出。下面是一个结合SelectControl的示例:
const renderDefaultControl = ( labelComponent, viewport ) => { const { id, label } = viewport; // eg: // { // id: 'small', // label: 'All' // } return <SelectControl label={ labelComponent } />; };从 index.jsx 源码可以看出,renderDefaultControl无论处于哪种模式都会被调用一次(用于渲染默认控件),响应式模式下还会为每个视口各调用一次(见defaultResponsiveControls中的循环,index.jsx)。测试用例也验证了这一调用次数规律:自定义视口集合有 N 个时,renderDefaultControl共被调用N + 1次(默认一次 + 每个视口一次)。
renderResponsiveControls
- 类型:
Function - 默认值:
undefined - 必填:
false - 参数:
- viewports:(
Array)viewport 对象数组,每个对象含id与label属性。
- viewports:(
可选的响应式控件渲染函数。如果不提供,组件会使用renderDefaultControl返回的控件自动为每个视口生成响应式控件;如果提供,则在响应式模式下完全用你返回的组件替换输出,实现 100% 的控制权。
let uniqueId = 0; const renderResponsiveControls = ( viewports ) => { const inputId = ++uniqueId; return viewports.map( ( { id, label } ) => { return ( <Fragment key={ `${ inputId }-${ id }` }> <label htmlFor={ `${ inputId }-${ id }` }> Custom Viewport { label } </label> <input id={ `${ inputId }-${ id }` } defaultValue={ label } type="range" /> </Fragment> ); } ); };注意源码中的渲染分支(index.jsx):
{ isResponsive && ( renderResponsiveControls ? renderResponsiveControls( viewports ) : defaultResponsiveControls() ) }即:renderResponsiveControls存在时优先使用它,否则回退到用renderDefaultControl自动生成。同时,即便提供了renderResponsiveControls,renderDefaultControl仍会被调用一次(用于默认模式),这一点在测试中也有明确断言。
toggleLabel
- 类型:
String - 默认值:
Use the same %s on all screen sizes.(其中%s会被替换为property属性的值) - 必填:
false
切换开关的显示文本。不传时,源码会通过sprintf基于property动态生成,例如property="padding"时显示 "Use the same padding on all screen sizes."(见 index.jsx)。
defaultLabel
- 类型:
Object - 默认值:
{ id: 'all', label: 'All', }- 必填:
false
描述默认值的对象。默认All表示该控件作用于"所有视口/屏幕尺寸"。需要自定义"默认档"的名称与 id 时(例如测试中的{ id: 'everything', label: 'Everything' }),可传入此属性覆盖。
viewports
- 类型:
Array - 默认值:
[ { id: 'small', label: 'Small screens', }, { id: 'medium', label: 'Medium screens', }, { id: 'large', label: 'Large screens', }, ];- 必填:
false
viewport 对象数组,每个对象描述一种视口尺寸的配置(id与label)。它决定了响应式模式下渲染的控件数量与各自配置。你可以自由定制视口集合——例如测试用例中使用了tiny / small / medium / huge四个自定义视口,验证了renderDefaultControl恰好被调用4 + 1 = 5次(test/index.jsdom.test.jsx)。
可访问性设计:legend、label 与屏幕阅读器文本
该组件在可访问性上做了完整设计,这是它区别于"自己手写切换逻辑"的关键价值:
- 最外层使用
<fieldset>+<legend>语义化分组(class 为block-editor-responsive-block-control),title作为 legend 文本,辅助技术可将其识别为具名分组; - 切换开关使用
ToggleControl,其help文本为"Choose whether to use the same value for all screen sizes or a unique value for each screen size."(源码 index.jsx); - 每个控件都会收到一个由 label.jsx 渲染的
ResponsiveBlockControlLabel:可见文本为视口标签(如 "All"),同时通过VisuallyHidden附带完整的描述文本——形如 "Controls the padding property for All viewports.",并用aria-describedby建立关联(id 通过useInstanceId生成,保证唯一性)。
从测试断言中可以看到这一可访问性文本的完整形态:screen.getByRole( 'combobox', { name: 'All Controls the padding property for All viewports.' } )——即"视口标签 + 隐藏描述"拼接后的名称。这意味着你的控件只要把labelComponent用作 label 或传给组件的 label 属性,即可免费获得无障碍命名。
样式方面,style.scss 定义了分组底部分隔线、标题间距等视觉细节,并将切换开关的 help 文本通过screen-reader-text混入隐藏(仅对屏幕阅读器可见),保证界面整洁而不牺牲可访问性。
状态管理与持久化:受控组件的正确姿势
ResponsiveBlockControl是严格受控的:
- 模式状态(
isResponsive)由你维护,通过onIsResponsiveChange反向更新; - 各控件的值由你的控件自行维护与持久化(通常写入区块 attributes),组件本身不持有任何值状态。
因此,一个完整的落地实现通常形如:
edit( { attributes, setAttributes } ) { const [ isResponsive, setIsResponsive ] = useState( false ); const { padding } = attributes; const renderPaddingControl = ( labelComponent, viewport ) => { const propKey = viewport.id === 'all' ? 'padding' : `padding_${ viewport.id }`; return ( <RangeControl label={ labelComponent } value={ attributes[ propKey ] } onChange={ ( value ) => setAttributes( { [ propKey ]: value } ) } /> ); }; return ( <InspectorControls> <ResponsiveBlockControl title="区块内边距" property="padding" renderDefaultControl={ renderPaddingControl } isResponsive={ isResponsive } onIsResponsiveChange={ setIsResponsive } /> </InspectorControls> ); }其中viewport.id === 'all'对应默认档(即defaultLabel.id),其余 id 对应各响应式视口;渲染区块时再根据当前媒体查询选择对应值(响应式 CSS 变量或 JS 媒体查询皆可)。这样既复用了组件的界面与切换逻辑,又保持了数据层完全透明可控。
测试验证:行为与边界条件的保证
组件仓库附带的单元测试 test/index.jsdom.test.jsx 覆盖了以下关键行为,可以作为你理解和使用该组件的"行为契约":
- 必填 Props 守卫:缺少
title、property或renderDefaultControl任一必填项时,组件返回null(渲染为空),与源码if ( ! title || ! property || ! renderDefaultControl ) return null;(index.jsx)一致; - 默认模式渲染:默认渲染 "All" 控件、勾选状态的切换开关,且响应式控件组不存在;
- 响应式模式渲染:
isResponsive为true时渲染响应式控件组、隐藏默认控件; - 自定义视口:传入 4 个自定义视口时,
renderDefaultControl恰好被调用 5 次,且每个视口都得到正确命名的可访问 label; - 模式切换交互:通过
userEvent模拟点击切换开关,能在默认/响应式两种模式间往返切换(对应 index.jsx 中的渲染分支); - 自定义响应式渲染:提供
renderResponsiveControls时,响应式模式渲染 3 个自定义控件,且renderDefaultControl仍被调用 1 次(仅为默认模式准备)。
这些测试同时验证了组件与SelectControl、RangeControl等@wordpress/components控件的组合可行性,你可以放心将其与任意自己的控件搭配使用。
适用场景与注意事项
适合使用 ResponsiveBlockControl 的场景:
- 区块需要为 padding、margin、字号、间距等布局属性提供按屏幕尺寸区分的设置;
- 希望复用 Gutenberg 标准的"同一值用于所有屏幕 vs 各屏幕单独设置"交互范式,保持 UI 一致性;
- 需要开箱即用的 fieldset/legend 分组与屏幕阅读器标签,降低无障碍实现成本。
注意事项:
- 组件 API 带
__experimental前缀,属于实验性接口,升级 Gutenberg 时需关注变更; - 组件不持久化任何值,所有状态(模式 + 控件值)都由外部维护,务必在
renderDefaultControl中处理好每个视口的读写; - 默认只提供 small/medium/large 三档视口,若产品需要 tablet 等更多断点,请通过
viewports自定义; - "响应式"在这里指的是"按视口分别取值"的设置界面,实际生效仍需在渲染侧结合媒体查询/CSS 变量输出对应的值。
相关资源:组件实现 index.jsx、标签组件 label.jsx、样式 style.scss、单元测试 test/index.jsdom.test.jsx,以及官方文档 README.md。
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考