Skip to content

[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 label for dangles #3952

Description

@yinlianghui

Found while browser-verifying #3910 (docs field examples moved to form hosting). Out of that card's scope — pre-existing and independent of the hosting change, so filed rather than fixed.

What happens

Render any boolean field inside a form ({ type: 'form', fields: [{ name: 'notifications', label: 'Email Notifications', type: 'boolean' }] }) and the form emits two labels carrying the same text, of which the visible one points at an element that does not exist:

labels = [
  { for: "_r_3_-form-item", text: "Email Notifications", srOnly: false },   <-- visible
  { for: "notifications",   text: "Email Notifications", srOnly: true  }    <-- widget's own
]
switch id       = "notifications"   role=switch
all ids in form = ["notifications"]

label for="_r_3_-form-item" -> target exists? false
label for="notifications"   -> target exists? true

Cause

packages/components/src/renderers/form/form.tsx renders its FormLabel with the for that FormItem/FormControl generate (the _r_N_-form-item id). But packages/fields/src/widgets/BooleanField.tsx:16-17 replaces the control's id with the field name:

const generatedId = useId();
const id = config?.name || generatedId;

and then puts that id on the Switch/Checkbox (:30, :44) with its own sr-only label pointing at it (:35, :49). So the id the form told the control to use is discarded, the form's visible label references a now-nonexistent id, and the widget supplies a second, duplicate label.

Impact

  • Clicking the visible label does not toggle the switch — the normal affordance for a checkbox/switch row is dead. This is every boolean/checkbox field on every generated form in every app, not a docs-only artifact.
  • The accessible name survives only because the widget's sr-only label happens to be correctly wired; the form's own label contributes nothing. Any future change that drops the widget's internal label would silently leave the switch unnamed.
  • Duplicate label text in the DOM is also why a naive getByText(label) on a boolean field returns two nodes.

Only the widgets that override the id are affected. Text/number/etc. leave FormControl's id alone and their visible label works.

Suggested direction (not decided here)

The host should own the control id: BooleanField should use the id handed to it (the id prop the form's FormControl supplies) and fall back to useId()/name only when standalone — and then its internal sr-only label is redundant inside a form and should not be emitted twice. Worth checking the other widgets for the same id override while in there.

Reproduce

render(<SchemaRenderer schema={{ type: 'form', showSubmit: false, showCancel: false,
  fields: [{ name: 'notifications', label: 'Email Notifications', type: 'boolean' }] }} />);
// then compare every label's `for` against the ids present in the form

Confirmed in jsdom and in a real browser on /docs/fields/boolean (all three demos).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions