Gutenberg FormTokenField 深度解析:带自动补全的令牌字段组件、Props 全解与键盘可访问性实现
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
本篇围绕 Gutenberg 仓库packages/components中的FormTokenField组件展开,内容以组件自带的 README 为主体骨架,并结合 组件实现源码、类型定义 与 Storybook 故事文件 进行源码级扩充。读完本文,你将能够掌握该组件的完整 Props 语义、受控用法、键盘操作规范,以及其建议匹配、粘贴分词、输入校验等底层机制,从而在编辑器侧边栏、数据表单等场景中正确复用或定制这个"类标签字段"。
一、FormTokenField 是什么
FormTokenField是一种"令牌字段"(Token Field),交互模式类似于旧版编辑器界面中的标签/分类字段,也类似于 macOS 邮件客户端的"收件人"输入框:用户既可以逐键输入令牌,也可以从建议列表中挑选自动补全项。令牌之间以逗号 "," 分隔,输入过程中会展示最多 100 条与当前输入匹配的建议,用户可用上/下方向键选择建议,再用 Tab 或 Enter 键加入令牌。
从源码注释(index.tsx)可以看到上述行为即组件的官方定义。几点关键设定:
- 受控组件模式:
value属性的处理方式与 React 受控表单组件一致——组件不持有"最终令牌列表"的权威状态,父组件必须通过onChange拿到新数组并回写value,字段才能正确更新; - 组件状态:在 Storybook 故事 中标注为
status: 'recommended'、whereUsed: 'global',并注明"后续将被@wordpress/ui中的SearchableChipSelect取代,目前继续使用"。也就是说它仍是当前版本的推荐组件,但属于过渡期选型; - 文件结构:组件由 主实现 index.tsx、单令牌渲染 token.tsx、建议列表 suggestions-list.tsx、内部输入框 token-input.tsx 与 类型定义 types.ts 组成,配套 单元测试。
二、基本用法
最小可用示例(继承自 README 的 Usage 章节):
import { useState } from 'react'; import { FormTokenField } from '@wordpress/components'; const continents = [ 'Africa', 'America', 'Antarctica', 'Asia', 'Europe', 'Oceania', ]; const MyFormTokenField = () => { const [ selectedContinents, setSelectedContinents ] = useState( [] ); return ( <FormTokenField value={ selectedContinents } suggestions={ continents } onChange={ ( tokens ) => setSelectedContinents( tokens ) } /> ); };要点:value是受控状态,onChange回传"新令牌数组"(string[]或TokenItem对象数组),父组件负责存回 state。
异步建议(远程搜索)场景:仓库的 Async 故事 展示了如何利用onInputChange触发"模拟远程请求"——每次输入变化时重置一个 1 秒定时器,过滤出匹配项后更新suggestions:
const searchContinents = ( input: string ) => { const timeout = setTimeout( () => { const available = ( suggestions || [] ).filter( ( continent ) => continent.toLowerCase().includes( input.toLowerCase() ) ); setAvailableContinents( available ); }, 1000 ); return () => clearTimeout( timeout ); }; return ( <FormTokenField { ...args } value={ selectedContinents } suggestions={ availableContinents } onChange={ ( tokens ) => setSelectedContinents( tokens ) } onInputChange={ searchContinents } /> );onInputChange在用户于输入框中打字时触发(见源码 onInputChangeHandler 中每次都会调用onInputChange( tokenValue )),README 明确建议用它触发自动补全请求。
三、Props 全解(含源码中的默认值)
以下参数表在 README Properties 章节 基础上,补充了从 index.tsx 参数解构 中确认的默认值:
| Prop | 说明 | 默认值 |
|---|---|---|
label | 字段标签文本 | 'Add item.'(可国际化) |
value | 要显示的令牌数组,元素为字符串或必须含value属性的对象 | [] |
suggestions | 展示给用户的建议令牌字符串数组 | [] |
maxSuggestions | 一次最多展示的建议条数 | 100 |
displayTransform | 展示前对令牌做变换(编辑器中用于解码 HTML 实体,避免&被二次编码成&) | identity |
saveTransform | 保存前对令牌做变换,默认token.trim();该函数同时用于"当前值与建议的匹配",保证首尾空格不影响匹配 | ( token ) => token.trim() |
onChange | 令牌变化回调,入参为新令牌数组 | 空函数 |
onInputChange | 用户在输入框打字时触发,常用于发起自动补全请求 | 空函数 |
onFocus | 字段获得焦点时触发,事件对象传入回调,可用于埋点分析 | undefined |
isBorderless | 为true时令牌无背景渲染 | false |
maxLength | 传入后,当令牌数 ≥maxLength时禁止继续添加新令牌 | 无 |
disabled | 为true时令牌不可添加/删除 | false |
placeholder | 无令牌时输入框显示的占位文本 | 无 |
help | 控件的附加说明,通过aria-describedby与输入框建立程序化关联;默认显示操作提示文案,传空字符串可隐藏 | 'Separate with commas or the Enter key.'(开启tokenizeOnSpace时为Separate with commas, spaces, or the Enter key.) |
tokenizeOnSpace | 为true时,聚焦状态下按空格键即把当前输入固化为令牌 | false |
tokenizeOnBlur | 为true时,字段失焦会把未完成的输入(incompleteTokenValue)固化为新令牌 | false |
messages | 自定义屏幕阅读器播报的四类消息:added(新增令牌)、removed(删除令牌)、remove(聚焦到删除按钮)、__experimentalInvalid(输入未通过校验) | 见下文 |
__experimentalExpandOnFocus | 为true时,输入框一有焦点建议列表就保持展开 | false |
__experimentalAutoSelectFirstMatch | 为true时,用户按 Enter(或tokenizeOnSpace下的空格)会自动选中第一条匹配建议 | false |
__experimentalValidateInput | 传入时,所有引入值在成为令牌前先经其校验,返回false则拒绝 | () => true |
__experimentalRenderItem | 建议列表中每一项的自定义渲染函数,入参形如{ item },item直接取自options/建议数组中的单个数据 | 无 |
__experimentalShowHowTo | 已废弃:改用help属性;原来传__experimentalShowHowTo={ false }隐藏提示的,改传help=""即可 | — |
messages的默认值在 源码中定义:
messages = { added: __( 'Item added.' ), removed: __( 'Item removed.' ), remove: __( 'Remove item' ), __experimentalInvalid: __( 'Invalid item' ), };令牌对象TokenItem结构
当value数组中混入对象时,对象必须有value属性。README 中的示例与 types.ts 中的TokenItem接口 给出了完整字段:
{ value: '字符串,令牌的取值(必填)', status: "'error' | 'validating' | 'success',用于给令牌套样式", title: '字符串,非 falsey 时给令牌附加 title', isBorderless: '布尔,单个令牌级无边框渲染', onMouseEnter: '令牌上触发 onMouseEnter 时的回调', onMouseLeave: '令牌上触发 onMouseLeave 时的回调' }注意types.ts中比 README 多了isBorderless字段(单令牌级别无边框),renderToken 中会将其与组件级isBorderless做"或"运算。此外 index.tsx 中有一处细节:disabled仅在令牌status不为'error'时生效——即错误状态的令牌即使在禁用字段里也能被移除,这是刻意保留的"错误可撤销"交互。
已废弃的样式过渡属性
types.ts 还定义了若干仅用于版本迁移的废弃属性,阅读旧代码时可能遇到:
__next36pxDefaultSize:已废弃;__next40pxDefaultSize:自 WordPress 7.1 起 40px 高度已是默认行为,可安全删除;__nextHasNoMarginBottom:自 WP 7.0 起无外边距样式已是默认,可安全删除。
四、键盘操作与可访问性
4.1 键盘操作表(继承自 README)
left arrow— 输入框为空时,把插入点移动到上一个令牌之前right arrow— 输入框为空时,把插入点移动到下一个令牌之后up arrow— 选中上一条建议down arrow— 选中下一条建议tab/enter— 若有选中的建议,把建议插入为新令牌;否则把输入框内容插入为新令牌comma— 把输入框内容插入为新令牌
4.2 源码层面比 README 多出的按键行为
从 onKeyDown 分支 可以看到,实际实现还覆盖:
- Backspace:输入为空且聚焦在输入框时删除输入点前的令牌(handleDeleteKey);
- Delete:删除输入点后的令牌;
- Space:仅在
tokenizeOnSpace开启时固化为令牌,且校验失败时不拦截默认行为(便于继续输入); - Escape:折叠建议列表并保留当前输入(handleEscapeKey);
- Tab:折叠建议列表但不阻止默认行为,焦点正常移出(handleTabKey);
- 键盘事件经
withIgnoreIMEEvents包裹,避免中文/日文等输入法组合输入期间误触发。
方向键的选择逻辑也很讲究:handleDownArrowKey 用( index + 1 ) % 匹配数实现环形轮转(最后一条再按 Down 回到第一条),而 Up 键在索引 ≤ 0 时回到列表末尾,两端都可循环。
4.3 ARIA 与屏幕阅读器
组件的无障碍实现集中在两处:
- 输入框(token-input.tsx)采用标准 combobox 模式:
role="combobox"、aria-autocomplete="list"、aria-expanded反映建议列表展开状态、aria-owns指向建议列表容器,并在"聚焦 + 有选中项 + 列表已渲染"三个条件同时满足时设置aria-activedescendant,精确指向当前选中建议的li节点。 - 建议列表(suggestions-list.tsx)使用
role="listbox"+role="option"结构,aria-selected标记选中项。 - 播报:addNewToken 在添加成功时
speak( messages.added, 'assertive' ),被校验拒绝时播报__experimentalInvalid;deleteToken 播报removed;匹配结果数量由 updateSuggestions 通过 500ms 防抖的debouncedSpeak播报(如 "%d results found, use up and down arrow keys to navigate.")。 - 令牌位置提示:Token 组件 会在每个令牌文本中用
VisuallyHidden插入 "令牌名 (第 n 个 / 共 m 个)" 的隐藏文本,供屏幕阅读器读出排序与总数;help文案则经aria-describedby关联到输入框(renderInput 中生成对应 id)。
五、建议匹配机制(autocomplete 原理)
这是 README 一句"最多 100 条匹配建议"背后真正的算法,位于 getMatchingSuggestions:
- 先过
saveTransform:匹配前把输入值与value现有令牌都先经saveTransform处理,因此首尾空格不会导致匹配失效——这就是 README 强调该函数"同时用于建议匹配"的原因; - 空输入:输入(转换后)为空时,直接返回
suggestions中尚未存在于value的全部项(即已选令牌不会再出现在建议里); - 非空输入:把输入与建议都经
normalize('NFKC')+toLocaleLowerCase()归一化,然后分桶——"以输入开头"的建议排入startsWithMatch,"包含输入"的排入containsMatch,最终startsWithMatch整体优先于containsMatch; - 截断:取前
maxSuggestions(默认 100)条。
展示门槛:从 updateSuggestions 看,输入trim后长度需> 1 个字符且存在匹配项,列表才会展开(除非__experimentalExpandOnFocus为true且输入框持有焦点,此时列表常开)。若开启__experimentalAutoSelectFirstMatch,满足上述条件时会自动把选中索引设为 0 并滚动到可视区,实现"首条预选 + Enter 直接提交"的下拉选择器体验(见 DropdownSelector 故事)。
建议项渲染:默认渲染会把命中片段用<strong class="components-form-token-field__suggestion-match">高亮(见 computeSuggestionMatch);传入__experimentalRenderItem后则完全交由自定义函数,入参{ item }就是建议数组中的原始数据。列表为空时会渲染一条 "No items found" 占位项。
交互细节:suggestions-list.tsx 在onMouseDown上preventDefault(),防止点击建议项时输入框失焦——这与 index.tsx 容器级触摸处理 配合,保证点选建议不会顺带触发失焦固词逻辑。
六、令牌校验、粘贴分词与 maxLength
6.1 校验链路
引入一个令牌要依次通过三道关(见 addNewTokens):
saveTransform(token)后非空(filter(Boolean));- 现有
value中不存在同值令牌(valueContainsToken去重); __experimentalValidateInput(token)返回真值。
任意一道关被拒,行为不同:单键 Enter/逗号触发的 addNewToken 若校验失败会speak( messages.__experimentalInvalid, 'assertive' )并保留输入内容,让用户就地修正;成功添加后清空未完成输入、重置选中索引、收起列表(除非__experimentalExpandOnFocus),并保持输入框焦点。
6.2 粘贴与分隔符分词
onInputChangeHandler 处理"粘贴多段文本"的场景:按tokenizeOnSpace ? /[ ,\t]+/ : /[,\t]+/切分,前 N-1 段立即成为令牌,最后一段保留为未完成输入。值得注意的是其中的失败保留策略:若某些段未通过校验,组件会把被拒绝的段(用用户实际使用的那个分隔符重新拼接)回写到未完成输入区,而不是让整次粘贴卡死——例如tokenizeOnSpace下粘贴逗号分隔文本仍保持逗号分隔。
6.3 tokenizeOnBlur 与 maxLength
- 失焦固词:onBlur 中,若当前输入有效且通过校验,
tokenizeOnBlur为真时立即addNewToken( incompleteTokenValue );否则复位全部中间状态并收起建议列表。 - 上限拦截:renderInput 在
maxLength && value.length >= maxLength时直接不传onChange给TokenInput,从根上阻断继续输入。
七、Gutenberg 仓库中的真实应用
FormTokenField在 Gutenberg 前端代码中有多处生产级用法,可作为参考实现:
- 查询编辑器侧边栏:taxonomy-controls.jsx、author-control.jsx、format-controls.jsx、parent-control.jsx 用它做多选分类/作者等筛选;
- Terms Query 块: include-control.jsx;
- Patterns 组件: category-selector.jsx;
- DataViews 表单体系: validated-form-controls/form-token-field.tsx 将其包装进校验表单控件,并有配套测试 form-token-field.jsdom.test.tsx 的同级实现;
- 组件导出:它作为
@wordpress/components包的一部分对外导出,组件目录的 README 属性表 即官方文档源。
八、开发调试入口
- Storybook 故事: stories/index.story.tsx 提供
Default(静态建议)、Async(模拟 1 秒延迟搜索)、DropdownSelector(__experimentalExpandOnFocus+__experimentalAutoSelectFirstMatch组合)、WithCustomRenderedItems(displayTransform/__experimentalRenderItem自定义渲染)等场景,可直接在 Storybook 中逐项调整 Props 观察行为; - 单元测试: test/index.jsdom.test.tsx 覆盖键盘操作、建议匹配与受控更新等核心路径;
- 样式: style.module.scss 与 style.scss,令牌状态类
is-error/is-success/is-validating/is-borderless/is-disabled在 token.tsx 中按status与isBorderless装配。
九、使用建议小结
- 始终把
FormTokenField当作受控组件使用:保存value数组 →onChange回写;远程建议场景再叠加onInputChange+ 防抖请求; - 涉及 HTML 实体或首尾空格的领域(如标签名),记得成对提供
displayTransform/saveTransform,前者管"显示解码",后者管"保存归一 + 匹配归一"; - 键盘与读屏体验已由组件内建(combox/listbox 角色、activedescendant、防抖播报、令牌序号隐藏文本),自定义时优先用
messages覆盖文案,而非绕过; - 若目标是"聚焦即展开、回车直接选首条"的下拉选择器,用
__experimentalExpandOnFocus+__experimentalAutoSelectFirstMatch组合即可; - 选型时注意组件故事中标注的迁移方向:后续将被
@wordpress/ui的SearchableChipSelect取代,新大型项目可关注该组件的演进。
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考