fix(metadata-admin): 检查器三个共享字段原子的 label 与控件建立编程关联 (#3994) - #3996
Merged
Conversation
`InspectorTextField` / `InspectorNumberField` / `InspectorSelectField` 都把 `Label`
渲染成控件的兄弟节点,却既没有 `htmlFor`、也没有 `id`,更没有 `aria-label` 兜底 ——
标签与控件之间只有视觉邻接。修前实测:三者的 `getByLabelText('Group')`(与屏幕阅读器
同一条 for→id 链)全部 0 命中,焦点落进去读到的是匿名「编辑框 / combobox」,可见的
字段名成了一段无归属文本,点标签什么都不会发生。这三个原子被 16 个非测试模块消费
(页面块 / 流程节点 / 报表 / 数据集 / 权限 / 对象字段各检查器,以及 studio-design 的
对象分组检查器),打开任意检查器面板即渲染。
三者各自 `React.useId()` 生成 id 并闭合 `htmlFor` ⇄ `id`。id 在原子内部生成而非由调用方
传入是有意的:这些原子渲染在数组项循环里(`record:details.sections[i]`、
`page:tabs.items[i]`),每项重复同一个 label —— 那正是调用方传 id 会撞车的地方,而撞车
是静默的(两个 label 都能解析,只是都指向第一个控件)。`useId()` 结构上不可能撞车;
多实例互不串 id 是钉住的,不是假设的。
select 的 id 落在 `SelectTrigger` 而非 `Select`:Radix `Select.Root` 自己不渲染任何
DOM 元素,交给它的 id 会被静默丢弃、label 的 for 随之悬空 —— 就是 #3976(PR #3992)
在隔壁修掉的同一机制。trigger 渲染真正的 `button[role=combobox]`,是可 label 元素,
一对 for/id 即完成命名,不需要第二条 `aria-labelledby` 通道;`disabled` 仍留在 Root
(对 trigger / items / 隐藏 native mirror 的单一权威),禁用态下依然有可访问名。
`InspectorCheckboxField` 本来就是对的(包裹式 label,合法关联、无需 id),一字未动,
在测试里作为阳性对照 —— 两个反向变异下它必须保持绿。
钉子 `_shared.labels.test.tsx`(26 条):三原子共用 `describe.each` 跑同一组保证 ——
label 的 for 有宿主、`getAllByLabelText` 命中且就是那个可聚焦控件(select 命中 trigger、
tagName BUTTON)、可访问名等于 label、只有 for 一条命名通道(先断言 forId 为真,
否则修前 null === null 会假绿)、id 全文档唯一;同 label 两实例 id 不同且各自归属、
不同 label 兄弟各自解析;以标签定位到的控件是真控件(输入提交只来自该实例、number
提交数字、select 的 trigger 就是显示选中项 label 的那个元素)。
`PageBlockInspector.sectionName.test.tsx` 的变通处随之收紧:原注释把「必须按 placeholder
定位,因为 `InspectorTextField` 的 Label 与 input 无关联,`getByLabelText` 到不了」写下来
当作理由,那正是本缺陷的机械症状。定位改为 `getAllByLabelText('Name (i18n key)')`
(与 AT 同一条链),`snake_case` 约定不再搭便车藏在定位器里,单独一条断言;
「一个分区一个 name 框」顺带成为消费侧的 id 不撞车检查。
Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3994 落地。 核验记录(head
concern 中「点 label 会展开下拉」为 button 可 label 元素的标准行为、与 PR3992 已接受形状一致 —— 同意非回归,维护者若要「聚焦不展开」属另一决定。out-of-scope #3997(第四原子 InspectorComboField + 18 处待核清单)由 PM 分诊(另评)。 Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3994
缺陷
packages/app-shell/src/views/metadata-admin/inspectors/_shared.tsx的三个通用字段原子 ——InspectorTextField/InspectorNumberField/InspectorSelectField—— 都把Label渲染成控件的兄弟节点,却既没有htmlFor、也没有id,更没有aria-label兜底。标签与控件之间只有视觉邻接,没有可编程关联:焦点落进去时辅助技术读到的是匿名「编辑框 / combobox」,可见的字段名是上方一段无归属文本,点标签什么都不会发生。修前实测三者的getByLabelText('Group')(与屏幕阅读器同一条 for→id 链)全部 0 命中。不是 dormant:这三个原子被 16 个非测试模块消费(页面块 / 流程节点 / 报表 / 数据集 / 权限 / 对象字段各检查器,以及 studio-design 的对象分组检查器),打开任意检查器面板即渲染。
修法
三者各自
React.useId()生成 id 并闭合htmlFor⇄id。id 在原子内部生成、不由调用方传入(issue 把这个小决定留给实施者论证):这些原子渲染在数组项循环里(
record:details.sections[i]、page:tabs.items[i]),每项重复同一个 label —— 那正是调用方传 id 会撞车的地方,而撞车是静默的:两个 label 都能解析,只是都指向第一个控件,任何「存在一个名为 X 的控件」的断言仍会通过。useId()结构上不可能撞车,是「让 AI 写的代码难以写错」的那一侧。将来的aria-describedby(#3912 的校验消息)属于同一个原子,可以从这个私有 id 派生;只有当关联必须从外部建立时才需要暴露 id,而现在没有任何调用点需要。select 的 id 落点:
SelectTrigger,不是Select。RadixSelect.Root自己不渲染任何 DOM 元素,交给它的 id 会被静默丢弃、label 的 for 随之悬空 —— 就是 #3976(PR #3992)在隔壁packages/components内建 select 分支修掉的同一机制。trigger 渲染真正的button[role=combobox],是可 label 元素,一对 for/id 即完成命名,不需要第二条aria-labelledby通道(测试把「只有 for、没有第二条通道」钉住了)。disabled仍留在 Root(对 trigger / items / 隐藏 native mirror 的单一权威),禁用态下依然有可访问名。InspectorCheckboxField本来就是对的(包裹式 label,合法关联、无需 id),一字未动 —— issue 把它列为反例参照,这里把它变成测试里的阳性对照。消费面 16 个文件无需改动:id 是组件内部生成的,调用点不感知。
钉子
新增
_shared.labels.test.tsx(26 条)。三原子共用一组describe.each跑同一组保证:getAllByLabelText命中且就是那个可聚焦控件(select 命中 trigger、tagName BUTTON,不是包裹 div)toHaveAccessibleName,断在计算结果而不是标记上)null、toBe会 null === null 假绿getAllByLabelText返回 2 个且与getAllByRole逐个对齐(数组项场面);不同 label 兄弟各自解析PageBlockInspector.sectionName.test.tsx的变通处随之收紧。原注释把「必须按 placeholder 定位,因为InspectorTextField的 Label 与 input 无关联、getByLabelText到不了」写下来当作理由 —— 那正是本缺陷的机械症状。定位改为getAllByLabelText('Name (i18n key)')(与 AT 同一条链);snake_case约定不再搭便车藏在定位器里,单独一条断言;「一个分区一个 name 框」顺带成为消费侧的 id 不撞车检查(两个分区重复同一 label,撞车会让它返回 1 个)。反向验证(先写预判再跑,两个变异均未提交)
预判方向都是朴素翻红,但形状不同,分别跑:
变异 1 —— 只从
InspectorTextField撤掉htmlFor/id。 预判:_shared.labels.test.tsx中该原子的 5 条命名 + 2 条多实例 + 1 条「输入提交」共 8 条红,其余两原子与 checkbox 全绿;sectionName9 条里凡调用nameBoxes()的 7 条红,不按标签定位的 2 条(「渲染本身不写入」「作者不输入就保持无名」)绿。共 15 红。实测逐条一致:
变异 2 —— 把 select 的 id 从
SelectTrigger移回Select(Root)。 预判:只有 select 的 9 条红(5 命名 + 2 多实例 + 2 decoy),text/number/checkbox 17 条绿,sectionName整个文件绿(它从不按标签定位 select)。这一条专门证 Root 静默丢弃机制,而不只是「少了个属性」。实测:
注意变异 2 下「emits no label pointing at an id nothing carries」是靠
resolves为假翻红(label 的 for 还在,只是没有宿主),正是缺陷的原貌。验证
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build绿。vitest run packages/app-shell/src/views/metadata-admin→ 136 文件 / 1399 通过 | 1 skipped。vitest run packages/app-shell/src/views/studio-design→ 17 文件 / 114 通过。turbo run type-check --concurrency=2→ 78/78 successful。node scripts/check-control-bytes.mjs→ OK(3904 tracked text files)。边界
_shared.tsx+ 两个测试文件 + changeset;PageBlockInspector.tsx(PageBlockInspector 自身的 chrome 字面量未过 t() —— 列表「Add」按钮、Remove aria-label、Invalid JSON、两个 placeholder 在 zh-CN 下仍是英文 #3963/block-config.ts 的 8 个 placeholder 仍是英文显示文本 —— #3913 只搬了 label/addLabel/option,zh-CN 面板里的输入框提示还是 "lucide icon name" #3979 刚定型)一字未动,只改了它的测试定位器。BlockPropField没有格式/校验能力位 —— 所有标识符类字段(sectionname、tabkey、accordionvalue)只能靠 placeholder 陈述约定 #3912 的能力位面(pattern/validate),不碰 [components] 内建 select 的空选项/依赖门态分支把 host id 落在一个 div 上 —— label 的 for 指向不可 label 元素,仍是不关联的 #3991 的 components 内建 select 空态分支。@object-ui/app-shellpatch。顺手发现(未在本 PR 修)
同目录的
InspectorComboField.tsx是同族缺陷的第四个站点(label 兄弟节点、trigger 无 id),已另开 issue #3997 交 PM triage,本 PR 不夹带。