Telephone field: the number box drops aria-describedby and aria-invalid, so its help and errors reach no screen reader #416

Closed
opened 2026-10-01 10:46:32 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by rendering a bound PhoneField beside a plain CharField and comparing the markup.

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

Since #114, Postulo relies on Django writing aria-describedby="<id>_helptext <id>_error" and aria-invalid="true" on each widget. <c-field-feedback> draws the help and error blocks with those ids. The telephone widget's template writes its own <select> and <input> with only a name and an id. Every other attribute Django computed for the field, including the ARIA pair, is thrown away.

  • Where: templates/partials/phone_widget.html:26-39 (the hand-written <select name=… id=…> and <input type="text" name=… id=… value=… class=… inputmode="tel" autocomplete="tel">, which never read widget.attrs); core/phone_field.py:20-50 (PhoneWidget); templates/cotton/field_feedback.html:28-35 (the _helptext and _error blocks the box should point at). The template is used by the single box on Your details and the contact form (<c-field :field="form.phone" />), and by every row of partials/phone_numbers.html:34-35, whose feedback is drawn at :88.
  • What happens: in the rendering test, the number box came out as <input type="text" name="phone_1" id="id_phone_1" value="" class="min-w-0 flex-1" inputmode="tel" autocomplete="tel">, with no aria-describedby and no aria-invalid. A plain CharField in the same form rendered aria-invalid="true" aria-describedby="id_other_helptext id_other_error". A screen-reader user whose number is refused (for example "That number is already recorded on this instance…") lands on a box that announces neither that it is invalid nor why. It is also not described by its help text ("Kept in the international form…").
  • Why it matters: this fails WCAG 2.2 SC 1.3.1 and 3.3.1 (level A) on Your details and on every contact, which is the failure #114 fixed for every other field.
  • Fix: render the subwidgets' own attributes instead of choosing two, for example with {% include "django/forms/widgets/attrs.html" %} inside each tag, or by rendering each subwidget through its template_name. The number box should then carry the field-level aria-describedby and aria-invalid that Django put in widget.attrs. Keep the select's own aria-label.

Related to #114 (closed), which fixed the dangling references for fields drawn through <c-field> but not for this widget. Related to #304, which rebuilds this control as one input group and should carry the fix and its test.

Done when

  • A test in tests/test_assistive_names.py renders a bound PhoneField with help text and an error. It asserts that the number box has aria-invalid="true" and an aria-describedby naming the _helptext and _error ids that <c-field-feedback> draws, and that both ids exist in the page.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by rendering a bound `PhoneField` beside a plain `CharField` and comparing the markup. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. Since #114, Postulo relies on Django writing `aria-describedby="<id>_helptext <id>_error"` and `aria-invalid="true"` on each widget. `<c-field-feedback>` draws the help and error blocks with those ids. The telephone widget's template writes its own `<select>` and `<input>` with only a name and an id. Every other attribute Django computed for the field, including the ARIA pair, is thrown away. - **Where:** `templates/partials/phone_widget.html:26-39` (the hand-written `<select name=… id=…>` and `<input type="text" name=… id=… value=… class=… inputmode="tel" autocomplete="tel">`, which never read `widget.attrs`); `core/phone_field.py:20-50` (`PhoneWidget`); `templates/cotton/field_feedback.html:28-35` (the `_helptext` and `_error` blocks the box should point at). The template is used by the single box on *Your details* and the contact form (`<c-field :field="form.phone" />`), and by every row of `partials/phone_numbers.html:34-35`, whose feedback is drawn at `:88`. - **What happens:** in the rendering test, the number box came out as `<input type="text" name="phone_1" id="id_phone_1" value="" class="min-w-0 flex-1" inputmode="tel" autocomplete="tel">`, with no `aria-describedby` and no `aria-invalid`. A plain `CharField` in the same form rendered `aria-invalid="true" aria-describedby="id_other_helptext id_other_error"`. A screen-reader user whose number is refused (for example "That number is already recorded on this instance…") lands on a box that announces neither that it is invalid nor why. It is also not described by its help text ("Kept in the international form…"). - **Why it matters:** this fails WCAG 2.2 SC 1.3.1 and 3.3.1 (level A) on *Your details* and on every contact, which is the failure #114 fixed for every other field. - **Fix:** render the subwidgets' own attributes instead of choosing two, for example with `{% include "django/forms/widgets/attrs.html" %}` inside each tag, or by rendering each subwidget through its `template_name`. The number box should then carry the field-level `aria-describedby` and `aria-invalid` that Django put in `widget.attrs`. Keep the select's own `aria-label`. Related to #114 (closed), which fixed the dangling references for fields drawn through `<c-field>` but not for this widget. Related to #304, which rebuilds this control as one input group and should carry the fix and its test. ## Done when - A test in `tests/test_assistive_names.py` renders a bound `PhoneField` with help text and an error. It asserts that the number box has `aria-invalid="true"` and an `aria-describedby` naming the `_helptext` and `_error` ids that `<c-field-feedback>` draws, and that both ids exist in the page.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:32 +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#416
No description provided.