<c-field>: a group of checkboxes gets a label attached to nothing, so the group has no name #624

Closed
opened 2026-10-01 10:48:28 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading cotton/field.html and rendering the Freeze and attach page through the test client.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

<c-field> draws two shapes: a single checkbox, and everything else as <label for="{{ field.id_for_label }}"> followed by the widget. For a group widget (CheckboxSelectMultiple, RadioSelect) Django returns an empty id_for_label on purpose and sets use_fieldset, expecting a <fieldset> and <legend>; it also gives the group's inputs no aria-describedby and offers it as BoundField.aria_describedby for the fieldset instead. The component checks for none of this, so a group drawn through it gets <label for="">: the group has no accessible name, and its help text and errors are tied to nothing. The pages that draw groups by hand (api/token_list.html:39-40, applications/interview_form.html:32-33, applications/tag_form.html:43-60, jobs/company_form.html:44-46) already do it the right way.

  • Where: templates/cotton/field.html:36-50; used for groups by templates/documents/send.html:52-54, which loops every field of SendDocumentsForm (documents/forms.py:302-318, uploads and links, both CheckboxSelectMultiple) through <c-field>.
  • What happens: the page at documents:send renders <label for="">Files you have already</label><div id="id_uploads">… and <p id="id_uploads_helptext">…</p>, with nothing pointing at either. When the person has no links, the empty links group falls into the single-checkbox branch instead (not field.field.widget.choices is true for an empty queryset), which draws an empty <div id="id_links"> beside a second <label for="">. A screen-reader user moving through the checkboxes hears each file's name, never "Files you have already" or "Links you pointed them at", and never either help sentence, so two groups of similar items run together.
  • Why it matters: WCAG 2.2 1.3.1 and 4.1.2 (level A) on a page every applicant uses when they send documents, and any later group field drawn through <c-field> inherits the same fault.
  • Fix: in cotton/field.html, branch on field.use_fieldset before the other two shapes: draw <fieldset class="field mb-4" {% if field.aria_describedby %}aria-describedby="{{ field.aria_describedby }}"{% endif %}> with <legend>{{ field.label }}</legend>, the widget and <c-field-feedback>, as the hand-drawn groups above do. Keep the single-checkbox branch for a lone checkbox only (test the widget class, not whether it has choices).

Done when

  • A test in tests/test_components.py renders a CheckboxSelectMultiple field and a RadioSelect field through <c-field>, with and without choices, and asserts a <fieldset> with a <legend>, an aria-describedby naming the help text, and no for="" anywhere.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading `cotton/field.html` and rendering the *Freeze and attach* page through the test client. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `<c-field>` draws two shapes: a single checkbox, and everything else as `<label for="{{ field.id_for_label }}">` followed by the widget. For a group widget (`CheckboxSelectMultiple`, `RadioSelect`) Django returns an empty `id_for_label` on purpose and sets `use_fieldset`, expecting a `<fieldset>` and `<legend>`; it also gives the group's inputs no `aria-describedby` and offers it as `BoundField.aria_describedby` for the fieldset instead. The component checks for none of this, so a group drawn through it gets `<label for="">`: the group has no accessible name, and its help text and errors are tied to nothing. The pages that draw groups by hand (`api/token_list.html:39-40`, `applications/interview_form.html:32-33`, `applications/tag_form.html:43-60`, `jobs/company_form.html:44-46`) already do it the right way. - **Where:** `templates/cotton/field.html:36-50`; used for groups by `templates/documents/send.html:52-54`, which loops every field of `SendDocumentsForm` (`documents/forms.py:302-318`, `uploads` and `links`, both `CheckboxSelectMultiple`) through `<c-field>`. - **What happens:** the page at `documents:send` renders `<label for="">Files you have already</label><div id="id_uploads">…` and `<p id="id_uploads_helptext">…</p>`, with nothing pointing at either. When the person has no links, the empty `links` group falls into the single-checkbox branch instead (`not field.field.widget.choices` is true for an empty queryset), which draws an empty `<div id="id_links">` beside a second `<label for="">`. A screen-reader user moving through the checkboxes hears each file's name, never "Files you have already" or "Links you pointed them at", and never either help sentence, so two groups of similar items run together. - **Why it matters:** WCAG 2.2 1.3.1 and 4.1.2 (level A) on a page every applicant uses when they send documents, and any later group field drawn through `<c-field>` inherits the same fault. - **Fix:** in `cotton/field.html`, branch on `field.use_fieldset` before the other two shapes: draw `<fieldset class="field mb-4" {% if field.aria_describedby %}aria-describedby="{{ field.aria_describedby }}"{% endif %}>` with `<legend>{{ field.label }}</legend>`, the widget and `<c-field-feedback>`, as the hand-drawn groups above do. Keep the single-checkbox branch for a lone checkbox only (test the widget class, not whether it has choices). ## Done when - A test in `tests/test_components.py` renders a `CheckboxSelectMultiple` field and a `RadioSelect` field through `<c-field>`, with and without choices, and asserts a `<fieldset>` with a `<legend>`, an `aria-describedby` naming the help text, and no `for=""` anywhere.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:28 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#624
No description provided.