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,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( 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 %} - + {% 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 %} - + {% 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: