From 21794f2ca0f7a8c2d3f79bb3d49f223b069b1d93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 22:40:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(fields):=20group-labelled=20widget=20?= =?UTF-8?q?=E7=9A=84=E5=8F=AA=E8=AF=BB/=E9=9B=B6=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=B9=9F=E6=B6=88=E8=B4=B9=20host=20label=20?= =?UTF-8?q?=E7=9A=84=20IDREF=20(#3990)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 七个 `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` 占位符本身。 --- .../fields-readonly-group-label-idref-3990.md | 57 +++ ...omposite-group-label-readonly-e2e.test.tsx | 403 ++++++++++++++++++ packages/fields/src/widgets/AddressField.tsx | 33 +- .../fields/src/widgets/CheckboxesField.tsx | 20 +- packages/fields/src/widgets/FileField.tsx | 13 +- .../fields/src/widgets/GeolocationField.tsx | 27 +- .../fields/src/widgets/MultiSelectField.tsx | 21 +- .../fields/src/widgets/OptionsEmptyState.tsx | 16 + packages/fields/src/widgets/RadioField.tsx | 20 +- packages/fields/src/widgets/RatingField.tsx | 6 +- .../fields/src/widgets/toHostGroupProps.ts | 102 +++++ 11 files changed, 689 insertions(+), 29 deletions(-) create mode 100644 .changeset/fields-readonly-group-label-idref-3990.md create mode 100644 packages/fields/src/__tests__/composite-group-label-readonly-e2e.test.tsx create mode 100644 packages/fields/src/widgets/toHostGroupProps.ts diff --git a/.changeset/fields-readonly-group-label-idref-3990.md b/.changeset/fields-readonly-group-label-idref-3990.md new file mode 100644 index 0000000000..25bff04cf5 --- /dev/null +++ b/.changeset/fields-readonly-group-label-idref-3990.md @@ -0,0 +1,57 @@ +--- +"@object-ui/fields": patch +--- + +Group-labelled field widgets now consume the host label's IDREF in their readonly and zero-option states, so the visible label names something there too + +A field declared `labelling: 'group'` (objectui#3961) is named by IDREF: the form +renderer publishes its ``'s own `id`, drops the inert `for`, and hands +the widget `aria-labelledby` plus the host control `id`. All seven such widgets +consumed that pair on their editable surface only. A field-level +`readonly: true`, and an option list that resolved to zero offered options, take +early returns that render before the container doing the consuming — so the label +published an id that no element in the document referenced. + +Measured on the previous `main`, one field per row in a real form, counting the +elements that reference the host label's published id: + +``` + consumers byLabelText named group +multiselect readonly+value 0 0 0 +multiselect readonly+empty 0 0 0 +multiselect zeroOptions 0 0 0 +multiselect editable 1 1 1 +``` + +All seven measured identically in every readonly state; each is 1 now. The +user-visible effect is the one objectui#3961 fixed for editable fields: the +visible label was the accessible name of NOTHING, so a readonly "Tags" or +"Shipping Address" was announced as loose text next to unattributed values. It is +not a regression of #3961 or objectui#3975 — before them these same states +emitted a `for` pointing at an id no element carried, which named nothing either. +The shape changed from a dangling `for` to an unconsumed IDREF; the defect did +not. + +Each readonly surface now carries exactly two keys — the host `id` and +`aria-labelledby` — plus the `role="group"` that makes them meaningful, in one +shared spelling (`toHostGroupProps`). The narrow pair is deliberate: a readonly +display has no focusable control, so `aria-describedby` / `aria-required` / +`disabled` / the focus handlers have nothing to announce on, and `name` on a +`div` is exactly the DOM leak objectui#3291 sweeps for. + +Two widgets answer with a different role than they do while editable, because +they render a different surface: `radio` (editable `radiogroup`) shows the chosen +option's label as text with no radios in it, and `file` (editable `button`, the +dropzone) shows file names with no dropzone. `role="group"` is also what the +shared "no options available" box now answers with for `checkboxes` / `radio` / +`multiselect`; the single `select` is not group-labelled, keeps its working +`for`, and that box emits nothing new for it. + +Standalone rendering is untouched. The grid's inline cell editor and bare SDUI +nodes hand down neither key, so nothing is emitted and the markup stays +byte-identical — including the `EmptyValue` placeholder, which keeps its own +`aria-label` and gains no role. Hosted-and-empty, that placeholder is the whole +readonly surface, so it carries the group props: its `aria-label` ("No value") is +then outranked by `aria-labelledby` per accname, which is the intended outcome — +on the `generic` role that span carries, an author name is prohibited and was +never exposed, so the choice was between the field's name and no name. diff --git a/packages/fields/src/__tests__/composite-group-label-readonly-e2e.test.tsx b/packages/fields/src/__tests__/composite-group-label-readonly-e2e.test.tsx new file mode 100644 index 0000000000..1febff6e85 --- /dev/null +++ b/packages/fields/src/__tests__/composite-group-label-readonly-e2e.test.tsx @@ -0,0 +1,403 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * End-to-end: a group-labelled field's visible label names the widget's surface + * in EVERY state it renders — not only the editable one (objectui#3990). + * + * The residual of objectui#3961 / #3975. Those two moved the association from an + * inert `