From 966ecdd482167f3f6b08b00f484936c837751cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20Jer=C5=A1e?= Date: Thu, 1 Jun 2023 16:44:57 +0200 Subject: [PATCH] Fixed #32819 -- Established relationship between form fields and their help text. Thanks Nimra for the initial patch. Thanks Natalia Bidart, Thibaud Colas, David Smith, and Mariusz Felisiak for reviews. --- django/forms/boundfield.py | 7 ++ django/forms/jinja2/django/forms/field.html | 2 +- django/forms/jinja2/django/forms/p.html | 2 +- django/forms/jinja2/django/forms/table.html | 2 +- django/forms/jinja2/django/forms/ul.html | 2 +- .../forms/templates/django/forms/field.html | 2 +- django/forms/templates/django/forms/p.html | 2 +- .../forms/templates/django/forms/table.html | 2 +- django/forms/templates/django/forms/ul.html | 2 +- docs/ref/forms/fields.txt | 42 +++++++++++ docs/releases/5.0.txt | 16 ++++- docs/topics/forms/index.txt | 4 +- tests/admin_inlines/tests.py | 2 +- tests/forms_tests/tests/test_forms.py | 70 ++++++++++++++++++- tests/model_forms/tests.py | 3 +- 15 files changed, 144 insertions(+), 16 deletions(-) diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py index 39b0aaf97b..deba739329 100644 --- a/django/forms/boundfield.py +++ b/django/forms/boundfield.py @@ -287,6 +287,13 @@ class BoundField(RenderableFieldMixin): attrs["required"] = True if self.field.disabled: attrs["disabled"] = True + # If a custom aria-describedby attribute is given and help_text is + # used, the custom aria-described by is preserved so user can set the + # desired order. + if custom_aria_described_by_id := widget.attrs.get("aria-describedby"): + attrs["aria-describedby"] = custom_aria_described_by_id + elif self.field.help_text and self.id_for_label: + attrs["aria-describedby"] = f"{self.id_for_label}_helptext" return attrs @property diff --git a/django/forms/jinja2/django/forms/field.html b/django/forms/jinja2/django/forms/field.html index 56ffa1ad83..c8943d9481 100644 --- a/django/forms/jinja2/django/forms/field.html +++ b/django/forms/jinja2/django/forms/field.html @@ -4,7 +4,7 @@ {% else %} {% if field.label %}{{ field.label_tag() }}{% endif %} {% endif %} -{% if field.help_text %}
{{ field.help_text|safe }}
{% endif %} +{% if field.help_text %}
{{ field.help_text|safe }}
{% endif %} {{ field.errors }} {{ field }} {% if field.use_fieldset %}{% endif %} diff --git a/django/forms/jinja2/django/forms/p.html b/django/forms/jinja2/django/forms/p.html index a2872d993e..4db46058b5 100644 --- a/django/forms/jinja2/django/forms/p.html +++ b/django/forms/jinja2/django/forms/p.html @@ -8,7 +8,7 @@ {% if field.label %}{{ field.label_tag() }}{% endif %} {{ field }} {% if field.help_text %} - {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if loop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/django/forms/jinja2/django/forms/table.html b/django/forms/jinja2/django/forms/table.html index d1d51f2b12..18fa63f8a7 100644 --- a/django/forms/jinja2/django/forms/table.html +++ b/django/forms/jinja2/django/forms/table.html @@ -16,7 +16,7 @@ {{ field }} {% if field.help_text %}
- {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if loop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/django/forms/jinja2/django/forms/ul.html b/django/forms/jinja2/django/forms/ul.html index cc4d893c0e..42c46847fd 100644 --- a/django/forms/jinja2/django/forms/ul.html +++ b/django/forms/jinja2/django/forms/ul.html @@ -12,7 +12,7 @@ {% if field.label %}{{ field.label_tag() }}{% endif %} {{ field }} {% if field.help_text %} - {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if loop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/django/forms/templates/django/forms/field.html b/django/forms/templates/django/forms/field.html index 8f26213782..72fb357b71 100644 --- a/django/forms/templates/django/forms/field.html +++ b/django/forms/templates/django/forms/field.html @@ -4,7 +4,7 @@ {% else %} {% if field.label %}{{ field.label_tag }}{% endif %} {% endif %} -{% if field.help_text %}
{{ field.help_text|safe }}
{% endif %} +{% if field.help_text %}
{{ field.help_text|safe }}
{% endif %} {{ field.errors }} {{ field }} {% if field.use_fieldset %}{% endif %} diff --git a/django/forms/templates/django/forms/p.html b/django/forms/templates/django/forms/p.html index 1346937a34..829c42eca6 100644 --- a/django/forms/templates/django/forms/p.html +++ b/django/forms/templates/django/forms/p.html @@ -8,7 +8,7 @@ {% if field.label %}{{ field.label_tag }}{% endif %} {{ field }} {% if field.help_text %} - {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if forloop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/django/forms/templates/django/forms/table.html b/django/forms/templates/django/forms/table.html index d4aaafcf53..5d41bc4402 100644 --- a/django/forms/templates/django/forms/table.html +++ b/django/forms/templates/django/forms/table.html @@ -16,7 +16,7 @@ {{ field }} {% if field.help_text %}
- {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if forloop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/django/forms/templates/django/forms/ul.html b/django/forms/templates/django/forms/ul.html index ae38af6527..7383b96235 100644 --- a/django/forms/templates/django/forms/ul.html +++ b/django/forms/templates/django/forms/ul.html @@ -12,7 +12,7 @@ {% if field.label %}{{ field.label_tag }}{% endif %} {{ field }} {% if field.help_text %} - {{ field.help_text|safe }} + {{ field.help_text|safe }} {% endif %} {% if forloop.last %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index e443e9eabf..77ccc9e862 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -275,6 +275,48 @@ fields. We've specified ``auto_id=False`` to simplify the output:
Sender:
A valid email address, please.
Cc myself:
+When a field has help text and :attr:`~django.forms.BoundField.id_for_label` +returns a value, we associate ``help_text`` with the input using the +``aria-describedby`` HTML attribute: + +.. code-block:: pycon + + >>> from django import forms + >>> class UserForm(forms.Form): + ... username = forms.CharField(max_length=255, help_text="e.g., user@example.com") + ... + >>> f = UserForm() + >>> print(f) +
+ +
e.g., user@example.com
+ +
+ +When adding a custom ``aria-describedby`` attribute, make sure to also include +the ``id`` of the ``help_text`` element (if used) in the desired order. For +screen reader users, descriptions will be read in their order of appearance +inside ``aria-describedby``: + +.. code-block:: pycon + + >>> class UserForm(forms.Form): + ... username = forms.CharField( + ... max_length=255, + ... help_text="e.g., user@example.com", + ... widget=forms.TextInput( + ... attrs={"aria-describedby": "custom-description id_username_helptext"}, + ... ), + ... ) + ... + >>> f = UserForm() + >>> print(f["username"]) + + +.. versionchanged:: 5.0 + + ``aria-describedby`` was added to associate ``help_text`` with its input. + ``error_messages`` ------------------ diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index a2a04c6060..f309721d44 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -61,7 +61,9 @@ For example, the template below:
{{ form.name.label_tag }} {% if form.name.help_text %} -
{{ form.name.help_text|safe }}
+
+ {{ form.name.help_text|safe }} +
{% endif %} {{ form.name.errors }} {{ form.name }} @@ -69,7 +71,9 @@ For example, the template below:
{{ form.email.label_tag }} {% if form.email.help_text %} -
{{ form.email.help_text|safe }}
+
+ {{ form.email.help_text|safe }} +
{% endif %} {{ form.email.errors }} {{ form.email }} @@ -77,7 +81,9 @@ For example, the template below:
{{ form.password.label_tag }} {% if form.password.help_text %} -
{{ form.password.help_text|safe }}
+
+ {{ form.password.help_text|safe }} +
{% endif %} {{ form.password.errors }} {{ form.password }} @@ -294,6 +300,10 @@ Forms * The new ``assume_scheme`` argument for :class:`~django.forms.URLField` allows specifying a default URL scheme. +* In order to improve accessibility and enable screen readers to associate form + fields with their help text, the form field now includes the + ``aria-describedby`` HTML attribute. + Generic Views ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 27ea496ca6..673111b025 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -723,7 +723,9 @@ loop: {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.help_text %} -

{{ field.help_text|safe }}

+

+ {{ field.help_text|safe }} +

{% endif %}
{% endfor %} diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 0be8d665d1..9533cc9af3 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -452,7 +452,7 @@ class TestInline(TestDataMixin, TestCase): self.assertContains( response, '', + 'maxlength="100" aria-describedby="id_-1-0-name_helptext">', html=True, ) self.assertContains( diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index c283ab20ef..99a38eedcb 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -3016,6 +3016,72 @@ Options: ' + "
" + '' + '
Wählen Sie mit Bedacht.' + '
', + ) + self.assertHTMLEqual( + p.as_ul(), + '
  • ' + 'e.g., user@example.com' + "
  • " + '' + '' + 'Wählen Sie mit Bedacht.' + "
  • ", + ) + self.assertHTMLEqual( + p.as_p(), + '

    ' + 'e.g., user@example.com' + "

    " + '' + '' + 'Wählen Sie mit Bedacht.' + "

    ", + ) + self.assertHTMLEqual( + p.as_table(), + '' + '
    ' + 'e.g., user@example.com' + "" + '' + '
    ' + 'Wählen Sie mit Bedacht.' + "", + ) + def test_subclassing_forms(self): # You can subclass a Form to add fields. The resulting form subclass will have # all of the fields of the parent Form, plus whichever fields you define in the @@ -4796,7 +4862,7 @@ class TemplateTests(SimpleTestCase): "
    " '

    ' '

    " + 'aria-describedby="id_username_helptext" required>

    ' '

    ' '

    ' '

    ' @@ -4833,7 +4899,7 @@ class TemplateTests(SimpleTestCase): "" '

    Username:' '

    " + 'aria-describedby="id_username_helptext" required>

    ' '

    Password1:' '

    ' '

    Password2:' diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index b807e90ef9..d1716ce201 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -958,7 +958,8 @@ class TestFieldOverridesByFormMeta(SimpleTestCase): ) self.assertHTMLEqual( str(form["slug"]), - '', + '', ) def test_label_overrides(self):