在 #3975(PR #3983,给 multiselect 加 labelling: 'group')的验证过程中顺手实测到的一条,不夹带进那个 PR:同一失效类的另一条路径,而且它无法用「往 FIELD_TYPES_GROUP_LABELLED 里再加一项」来修。
实测(在 #3975 的修复已生效的树上)
一次性 probe(未提交),真表单 + registerAllFields() 真惰性注册路径:
type: 'multiselect' → byRoleGroupNamed=1 role=group ← #3975 已修
type: 'field:select', multiple: true → for=…-form-item 解析到 div
byRoleGroupNamed=0 byLabelText=0 ← 本单
为什么 #3975 的修法碰不到它
三段都是现成的、各自都正确的代码,拼起来漏掉一格:
packages/fields/src/field-type-alias.ts:45 —— mapFieldTypeToFormType 只按字段的 type 字符串映射:select: 'field:select',不看 multiple;
packages/plugin-form/src/sectionFields.ts:101,109 —— 对象表单据此发出 type: 'field:select',并显式把 multiple: field.multiple 抄进 form field(ObjectForm.tsx:567 同样走 mapFieldTypeToFormType,并以 field: field 携带整份元数据);
packages/fields/src/widgets/SelectField.tsx:36-41 —— SelectField 见到 config.multiple 就委派给 MultiSelectField,渲染出 chip 行那个包裹 div。
于是渲染的是 MultiSelectField,但决定 label 怎么关联的声明是按 select 这个键查的(resolveFieldLabelling → getMeta('select')),而 select 必须保持未声明 —— 单选的 trigger 是可 label 的,给它剥掉 for 是把一个能用的字段弄坏(form-group-label-association.test.ts 里已有一条钉专门守这个方向:「a BUILTIN type ignores a registry declaration under the same name」)。
所以 host label 照旧发 for,指向 chip 行的 div,而 label[for] 指向不可 label 的元素是惰性的(HTMLLabelElement.control 返回 null):视觉上有标签,屏幕阅读器听不到这一组是什么。与 #3975 / #3961 同一个事实,只是入口不同。
可达性:这不是只能手写 SDUI 才碰到的死角
spec 里 multiple 对 select 合法,而上面第 2 步是对象表单的正常路径。也就是说一个 { type: 'select', multiple: true } 的普通 picklist 字段,在真实对象表单里就是这个形态。(metadata-admin 自己的 SchemaForm.tsx:197 走的是另一个映射器,它 multiple ? 'multiselect' : 'select' —— 恰好绕过了本问题,这也是它一直没被发现的原因。)
处置需要裁决(所以不自行实施)
倾向 A(长期正确:一处决定「渲染哪个 widget」,声明与渲染因此不可能再错位),但它的波及面值得先量一遍再动手 —— 交给三分诊定。
参考
在 #3975(PR #3983,给
multiselect加labelling: 'group')的验证过程中顺手实测到的一条,不夹带进那个 PR:同一失效类的另一条路径,而且它无法用「往FIELD_TYPES_GROUP_LABELLED里再加一项」来修。实测(在 #3975 的修复已生效的树上)
一次性 probe(未提交),真表单 +
registerAllFields()真惰性注册路径:为什么 #3975 的修法碰不到它
三段都是现成的、各自都正确的代码,拼起来漏掉一格:
packages/fields/src/field-type-alias.ts:45——mapFieldTypeToFormType只按字段的type字符串映射:select: 'field:select',不看multiple;packages/plugin-form/src/sectionFields.ts:101,109—— 对象表单据此发出type: 'field:select',并显式把multiple: field.multiple抄进 form field(ObjectForm.tsx:567同样走mapFieldTypeToFormType,并以field: field携带整份元数据);packages/fields/src/widgets/SelectField.tsx:36-41——SelectField见到config.multiple就委派给MultiSelectField,渲染出 chip 行那个包裹div。于是渲染的是
MultiSelectField,但决定 label 怎么关联的声明是按select这个键查的(resolveFieldLabelling→getMeta('select')),而select必须保持未声明 —— 单选的 trigger 是可 label 的,给它剥掉for是把一个能用的字段弄坏(form-group-label-association.test.ts里已有一条钉专门守这个方向:「a BUILTIN type ignores a registry declaration under the same name」)。所以 host label 照旧发
for,指向 chip 行的div,而label[for]指向不可 label 的元素是惰性的(HTMLLabelElement.control返回 null):视觉上有标签,屏幕阅读器听不到这一组是什么。与 #3975 / #3961 同一个事实,只是入口不同。可达性:这不是只能手写 SDUI 才碰到的死角
spec 里
multiple对select合法,而上面第 2 步是对象表单的正常路径。也就是说一个{ type: 'select', multiple: true }的普通 picklist 字段,在真实对象表单里就是这个形态。(metadata-admin 自己的SchemaForm.tsx:197走的是另一个映射器,它multiple ? 'multiselect' : 'select'—— 恰好绕过了本问题,这也是它一直没被发现的原因。)处置需要裁决(所以不自行实施)
mapFieldTypeToFormType对select+multiple发field:multiselect。声明保持「按类型的纯事实」,SelectField的委派分支在这条路径上随之变成死码。契约优先、结构上不易错,但有波及面:CASCADE_OPTION_FIELD_TYPES/DATA_SOURCE_FIELD_TYPES的匹配、以及任何按field:select判断的地方都要一起看,而且它需要一个值(不只是类型)才能算出 widget 名 —— 得确认所有调用点都拿得到multiple。resolveFieldLabelling读字段配置的multiple—— 改动最小,但把「labelling 是每个类型的一个事实」变成值相关的判断,声明位从此不再是纯声明;[fields] 复合/分组 field widget 丢弃或错投 host 下发的控件 id:address / geolocation 的表单 labelfor悬空,checkboxes / radio / rating / file 的for落在不可 label 的 div 上 #3961 刻意把它做成声明而不是让 renderer 猜 DOM,B 是往回走一步。倾向 A(长期正确:一处决定「渲染哪个 widget」,声明与渲染因此不可能再错位),但它的波及面值得先量一遍再动手 —— 交给三分诊定。
参考
for(#3975) #3983(multiselect本体,已修)for悬空,checkboxes / radio / rating / file 的for落在不可 label 的 div 上 #3961 / PR fix(fields,components): 复合/分组 field widget 的组标签改用 IDREF 关联(#3961) #3978(labelling声明位与六个 widget)select分支把 host id 展开到 Radix Select.Root —— 手写 form schema 里type: 'select'的 labelfor全部悬空(#3306 的内建侧同形) #3976(内建select分支把 host id 展开到 Radix Select.Root —— 另一条路径:那条走内建Select,本单走注册的SelectField→MultiSelectField,两者不重叠)emptyHint被每个选项 widget 丢弃,用户看到的是硬编码英文 #3231(field:select与select命名同一种字段、只认裸名导致整块逻辑落空 —— 同一根源的先例)