Browse Source

Refs #32819 -- Added aria-describedby property to BoundField.

David Smith 3 months ago
parent
commit
1e05431881

+ 16 - 12
django/forms/boundfield.py

@@ -289,20 +289,24 @@ 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(

+ 1 - 1
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 %}

+ 1 - 1
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 %}

+ 9 - 0
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

+ 3 - 0
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
 ~~~~~~~~~~~~~
 

+ 28 - 0
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):