You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[fields] 复合/分组 field widget 丢弃或错投 host 下发的控件 id:address / geolocation 的表单 label for 悬空,checkboxes / radio / rating / file 的 for 落在不可 label 的 div 上 #3961
B(checkboxes / radio / rating / file):host id 确实被保留了,但落在一个 div 上。label for 指向不可 label 的元素在 HTML 里是无效的:不产生点击激活,也不贡献可访问名。所以 for 虽然「解析成功」,关联却是空的 —— 比 A 轻,但同样是「视觉上有标签、语义上没有」。
倾向选项 1:两条轴都指向它 —— 长期上它是复合控件的标准语义、一次覆盖 A 和 B、不留下「两个 label 指一个输入」的隐性债;而在「让 AI 写的代码难以出错」这条轴上,把分组语义做成 widget 契约里的显式声明(声明即强制),比让 form 侧根据 widget 渲染出的 DOM 去猜要安全得多,未声明的新 widget 会被钉子按 for 解析当场翻红,而不是悄悄多出一个无效 for。
复现
packages/fields/src/__tests__/boolean-label-association-e2e.test.tsx(PR #3959 新增)里的 labelTargets() 辅助函数就是这套取证:在真表单里渲染任一字段,把每个 label 的 for 与 DOM 里的 id 逐个比对。把它对准 address / geolocation 就能复现 A,对准 checkboxes / radio 会看到 for 解析到一个 div。
Activity
yinlianghui commentedon Aug 9, 2026
PM 分诊(session_01GTRjn8xBqp75dk7kFupVRt):晋级
pm:queue。方向按正文推荐案定:widget 契约声明 group 语义,组容器role="group"+aria-labelledby关联 host label —— 这是 WAI-ARIA 标准的 group 模式,属正确性收敛而非产品口味,无需维护者另裁;实施时逐 widget 沿用正文实测表(address/geolocation 悬空、checkboxes/radio/rating/file 惰性关联),每类两方向钉子。前置 #3952(PR #3959)已验收入队,合入后可派。Generated by Claude Code
yinlianghui commentedon Aug 9, 2026
🔒 认领(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt,批次 4 补位)
claude/issue-3961-composite-group-labelrole="group"+aria-labelledby关联 host label(WAI-ARIA 标准 group 模式);逐 widget 沿正文实测表处置(address/geolocation 悬空、checkboxes/radio/rating/file 惰性关联),每类两方向钉子。fordangles #3952(PR fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959)已 MERGED,host id 语义已定型。packages/fields/src/widgets/{AddressField,GeolocationField,CheckboxesField,RadioField,RatingField,FileField}.tsx(实名以 worktree 为准)+ 测试;renderers/form只读;与在飞各单不相交。Generated by Claude Code
yinlianghui commentedon Aug 9, 2026
🔎 实施前验证结论:正文实测表全部复现成立,但裁定的 group 模式无法在 widget 侧自足完成 —— 需要一次范围裁决回到 PM/维护者。dev agent(session_01GTRjn8xBqp75dk7kFupVRt),基
53b7b88af(含 ea41a59),未提 PR。1. 复现(真 form 渲染器 + 每字段一行,probe 未提交)
逐个 label 的
for与它解析到的元素,以及该字段行里「host label 文本是否成为任何元素的可访问名」:forqueryAllByLabelText(hostLabel)getByRole(group/radiogroup,{name})_r_0_-form-item_r_2_-form-item_r_4_-form-itemdiv_r_6_-form-itemdiv[role=radiogroup]_r_a_-form-itemdiv_r_b_-form-itemdiv[role=button]子标签一侧全部正常(address 五个子 label 各自解析到自己的
input,checkboxes/radio 的 per-option label 解析到button[role=checkbox|radio])。两类形态的最终后果是同一个:六个 widget 里,host 的可见组标签今天都不是任何元素的可访问名(byLabelText 与 accname 两条路都是 0)—— 这比正文对 B 类「比 A 轻」的描述更严格:B 类的for虽然解析成功,但HTMLLabelElement.control对不可 label 元素返回 null,所以 accname 与 label 关联查询都拿不到它。2. 硬边界:
aria-labelledby没有可指向的 IDREF,且只能由 form 侧产出上表还测到一列关键事实:六个 host label 的
ownId全是(none)。packages/components/src/ui/form.tsx:97-113——FormLabel只渲染htmlFor={formItemId},自身不带id。该文件是 AGENTS.md 构建一个 **Live Playground (实时演练场)** (用于展示引擎能力) #7 的 Shadcn no-touch 区。packages/components/src/ui/form.tsx:114-135——FormControl(Radix Slot)下发给 widget 的只有id/aria-describedby/aria-invalid,没有任何 label 身份通道;formItemId派生得到的另两个 id 是 description / message,与 label 无关。label[for]指向div[role=group]→ accname 0;同一段 markup 给 label 加id并用aria-labelledby指过去 → 1;aria-labelledby指向不存在的 id → 0 且静默。所以生产者不存在时先落消费侧,等于 字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222 /aria-required从未到达任何输入控件——必填状态只以「可访问名的一部分」存在 #3290 反复钉的「declared but not delivered」。3. 退路「widget 侧用
aria-label={field.label}自造组名」是错的,不是省事可见 label 与 widget 拿到的
field.label是两个不同来源,有三条独立分叉:packages/plugin-form/src/sectionFields.ts:100可见 label 走ctx.fieldLabel(objectName, fieldName, field.label || fieldName)—— i18n 解析(packages/i18n/src/useObjectLabel.ts:258,原始 label 仅作 fallback);同函数 :107 传给 widget 的field保留的是原始未翻译的 label。中文包下 sighted 用户看中文、屏幕阅读器读英文,与 objectstack#5407 同一失效类。if (fd.label != null) base.label = fd.label—— 视图级 label 覆写不回写进 metadata。label时 form 根本不渲染 FormLabel,而 metadata 仍可能有 label → widget 会造出一个用户从未看见的名字。而且这正好是仓里被反复否掉的「一个名字两个作者」:#3290 的 required marker、#3222 的 message text、#3952 的重复 label。
packages/components/src/renderers/form/form.tsx:326-352(stripRegisteredFieldProps)已经故意不把label交给注册 widget,注释原话是「Whether the contract SHOULD publish alabelprop is a separate decision againstFieldWidgetPropsSchema」—— 本单要的东西正压在那个未裁决的决定上。发布契约侧同样没有通道:@objectstack/spec的FieldWidgetPropsSchema(dist/ui/index.d.ts:3408)只给field.{name,label,type},无顶层label;AriaPropsSchema(:599)只有ariaLabel/ariaDescribedBy/role,没有ariaLabelledBy。4. 为什么不落「widget 侧那一半」
把 host id 从子输入挪回组容器 + 补
role="group",在名字来源缺位的前提下,恰好把 A 类(悬空for,可检出)变成 B 类(for落在 div 上,惰性且更难检出)—— 正文选项 2 自己否掉的那笔交易(「把一个可检出的缺陷换成一个不可检出的缺陷」)。所以本单不提 PR。5. 待裁决(两案共用同一命名通道,只差一个声明位)
共用内核:
renderers/form/form.tsx给自己的 label 一个id(FormLabel把 props spread 到Label之后,传id不需要动 no-touch 的ui/form.tsx;同理传htmlFor={undefined}就能取消for),并把aria-labelledby={labelId}下发给 widget —— 零新 key:aria-*已在FieldWidgetComponentProps & AriaAttributes里,toDomProps已按前缀转发。group widget 把它放到组容器上 +role="group"即可。htmlFor。代价:复合 widget 上永久留一个惰性for(「每个label[for]都必须解析到可 label 控件」这条不变量永远无法当门禁钉住),且每个单控件 widget 多出第二条命名通道 —— 一个名字两个机制,以后会漂;新的复合 widget 忘了声明不会红。ComponentMeta(packages/core/src/registry/Registry.ts:26)加一个labelling?: 'control' | 'group',由registerField(packages/fields/src/index.tsx:2230)按一个与FIELD_TYPES_SKIP_FALLBACK并列的集合声明;form 侧用已存在的ComponentRegistry.getMeta(type, 'field')(Registry.ts:419)读它并分支:group 字段发id+aria-labelledby不发htmlFor,单控件路径完全不变。两条轴都指向 B:长期上它就是正文说的「一个声明位,而不是让 form 侧去猜 widget 的 DOM 形状」,不留惰性
for、不给单控件字段第二条命名通道;在「让 AI 写的代码难以出错」上,新的复合 widget 不声明就走单控件路径,被 #3952 这套「label 必须解析到真控件」的钉子当场翻红,而案 A 下它只会静默拿到一个落在无 role 包裹 div 上的aria-labelledby。第三方 widget 想声明 group,还需要 spec 侧在WidgetManifestSchema/AriaPropsSchema补一个位 —— 属本单裁定后的后续,不另开单。请 PM 定:(a) 把本单范围扩到
packages/components/src/renderers/form/form.tsx(按 B 一次做完),还是 (b) 拆成 components 侧前置单 + 本单作为 fields 侧后续。6. fields 侧六个 widget 的处置方案(任一裁定下均已就绪)
div.space-y-3id+role="group"+ 转发来的aria-labelledby;street 保留subId('street')(#3343 不回退);aria-describedby等其余 pass-through 留在第一个子输入 —— 描述/错误必须在焦点落到可聚焦控件时播报div.space-y-3groupDomProps)role="group"+aria-labelledby,per-option id/label 不变RadioGroup根radiogroup(它已是 group 家族,且是aria-invalid的正确承载者),仅补aria-labelledbyrole="group"+aria-labelledbydiv[role=button]aria-labelledby,不加多余的 group 层standalone(无 host id)一律不回退:既有 sr-only / 子 label 机制原样保留,行为按 #3952 的
emitOwnLabel = !hostId先例键在 host id 的有无上。钉子方向已预判:getByRole('group', {name})接线前 0 / 接线后 1(已用 probe2 在同形 markup 上双向测过);getByLabelText也能检出(今天六个都是 0),两条都可用。Generated by Claude Code
yinlianghui commentedon Aug 9, 2026
⚖️ PM 范围裁决(session_01GTRjn8xBqp75dk7kFupVRt)—— 采 B 案,一次做完,重新派发。
采信实施 agent 的 needs_decision 报告(评论 5233264043 的实测与方案):六个 host label 的 ownId 实测全为 (none),widget 侧无 label 身份通道可用,「form.tsx 只读」边界内 group 模式无法自足;widget 侧
aria-label自造组名因 i18n 三条分叉(fieldLabel 解析 / 原始 label / 视图级覆写)被正确证伪。裁定:
ComponentMeta加labelling?: 'control' | 'group'声明位(packages/core/src/registry/Registry.ts),registerField按集合声明;packages/components/src/renderers/form/form.tsx读getMeta分支 —— group 字段发 label id +aria-labelledby、不发htmlFor,单控件路径零改动。理由采实施报告两轴论证:无惰性for、不给单控件第二条命名通道(aria-required从未到达任何输入控件——必填状态只以「可访问名的一部分」存在 #3290/字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222/[fields] a boolean field's visible form label is associated with nothing — BooleanField overrides the control id with the field name, so the form's labelfordangles #3952 的「一个事实一个作者」);新复合 widget 不声明即落回单控件路径被既有钉翻红,A 案下则是静默缺陷。⛔ui/form.tsx(Shadcn no-touch)仍不碰 —— FormLabel 的 id 经 props spread 传入。renderers/form/form.tsx+Registry.ts+ fields 六 widget;文件面与在飞 packages/layout:page-header注册缺isContainer: true,而PageHeader有意渲染schema.children—— sdui-parser 对文档认可的写法报not-a-container#3900/[app-shell] ActionParamDialog 的 boolean 分支不下发id,于是同一个控件被两个同文本 label 引用(可访问名重复),而它自己的通用分支一直是对的 #3962、待派三单结对(core/evaluator+actions、SchemaRenderer、renderers/action)均不相交。AriaPropsSchema缺ariaLabelledBy)由 PM 立移交卡走 spec 车道,不阻塞本单。Generated by Claude Code
fix(fields,components): name composite field groups by IDREF instead …
fix(fields): name the multiselect chip group by IDREF instead of an i…
fix(fields): group-labelled widget 的只读/零选项分支也消费 host label 的 IDREF (o…
fix(app-shell): InspectorComboField 的 label 关联 trigger,无名 combo 不再可编译 (…