fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) - #3959
Merged
Conversation
表单里渲染 boolean/checkbox 字段时会发出**两个**同文本 label,而可见的那个指
向一个不存在的 id:`FormControl`(Radix `Slot`)把 `FormLabel htmlFor` 已经指
向的 id 交给控件,widget 却用字段名把它覆写掉 —— `label for="_r_3_-form-item"`
在 DOM 里无目标,switch 挂的是 `id="notifications"`。后果是点击可见 label 不会
翻转开关(switch/checkbox 行最常用的操作方式),而可访问名只是靠 widget 自己那
个 sr-only label 侥幸成立。
修法落在 widget 侧:`id` 本来就是 widget 契约(`FieldWidgetDomProps`)声明的键、
`toDomProps` 也一直在转发,丢弃已送达值的是消费者,所以按 AGENTS.md #0.1 在
消费者处改,不动 form 侧的 FormControl 机制。
- id 链改为 `hostId || config.name || useId()`。`config.name` 这一项不是冗余:
app-shell `ActionParamDialog` 的 boolean 分支渲染 `<Label htmlFor={param.name}>`
却**不**下发 id(与它自己的通用分支不同),全靠字段名派生的 id 成立 —— 已按
这条契约钉住,以后想「清理成裸 useId()」会直接翻红。
- host 下发了 id 就说明 host 自己渲染了 label,widget 不再重复发 sr-only label;
standalone(`FieldEditWidget` 的表格内联编辑、裸 SDUI 节点)没有 host label,
sr-only label 是控件唯一的可访问名,原样保留。
钉子(`boolean-label-association-e2e.test.tsx`,真表单 + 真 react-hook-form):
表单内每个 label 的 `for` 都必须解析到存在的元素、可见 label 点击后 switch 翻转、
label 文本在 DOM 中只出现一次、控件保留 host 的 id、两个 boolean 字段各归各的
label;另有 standalone 两个变体与「按字段名关联」的回归守卫。
反向验证(先预判方向再跑,变异不提交):修前 7/11 红。两个隔离变异确认各钉子
守的是哪一半 —— 只恢复 id 覆写:悬空 `for` 与 `getAllByLabelText` 翻红,后者报
「no form control was found associated to that label」,即 switch 完全失去可访问
名(正文预警的那个后果);只恢复无条件内部 label:label 计数与按文本定位的钉子
翻红,悬空检查保持绿。standalone 两个钉子在三种状态下都绿 —— 它们是回归守卫,
不是缺陷钉。
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This was referenced Aug 9, 2026
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3952 落地。 核验记录(head
out-of-scope 三条(#3961 / #3318 评论 / #3962)处置全部得当,由 PM 另行分诊。concern 中 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 9, 2026 17:45
This was referenced Aug 9, 2026
Closed
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 #3952
缺陷
表单里渲染
boolean/checkbox字段时会发出两个同文本 label,而可见的那个指向一个不存在的 id。FormControl是 RadixSlot,它把FormLabel htmlFor已经指向的 id(_r_N_-form-item)交给控件;BooleanField却用字段名覆写掉:上面是在真表单里实测到的(修前跑本 PR 的钉子,两字段用例直接打印出
{"forId":"_r_a_-form-item","resolves":false}),与 issue 正文的取证一致。用户可见的后果是点击可见 label 不会翻转开关 —— switch/checkbox 行最常用的操作方式失效,而且这是所有 app 里所有生成表单上的每个 boolean 字段。可访问名只是靠 widget 那个 sr-only label 侥幸成立。修法(落在 widget 侧,不动 form 侧)
id本来就是 widget 契约FieldWidgetDomProps声明的键,toDomProps一直在转发它 —— 丢弃已送达值的是消费者。所以按 AGENTS.md #0.1 在消费者处改,packages/components/src/renderers/form/form.tsx的 FormControl 机制只读不改。hostId || config.name || useId()。config.name这一项不是冗余:@object-ui/app-shell的ActionParamDialogboolean 分支渲染Label htmlFor={param.name}却不下发 id(与它自己的通用分支不同,那条是id={param.name}),全靠字段名派生的 id 成立(paramToField设name: param.name)。这条契约已单独钉住,以后想「清理成裸useId()」会直接翻红,而不是悄悄让控制台里每个 boolean action param 失联。FieldEditWidget的表格内联编辑、裸 SDUI 节点)没有 host label,sr-only label 是控件唯一的可访问名,原样保留。第 2 条刻意按 host id 判断,而不是按
config.label判断:表单内命名是 host 对每个 widget 的职责(没有 label 的表单行里,文本字段同样无名),而原来那个'Checkbox'兜底在表单内给出的是一个错误的可访问名,不是安全网。钉子
新增
packages/fields/src/__tests__/boolean-label-association-e2e.test.tsx(真表单 + 真 react-hook-form,范式同widget-aria-invalid-e2e.test.tsx),11 个用例分三组:for都解析到存在的元素;可见 label 命名的就是 switch;label 文本在 DOM 中只出现一次;点击可见 label 后 switch 翻转;控件保留 host 的 id(且不等于字段名);两个 boolean 字段各归各的 label。ActionParamDialog契约。反向验证(先预判方向,再跑;变异不提交)
修前 7/11 红,修后 11/11 绿。另做两个隔离变异,确认每个钉子守的是哪一半:
for第一个变异里
getAllByLabelText不是返回了错节点,而是抛found a label ... however no form control was found associated to that label—— 即 switch 完全没有可访问名,正是 issue 正文预警的「哪天 widget 内部 label 被去掉就会静默失名」。诚实记一笔:
getAllByLabelText长度为 1 这条不守重复 label 那一半 —— 两个 label 指向同一个控件时它仍然只解析出一个元素(实测),重复由 label 文本计数那条守。测试里原本写反了的注释已按实测改正。standalone 两个钉子在三种状态下都绿,它们是回归守卫,不是缺陷钉。同类排查(其它合成 id 的 widget)
按正文提示逐个 grep
useId/config?.name的 id 合成点,再用一次性 probe 在真表单里实测每个 widget 的 host label 落点(probe 未提交):forbutton[role=switch]subId('street')覆写subId('latitude')覆写只有
BooleanField属于「单一控件 + 覆写 host id + 自带重复 label」这一种形态,修法无歧义;address / geolocation 是复合控件(5 个和 2 个子输入各带自己的可见子 label),「组标签该指向谁」是设计裁决(指向第一个子输入 vs.role=group+aria-labelledby),不在本 issue 范围内,已另开 issue 附实测表与两个选项。验证
pnpm exec vitest run packages/fields packages/components/src/renderers/form --maxWorkers=2→ 100 files / 1176 tests 全绿。ActionParamDialog(×3)+paramToField+packages/plugin-detail→ 66 files / 559 tests 全绿(BooleanField 的另两个 host)。pnpm exec turbo run type-check --concurrency=2→ 78/78 successful。node scripts/check-control-bytes.mjs→ OK(3849 个文件);改动文件另做grep -naP自扫,零命中。@object-ui/fieldspatch(bug 修复,用户可见)。Generated by Claude Code