fix(fields): group-labelled widget 的只读/零选项分支也消费 host label 的 IDREF (#3990) - #4002
Merged
Merged
Conversation
…3990) 七个 `labelling: 'group'` widget 只在可编辑分支消费 host 递下来的 `aria-labelledby` + host id;字段级 `readonly: true` 与零可选项两种状态走的是 提前 return,渲染在那个容器之前,于是 label 发布了 id 却没有任何元素引用它。 实测(改前的 main,真 form renderer + 裸注册,逐字段数引用该 id 的元素): multiselect readonly+value consumers=0 byLabelText=0 named=0 multiselect readonly+empty consumers=0 byLabelText=0 named=0 multiselect zeroOptions consumers=0 byLabelText=0 named=0 multiselect editable consumers=1 byLabelText=1 named=1 七类在每个只读态上都是同一读数,改后都是 1。不是 #3961/#3975 的回归:那之前 同样这些状态发的是指向无人携带 id 的 `for`,一样什么都不命名 —— 形状从悬空 `for` 变成无人消费的 IDREF,严重程度未变。 每个只读面现在只带两个「整字段」键(host `id` 与 `aria-labelledby`)加上让它们 生效的 `role="group"`,统一走新的 `toHostGroupProps`。窄化是刻意的:只读面没有 可聚焦控件,`aria-describedby` / `aria-required` / `disabled` / 焦点回调在那里 无人宣读,而 `div` 上的 `name` 正是 #3291 扫的泄漏。 `radio`(可编辑 `radiogroup`)与 `file`(可编辑 `button`,即 dropzone)在只读态 答的是 `group`,因为那两个面里一个 radio、一个 dropzone 都不剩;共享的 「无可选项」框对 checkboxes / radio / multiselect 同样答 `group`,单选 `select` 不属于 group-labelled,继续什么都不发。 standalone(网格内联编辑器、裸 SDUI 节点)不递这两个键,输出保持逐字节不变, 包括 `EmptyValue` 占位符本身。
|
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。#3990 落地 —— group-labelled a11y 家族(#3961→#3975→#3990)三态全收口。 核验记录(head
concerns 采信:单成员 group 语义偏弱但是唯一不撒谎的可命名角色(已注释);「只递 id 的第三方 host」新增行为面方向正确。out-of-scope #4005(只读态 describedby 无消费者,Blocked-by 本单;首次噪声对照行已自行作废 —— 证据卫生好)由 PM 分诊(另评)。本单合入后 #3986(select multiple 路径)解锁可派。 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 #3990
结论
七个
labelling: 'group'widget 只在可编辑分支消费 host 递下来的aria-labelledby+ hostid。字段级readonly: true与零可选项两种状态走的是提前 return,渲染在那个容器之前 —— 于是 form renderer 让 label 发布了自己的
id、撤掉了for,而文档里没有任何元素引用这个 id。可见标签是 0 个元素的可访问名。不是 #3961 / #3975 的回归:改那两单之前,同样这些状态发的是指向「无人携带该 id」的
for,一样什么都不命名。形状从悬空for变成无人消费的 IDREF,严重程度未变 ——是那次修法没有覆盖到的状态(#3961 的钉子全部在可编辑态取样)。
逐 widget 实测(正文警告「写法各异不可推断」,所以七类逐个量)
一次性 probe(未提交,已删),真 form renderer + 裸注册
labelling: 'group',逐字段数「引用 host label 已发布 id 的元素个数 / byLabelText / 被命名的 role」:
即:七类在每个只读态上读数完全一致(全 0),并非只有正文实测过的
multiselect / checkboxes;可编辑态与整表单
mode: 'view'两列改前改后都是 1(view不走 widget 的 readonly 分支,与正文一致)。
处置(沿既定 group 机制)
新增
packages/fields/src/widgets/toHostGroupProps.ts:只读面统一只带两个「整字段」键 —— host
id与aria-labelledby—— 外加让它们生效的role="group",一处拼写。窄化是刻意的,不是偷懒:只读面没有可聚焦控件,
aria-describedby/aria-required/disabled/ 焦点回调在那里无人宣读;而name落到 div 上正是#3291 在扫的泄漏;把整份
toDomProps铺下去还会用 host 的className盖掉 widget自己的(
EmptyValue/ FileField / AddressField 的className都留在props里)。role必须随行:role-less 的 div / span 上aria-labelledby命名的是 nothing(
generic禁止 author name),这正是本单量到的形状。逐 widget 落点:
EmptyValue本身OptionsEmptyState框EmptyValue本身EmptyValue本身EmptyValue本身EmptyValue本身两处 role 与可编辑态故意不同,因为渲染的根本不是同一个面:
radio可编辑答radiogroup,只读面里一个 radio 都不剩;file可编辑答button(dropzone),只读面里没有 dropzone,只有文件名。共享的「无可选项」框对 checkboxes / radio /
multiselect 答
group;单选select不属于 group-labelled(label 仍是可用的for),该框对它一个属性都不多发 —— 这条有阳性对照钉。
顺带把 AddressField / GeolocationField 的可编辑容器也换成同一个
toHostGroupProps(它们此前手写
id+ 条件 role +aria-labelledby,语义与输出完全等价),这样同一文件里只读面与可编辑面不可能对同一个问题给出两种答案。
无缺口/豁免
没有豁免项:七类的每个只读渲染路径都找到了现成的承载元素,没有新增任何 DOM 节点。
一处如实记录的取舍:hosted 且空值时
EmptyValue就是整块只读面,于是它自己带上这三个属性,它原有的
aria-label(“No value”)按 accname 被aria-labelledby压过 ——这是想要的结果:在它此前的
genericrole 上 author name 本就被禁止、从未暴露,所以真实选择是「字段名」还是「没有名字」。standalone 时该占位符不带 role,行为不变。
钉子(64 个,新文件
composite-group-label-readonly-e2e.test.tsx)getAllByRole('group', { name })前 0 / 后 1,且byLabelText同元素;rating 无空态);
id以-form-item结尾)、label 无for、字段内无悬空或 inert 的for、只读面不带name/aria-invalid;OptionsEmptyState框本身(testId 断言);以及 readonly 与零选项同时成立时 readonly 分支胜出且仍被命名;
group)与只读角色 group,任何「统一角色」的后续改动都必须来这里说明它改了什么;
SelectField零选项框无 role / 无 IDREF / 无 id(共享组件新增 prop 的阳性对照)。
反向验证(先预判再跑,变异未提交)
只读钉 + 「七类同表」+ 漂移钉 = 6 条,不动 EmptyValue / 零选项 / standalone。
实测:
Tests 6 failed | 58 passed,失败项与预判逐条一致。OptionsEmptyState的hostGroupProps。预判只红「checkboxes: the unfillable-options box is the named group」1 条。
实测:
Tests 1 failed | 63 passed。两次都是经典方向(撤掉即翻红),没有出现计数型或反转型的情况 —— 因为这些钉断言的是
「被命名的元素个数从 0 到 1」这个谓词,而不是某个下游计数。
验证
pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' build→ 通过(先行,新 worktree 的依赖产物)。
pnpm exec vitest run packages/fields --maxWorkers=2→Test Files 73 passed (73),Tests 1160 passed (1160)。pnpm exec vitest run packages/components/src/renderers/form --maxWorkers=2→
Test Files 33 passed (33),Tests 182 passed (182)(group-labelling 是 form renderer与 widget 的联合机制,声明位那侧一并跑过)。
pnpm exec turbo run type-check --concurrency=2→Tasks: 78 successful, 78 total。node scripts/check-control-bytes.mjs→ OK(3902 个跟踪文本文件);另对本 PR 触碰的文件做了越界自扫(
grep -naP控制字符类)→ 无命中。_前缀丢弃变量与any)。changeset:
@object-ui/fieldspatch。边界:只动
packages/fields/src/widgets/**+ 测试 + changeset;未碰 form.tsx / index.tsx的声明集合(机制已定型),未碰
content/docs/releases/。Generated by Claude Code