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).
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
booleanfield 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:Cause
packages/components/src/renderers/form/form.tsxrenders itsFormLabelwith theforthatFormItem/FormControlgenerate (the_r_N_-form-itemid). Butpackages/fields/src/widgets/BooleanField.tsx:16-17replaces the control's id with the field name:and then puts that id on the Switch/Checkbox (
:30,:44) with its ownsr-onlylabel 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
sr-onlylabel 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.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:
BooleanFieldshould use the id handed to it (theidprop the form'sFormControlsupplies) and fall back touseId()/nameonly when standalone — and then its internalsr-onlylabel 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
Confirmed in jsdom and in a real browser on
/docs/fields/boolean(all three demos).