mirror of https://github.com/django/django.git
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.
This commit is contained in:
parent
649262a406
commit
966ecdd482
|
@ -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
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% else %}
|
||||
{% if field.label %}{{ field.label_tag() }}{% endif %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
|
||||
{% if field.help_text %}<div class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
{{ field }}
|
||||
{% if field.use_fieldset %}</fieldset>{% endif %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if field.label %}{{ field.label_tag() }}{% endif %}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if loop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<br>
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if loop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% if field.label %}{{ field.label_tag() }}{% endif %}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if loop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% else %}
|
||||
{% if field.label %}{{ field.label_tag }}{% endif %}
|
||||
{% endif %}
|
||||
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
|
||||
{% if field.help_text %}<div class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
{{ field }}
|
||||
{% if field.use_fieldset %}</fieldset>{% endif %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if field.label %}{{ field.label_tag }}{% endif %}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if forloop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<br>
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if forloop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% if field.label %}{{ field.label_tag }}{% endif %}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<span class="helptext">{{ field.help_text|safe }}</span>
|
||||
<span class="helptext"{% if field.id_for_label %} id="{{ field.id_for_label}}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
{% if forloop.last %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
|
|
|
@ -275,6 +275,48 @@ fields. We've specified ``auto_id=False`` to simplify the output:
|
|||
<div>Sender:<div class="helptext">A valid email address, please.</div><input type="email" name="sender" required></div>
|
||||
<div>Cc myself:<input type="checkbox" name="cc_myself"></div>
|
||||
|
||||
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)
|
||||
<div>
|
||||
<label for="id_username">Username:</label>
|
||||
<div class="helptext" id="id_username_helptext">e.g., user@example.com</div>
|
||||
<input type="text" name="username" maxlength="255" required aria-describedby="id_username_helptext" id="id_username">
|
||||
</div>
|
||||
|
||||
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"])
|
||||
<input type="text" name="username" aria-describedby="custom-description id_username_helptext" maxlength="255" id="id_username" required>
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
``aria-describedby`` was added to associate ``help_text`` with its input.
|
||||
|
||||
``error_messages``
|
||||
------------------
|
||||
|
||||
|
|
|
@ -61,7 +61,9 @@ For example, the template below:
|
|||
<div>
|
||||
{{ form.name.label_tag }}
|
||||
{% if form.name.help_text %}
|
||||
<div class="helptext">{{ form.name.help_text|safe }}</div>
|
||||
<div class="helptext" id="{{ form.name.id_for_label }}_helptext">
|
||||
{{ form.name.help_text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ form.name.errors }}
|
||||
{{ form.name }}
|
||||
|
@ -69,7 +71,9 @@ For example, the template below:
|
|||
<div class="col">
|
||||
{{ form.email.label_tag }}
|
||||
{% if form.email.help_text %}
|
||||
<div class="helptext">{{ form.email.help_text|safe }}</div>
|
||||
<div class="helptext" id="{{ form.email.id_for_label }}_helptext">
|
||||
{{ form.email.help_text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ form.email.errors }}
|
||||
{{ form.email }}
|
||||
|
@ -77,7 +81,9 @@ For example, the template below:
|
|||
<div class="col">
|
||||
{{ form.password.label_tag }}
|
||||
{% if form.password.help_text %}
|
||||
<div class="helptext">{{ form.password.help_text|safe }}</div>
|
||||
<div class="helptext" id="{{ form.password.id_for_label }}_helptext">
|
||||
{{ form.password.help_text|safe }}
|
||||
</div>
|
||||
{% 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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -723,7 +723,9 @@ loop:
|
|||
{{ field.errors }}
|
||||
{{ field.label_tag }} {{ field }}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
<p class="help" id="{{ field.id_for_label }}_helptext">
|
||||
{{ field.help_text|safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -452,7 +452,7 @@ class TestInline(TestDataMixin, TestCase):
|
|||
self.assertContains(
|
||||
response,
|
||||
'<input id="id_-1-0-name" type="text" class="vTextField" name="-1-0-name" '
|
||||
'maxlength="100">',
|
||||
'maxlength="100" aria-describedby="id_-1-0-name_helptext">',
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
|
|
|
@ -3016,6 +3016,72 @@ Options: <select multiple name="options" required>
|
|||
"</td></tr>",
|
||||
)
|
||||
|
||||
def test_widget_attrs_custom_aria_describedby(self):
|
||||
# aria-describedby provided to the widget overrides the default.
|
||||
|
||||
class UserRegistration(Form):
|
||||
username = CharField(
|
||||
max_length=255,
|
||||
help_text="e.g., user@example.com",
|
||||
widget=TextInput(attrs={"aria-describedby": "custom-description"}),
|
||||
)
|
||||
password = CharField(
|
||||
widget=PasswordInput, help_text="Wählen Sie mit Bedacht."
|
||||
)
|
||||
|
||||
p = UserRegistration()
|
||||
self.assertHTMLEqual(
|
||||
p.as_div(),
|
||||
'<div><label for="id_username">Username:</label>'
|
||||
'<div class="helptext" id="id_username_helptext">e.g., user@example.com'
|
||||
'</div><input type="text" name="username" maxlength="255" required '
|
||||
'aria-describedby="custom-description" id="id_username">'
|
||||
"</div><div>"
|
||||
'<label for="id_password">Password:</label>'
|
||||
'<div class="helptext" id="id_password_helptext">Wählen Sie mit Bedacht.'
|
||||
'</div><input type="password" name="password" required '
|
||||
'aria-describedby="id_password_helptext" id="id_password"></div>',
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
p.as_ul(),
|
||||
'<li><label for="id_username">Username:</label><input type="text" '
|
||||
'name="username" maxlength="255" required '
|
||||
'aria-describedby="custom-description" id="id_username">'
|
||||
'<span class="helptext" id="id_username_helptext">e.g., user@example.com'
|
||||
"</span></li><li>"
|
||||
'<label for="id_password">Password:</label>'
|
||||
'<input type="password" name="password" required '
|
||||
'aria-describedby="id_password_helptext" id="id_password">'
|
||||
'<span class="helptext" id="id_password_helptext">Wählen Sie mit Bedacht.'
|
||||
"</span></li>",
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
p.as_p(),
|
||||
'<p><label for="id_username">Username:</label><input type="text" '
|
||||
'name="username" maxlength="255" required '
|
||||
'aria-describedby="custom-description" id="id_username">'
|
||||
'<span class="helptext" id="id_username_helptext">e.g., user@example.com'
|
||||
"</span></p><p>"
|
||||
'<label for="id_password">Password:</label>'
|
||||
'<input type="password" name="password" required '
|
||||
'aria-describedby="id_password_helptext" id="id_password">'
|
||||
'<span class="helptext" id="id_password_helptext">Wählen Sie mit Bedacht.'
|
||||
"</span></p>",
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
p.as_table(),
|
||||
'<tr><th><label for="id_username">Username:</label></th><td>'
|
||||
'<input type="text" name="username" maxlength="255" required '
|
||||
'aria-describedby="custom-description" id="id_username"><br>'
|
||||
'<span class="helptext" id="id_username_helptext">e.g., user@example.com'
|
||||
"</span></td></tr><tr><th>"
|
||||
'<label for="id_password">Password:</label></th><td>'
|
||||
'<input type="password" name="password" required '
|
||||
'aria-describedby="id_password_helptext" id="id_password"><br>'
|
||||
'<span class="helptext" id="id_password_helptext">Wählen Sie mit Bedacht.'
|
||||
"</span></td></tr>",
|
||||
)
|
||||
|
||||
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):
|
|||
"<form>"
|
||||
'<p><label for="id_username">Username:</label>'
|
||||
'<input id="id_username" type="text" name="username" maxlength="10" '
|
||||
"required></p>"
|
||||
'aria-describedby="id_username_helptext" required></p>'
|
||||
'<p><label for="id_password1">Password1:</label>'
|
||||
'<input type="password" name="password1" id="id_password1" required></p>'
|
||||
'<p><label for="id_password2">Password2:</label>'
|
||||
|
@ -4833,7 +4899,7 @@ class TemplateTests(SimpleTestCase):
|
|||
"<form>"
|
||||
'<p><legend for="id_username">Username:</legend>'
|
||||
'<input id="id_username" type="text" name="username" maxlength="10" '
|
||||
"required></p>"
|
||||
'aria-describedby="id_username_helptext" required></p>'
|
||||
'<p><legend for="id_password1">Password1:</legend>'
|
||||
'<input type="password" name="password1" id="id_password1" required></p>'
|
||||
'<p><legend for="id_password2">Password2:</legend>'
|
||||
|
|
|
@ -958,7 +958,8 @@ class TestFieldOverridesByFormMeta(SimpleTestCase):
|
|||
)
|
||||
self.assertHTMLEqual(
|
||||
str(form["slug"]),
|
||||
'<input id="id_slug" type="text" name="slug" maxlength="20" required>',
|
||||
'<input id="id_slug" type="text" name="slug" maxlength="20" '
|
||||
'aria-describedby="id_slug_helptext" required>',
|
||||
)
|
||||
|
||||
def test_label_overrides(self):
|
||||
|
|
Loading…
Reference in New Issue