summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Smith <smithdc@gmail.com>2024-12-13 08:20:27 +0000
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-12-17 12:04:44 +0100
commit1e05431881d64e5e009cd9a709225744c05a48f1 (patch)
treee27c5e21a7f3198b3b47572a7ba6a92d47e2edc9
parent27375ad50ea3306844aab8122de13e9b3e0d1189 (diff)
Refs #32819 -- Added aria-describedby property to BoundField.
-rw-r--r--django/forms/boundfield.py28
-rw-r--r--django/forms/jinja2/django/forms/field.html2
-rw-r--r--django/forms/templates/django/forms/field.html2
-rw-r--r--docs/ref/forms/api.txt9
-rw-r--r--docs/releases/5.2.txt3
-rw-r--r--tests/forms_tests/tests/test_forms.py28
6 files changed, 58 insertions, 14 deletions
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
index d0d5c13b3d..4d3cd88660 100644
--- a/django/forms/boundfield.py
+++ b/django/forms/boundfield.py
@@ -289,21 +289,25 @@ class BoundField(RenderableFieldMixin):
attrs["disabled"] = True
if not widget.is_hidden and self.errors:
attrs["aria-invalid"] = "true"
- # If a custom aria-describedby attribute is given (either via the attrs
- # argument or widget.attrs) and help_text is used, the custom
- # aria-described by is preserved so user can set the desired order.
- if (
- not attrs.get("aria-describedby")
- and not widget.attrs.get("aria-describedby")
- and self.field.help_text
- and not self.use_fieldset
- and self.auto_id
- and not self.is_hidden
- ):
- attrs["aria-describedby"] = f"{self.auto_id}_helptext"
+ # Preserve aria-describedby provided by the attrs argument so user
+ # can set the desired order.
+ if not attrs.get("aria-describedby") and not self.use_fieldset:
+ if aria_describedby := self.aria_describedby:
+ attrs["aria-describedby"] = aria_describedby
return attrs
@property
+ def aria_describedby(self):
+ # Preserve aria-describedby set on the widget.
+ if self.field.widget.attrs.get("aria-describedby"):
+ return None
+ aria_describedby = []
+ if self.auto_id and not self.is_hidden:
+ if self.help_text:
+ aria_describedby.append(f"{self.auto_id}_helptext")
+ return " ".join(aria_describedby)
+
+ @property
def widget_type(self):
return re.sub(
r"widget$|input$", "", self.field.widget.__class__.__name__.lower()
diff --git a/django/forms/jinja2/django/forms/field.html b/django/forms/jinja2/django/forms/field.html
index 642d45ddc7..faeed0a45b 100644
--- a/django/forms/jinja2/django/forms/field.html
+++ b/django/forms/jinja2/django/forms/field.html
@@ -1,5 +1,5 @@
{% if field.use_fieldset %}
- <fieldset{% if field.help_text and field.auto_id and "aria-describedby" not in field.field.widget.attrs %} aria-describedby="{{ field.auto_id }}_helptext"{% endif %}>
+ <fieldset{% if field.aria_describedby %} aria-describedby="{{ field.aria_describedby }}"{% endif %}>
{% if field.label %}{{ field.legend_tag() }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag() }}{% endif %}
diff --git a/django/forms/templates/django/forms/field.html b/django/forms/templates/django/forms/field.html
index a4548fe54f..791bc49754 100644
--- a/django/forms/templates/django/forms/field.html
+++ b/django/forms/templates/django/forms/field.html
@@ -1,5 +1,5 @@
{% if field.use_fieldset %}
- <fieldset{% if field.help_text and field.auto_id and "aria-describedby" not in field.field.widget.attrs %} aria-describedby="{{ field.auto_id }}_helptext"{% endif %}>
+ <fieldset{% if field.aria_describedby %} aria-describedby="{{ field.aria_describedby }}"{% endif %}>
{% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag }}{% endif %}
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 4875a1ab72..494f3f244e 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -1161,6 +1161,15 @@ The field-specific output honors the form object's ``auto_id`` setting:
Attributes of ``BoundField``
----------------------------
+.. attribute:: BoundField.aria_describedby
+
+ .. versionadded:: 5.2
+
+ Returns an ``aria-describedby`` reference to associate a field with its
+ help text. Returns ``None`` if ``aria-describedby`` is set in
+ :attr:`Widget.attrs` to preserve the user defined attribute when rendering
+ the form.
+
.. attribute:: BoundField.auto_id
The HTML ID attribute for this ``BoundField``. Returns an empty string
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
index 918bd6a700..aaf47ff8e8 100644
--- a/docs/releases/5.2.txt
+++ b/docs/releases/5.2.txt
@@ -256,6 +256,9 @@ Forms
HTML ``id`` attribute to be added in the error template. See
:attr:`.ErrorList.field_id` for details.
+* An :attr:`~django.forms.BoundField.aria_describedby` property is added to
+ ``BoundField`` to ease use of this HTML attribute in templates.
+
Generic Views
~~~~~~~~~~~~~
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index b49f0a8a69..31ab0a15aa 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -4801,6 +4801,34 @@ Options: <select multiple name="options" aria-invalid="true" required>
with self.assertRaises(KeyError):
f["name"]
+ def test_aria_describedby_property(self):
+ class TestForm(Form):
+ name = CharField(help_text="Some help text")
+
+ form = TestForm({"name": "MyName"})
+ self.assertEqual(form["name"].aria_describedby, "id_name_helptext")
+
+ form = TestForm(auto_id=None)
+ self.assertEqual(form["name"].aria_describedby, "")
+
+ class TestFormHidden(Form):
+ name = CharField(help_text="Some help text", widget=HiddenInput)
+
+ form = TestFormHidden()
+ self.assertEqual(form["name"].aria_describedby, "")
+
+ class TestFormWithAttrs(Form):
+ name = CharField(widget=TextInput(attrs={"aria-describedby": "my-id"}))
+
+ form = TestFormWithAttrs({"name": "MyName"})
+ self.assertIs(form["name"].aria_describedby, None)
+
+ class TestFormWithoutHelpText(Form):
+ name = CharField()
+
+ form = TestFormWithoutHelpText()
+ self.assertEqual(form["name"].aria_describedby, "")
+
@jinja2_tests
class Jinja2FormsTestCase(FormsTestCase):