Uh oh!
There was an error while loading. Please reload this page.
fix(fields): resolve select + multiple to field:multiselect so its host label names the chip group - #4013
Merged
yinlianghui merged 1 commit intoAug 10, 2026
Conversation
…st label names the chip group
`mapFieldTypeToFormType` keyed the widget id on the field `type` string alone,
so an object-schema `{ type: 'select', multiple: true }` picklist became
`field:select`. `SelectField` then delegated to `MultiSelectField` on
`config.multiple`: the component that RENDERED was the chip picker, while
everything keyed on the widget id still answered for the single-value combobox
-- above all the label-association declaration (`ComponentMeta.labelling`,
objectui#3961), which the form renderer resolves per widget id. The host emitted
`label for` at the chip row's wrapper div, where a `for` is inert
(`HTMLLabelElement.control` is null): a visible label naming nothing.
Declaring `select` itself `labelling: 'group'` was not available -- a
single-value select's trigger is a labelable `button[role=combobox]` whose `for`
works, and a bare `select` is a builtin the renderer resolves without the
registry. So the producer names the widget that renders: the id now carries the
arity, and declaration and render can no longer diverge.
- `mapFieldTypeToFormType(fieldType, config?)` gains an optional second
argument; only `multiple` is read. Single-argument calls are unchanged. The
override is table-driven and `select` is its only member: the spec's
multi-capable set is larger (select / lookup / file / image, `radio` on the
select branch, `user` storing like `lookup`), but LookupField, FileField and
ImageField each render both arities themselves, so their id -- and their
labelling declaration -- is already right either way.
- The four object-form producers pass the pair: ObjectForm, DrawerForm,
ModalForm, sectionFields. In sectionFields the id is computed once from the
EFFECTIVE pair after view-level overrides merge, because `multiple` is itself
a spec FormField key: a view restating only `multiple: true` moves the widget
too, and `multiple: false` moves it back.
- SelectField's delegation is KEPT, not retired. Measured, three entrances
still reach it with `multiple` set, none of which consult the alias map: the
inline grid editor (FieldEditWidget finds `select` in its own table first),
ActionParamDialog (resolveFormWidgetType returns `select` from
fieldWidgetMap first), and hand-written SDUI naming `field:select`.
Read-only rendering is untouched (objectui#4005); so is the built-in `Select`
branch (objectui#3976).
Fixes#3986
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
|
This was referenced Aug 10, 2026
yinlianghui
commented
Aug 10, 2026
CollaboratorAuthor
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3986 落地 —— select+multiple 的 widget 由生产端一处决定,#3961 的 labelling 声明从此永远查到实际渲染的组件。 核验记录(实物核验 + CI 亲读):
#4005(只读态 describedby)自本 PR 落 main 后解锁可派 —— 当前额度受限暂停派发,已在队列注记。 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 10, 2026 01:22
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-3986-select-multiple-widget-mapping
branch
August 10, 2026 01:22
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 freeto 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#3986
按 PM 已裁的 A 案实施:让生产端决定渲染哪个 widget ——
mapFieldTypeToFormType对select+multiple: true发field:multiselect。一处决定 widget,声明(ComponentMeta.labelling)与渲染从此不可能错位。机制回顾(三段各自正确,拼起来漏一格)
mapFieldTypeToFormType只按type字符串映射,select恒为field:select,不看multiple;field:select,并把multiple随元数据带下去;SelectField见到config.multiple就委派给MultiSelectField。于是渲染的是 chip 行(
MultiSelectField),但 label 关联的声明是按select这个键查的 —— 而select必须保持未声明(单选 trigger 是button[role=combobox],可 label;裸select还是内建分支,压根不查注册表)。host 照旧发for指向 chip 行的包裹div,label[for]指向不可 label 元素是惰性的(HTMLLabelElement.control返回null):视觉上有标签,可及性树里这个标签什么都没命名。波及面测量表(改之前逐个量,正文点名的三项)
① 两个集合的匹配逻辑
field:select→field:multiselectCASCADE_OPTION_FIELD_TYPEScomponents/renderers/form/form.tsx:250normalizeFieldType(剥field:前缀)再查集合select,radio,multiselect,checkboxes} —— 两个拼写都是成员dependentValues/emptyHint照旧注入DATA_SOURCE_FIELD_TYPESform.tsx:233lookup,master_detail,tree,object-ref,filter-condition,recipient-picker} —— 两个拼写都不是成员dataSource② 一切按
field:select字符串判断的调用点(全仓 grep,逐点)BUILTIN_FIELD_TYPESform.tsx:232select,且刻意按 RAW type 比对;两个带前缀拼写都不命中 → 不变(裸select走内建分支的行为零触碰)resolveFieldLabellingform.tsx:348field:multiselect→ normalize →multiselect→labelling: 'group'→ host 改发 IDREFform.tsx:944f.widget或f.type),未 normalize;field:select与field:multiselect同样都落不进去 → 不变(既有缺陷,与本单无关,见文末「超范围发现」)PROTOCOL_COMPONENTSrenderers/placeholders.tsx:64field:select与field:multiselect都已在列 → 不变resolveActionParams.ts:37等消费半径核验:全仓 grep 确认
mapFieldTypeToFormType的非注释引用只在packages/fields(自身 2 处)与packages/plugin-form(4 处);components / app-shell 里的命中全是注释。③ 每个调用点是否拿得到
multiplesectionFields.ts:101(fromObjectSchema)field.multiple(整份对象元数据在作用域内)sectionFields.ts:148(spec 覆盖分支)fd.multiple与base.multiple都在ObjectForm.tsx:567field.multipleDrawerForm.tsx:325field.multipleModalForm.tsx:398field.multiplefields/src/index.tsx:2160(resolveFormWidgetType)fieldWidgetMap['select']命中即早退,select永不到达映射器fields/src/FieldEditWidget.tsx:145(resolveInlineEditType)'select' in EDIT_WIDGETS早退,select永不到达映射器没有任何调用点结构上被卡住(两个只有 type 字符串的调用点都在
select到达映射器之前早退,且它们本就不传第二参数,行为可证不变),因此按 A 案继续,未升级为契约面裁决。实施
mapFieldTypeToFormType(fieldType, config?)增可选第二参(结构化,只读multiple,仿 spec 自己的ValueShapeFieldDef写法),所有既有单参调用保持有效。MULTI_VALUE_FORM_TYPES)而非散落的if,且select是唯一成员 —— 这一点是量出来的:spec 的MULTI_CAPABLE_TYPES更大(select / lookup / file / image,radio挂在 select 分支、user与lookup同存储),但LookupField/FileField/ImageField各自在同一个 widget 内分支处理multiple,widget id(以及随之的 labelling 声明)对两种 arity 都已正确。只有select的多值形态是另一个组件。sectionFields的 spec 覆盖分支:id 改为在所有覆盖合并之后一次性从有效 pair 算出。因为multiple本身就是 spec FormField 键 —— 视图只重述multiple: true(不重述type)也必须把 widget 移过去,multiple: false也必须移回来。这是同一缺陷的第二道门。死码处置:
SelectField的委派分支 —— 保留,并注明量完确认它不是死码。三个入口仍会带着
multiple走到它,且都不经过类型解析层:FieldEditWidget先在自己的EDIT_WIDGETS表里查到select就早退,并把整份元数据作为field转发ActionParamDialogparamToField经resolveFormWidgetType,同样先从fieldWidgetMap拿到select就早退,并带上multiple: param.multiple{ type: 'field:select' }且元数据声明multiple所以按仓内语义保留,并把 doc comment 里「对象表单也走这里」的已过期说法改成上表(原注释声称覆盖 the object form —— 本 PR 之后不再成立,不留给下一个读者踩)。
反向验证(先预判,再跑变异,未提交)
变异 = 删掉映射器里的 arity override(调用点照旧传 pair),即精确撤掉构成修复的那一处逻辑。
预判方向 —— 普通 Red,不属于「更多诊断」或「倒置」两族:此前这条栈从未产出规范 id,
field:multiselect的 group 声明压根没被查过,所以修复前是规则未施加、修复后才施加。实测与预判一致:9 红,方向逐条对上
Regions的byRoleGroupNamed)红、映射钉红、声明接合钉红、normalizeSectionField的多值三例红;select的for→ trigger 关联、multiple: false回落单值、select-label-association-e2e(两条 select 路径)、composite-group-label-e2e(含 [fields] multiselect 的 host label 也落在不可 label 的包裹 div 上 —— 与 #3961 同一失效类,但不在其审计的六个 widget 内 #3975 的multiselect)、以及 components 的「a BUILTIN type ignores a registry declaration under the same name」。映射钉的实际输出:
测试
packages/fields/src/field-type-alias.multiple.test.ts(新)—— 映射钉:两个方向 + 幂等 + spec 多值可用但不移动 widget 的 6 个类型 + 未知类型兜底。packages/fields/src/__tests__/group-labelling-declaration.test.ts—— 加接合钉:生产端发出的 id 必须携带正确声明。这条只能放在两者之间 —— 映射器的测试不知道自己返回的 id 带什么labelling,声明测试不知道多值 select 解析到哪个 id,缺陷就活在两个盲区之间。packages/plugin-form/src/__tests__/selectMultipleGroupLabel.test.tsx(新)—— 真ObjectForm+ 真表单渲染器 + 真注册表 + 真 widget 的复现钉,只 stub dataSource;含守方向单选对照与「一张表里三个字段、没有任何 label 留着惰性/悬空for」的类不变式;另含normalizeSectionField的有效-pair 单元钉。aria-labelledby反查回 label 元素、检查 IDREF 不悬空);getAllByLabelText因 Set 去重,断言长度而不用getBy顺带成立;labelable白名单区分「resolves 但不可 label」这一本单特有形态。registerAllFields的React.lazy边界按 AGENTS.md 测试纪律在模块作用域预热(barrel 静态 re-export 即 warm-up),不放beforeAll。验证命令与结果
pnpm exec vitest run packages/fields packages/plugin-form --maxWorkers=2→ 112 files / 1523 tests passedpnpm exec turbo run type-check --concurrency=2→ 78/78 successfulnode scripts/check-control-bytes.mjs→ OK(3912 文件);另按[\x00-\x08\x0b\x0c\x0e-\x1f]自扫本 PR 全部改动文件,无命中面切割
Select路径([components] 表单渲染器内建select分支把 host id 展开到 Radix Select.Root —— 手写 form schema 里type: 'select'的 labelfor全部悬空(#3306 的内建侧同形) #3976 已修)—— 零触碰,并有对照钉守住。content/docs/releases/**—— 未触碰;user-visible 变更走 changeset(@object-ui/fields+@object-ui/plugin-form,patch)。超范围发现(仅报告,未在本 PR 修)
form.tsx:944的resolvedType取的是 RAW type(f.widget或f.type),与集合里的裸名比对;对象表单发的是带前缀 id,于是任何来自对象 schema 的选项字段都进不了这个 effect,选项集收窄后旧值不会被清掉。与 表单渲染器计算的emptyHint被每个选项 widget 丢弃,用户看到的是硬编码英文 #3231 同族:同文件:1514的isOptionField已经改用normalizeFieldType修过一次,这一处漏了。本 PR 前后同样都落不进去,故语义不变。RadioField完全不读multiple—— spec 的MULTI_CAPABLE_TYPES把radio挂在 select 分支(即multiple对radio合法),但 widget 里没有任何multiple分支,{ type: 'radio', multiple: true }会静默渲染成单值 radio 组。观察类:radio本身已声明labelling: 'group',不存在本单这条 a11y 洞。Generated by Claude Code