1
0
mirror of https://github.com/django/django.git synced 2024-12-23 01:25:58 +00:00

Fixed #32819 -- Added aria-describedby to fields with errors.

This commit is contained in:
David Smith 2023-11-19 19:26:12 +00:00 committed by Sarah Boyce
parent 3ee4c6a27a
commit 6af75bb7c7
4 changed files with 159 additions and 42 deletions

View File

@ -305,6 +305,8 @@ class BoundField(RenderableFieldMixin):
if self.auto_id and not self.is_hidden: if self.auto_id and not self.is_hidden:
if self.help_text: if self.help_text:
aria_describedby.append(f"{self.auto_id}_helptext") aria_describedby.append(f"{self.auto_id}_helptext")
if self.errors:
aria_describedby.append(f"{self.auto_id}_error")
return " ".join(aria_describedby) return " ".join(aria_describedby)
@property @property

View File

@ -986,14 +986,16 @@ the ``Form``, then the latter ``field_order`` will have precedence.
You may rearrange the fields any time using ``order_fields()`` with a list of You may rearrange the fields any time using ``order_fields()`` with a list of
field names as in :attr:`~django.forms.Form.field_order`. field names as in :attr:`~django.forms.Form.field_order`.
.. _form-error-display:
How errors are displayed How errors are displayed
------------------------ ------------------------
If you render a bound ``Form`` object, the act of rendering will automatically If you render a bound ``Form`` object, the act of rendering will automatically
run the form's validation if it hasn't already happened, and the HTML output run the form's validation if it hasn't already happened, and the HTML output
will include the validation errors as a ``<ul class="errorlist">`` near the will include the validation errors as a ``<ul class="errorlist">``.
field. The particular positioning of the error messages depends on the output
method you're using: The following:
.. code-block:: pycon .. code-block:: pycon
@ -1003,23 +1005,44 @@ method you're using:
... "sender": "invalid email address", ... "sender": "invalid email address",
... "cc_myself": True, ... "cc_myself": True,
... } ... }
>>> f = ContactForm(data, auto_id=False) >>> f = ContactForm(data)
>>> print(f)
<div>Subject: … gives HTML like:
<ul class="errorlist"><li>This field is required.</li></ul>
<input type="text" name="subject" maxlength="100" required aria-invalid="true"> .. code-block:: html
<div>
<label for="id_subject">Subject:</label>
<ul class="errorlist" id="id_subject_error"><li>This field is required.</li></ul>
<input type="text" name="subject" maxlength="100" required aria-invalid="true" aria-describedby="id_subject_error" id="id_subject">
</div> </div>
<div>Message: <div>
<textarea name="message" cols="40" rows="10" required>Hi there</textarea> <label for="id_message">Message:</label>
<textarea name="message" cols="40" rows="10" required id="id_message">Hi there</textarea>
</div> </div>
<div>Sender: <div>
<ul class="errorlist"><li>Enter a valid email address.</li></ul> <label for="id_sender">Sender:</label>
<input type="email" name="sender" value="invalid email address" required aria-invalid="true"> <ul class="errorlist" id="id_sender_error"><li>Enter a valid email address.</li></ul>
<input type="email" name="sender" value="invalid email address" maxlength="320" required aria-invalid="true" aria-describedby="id_sender_error" id="id_sender">
</div> </div>
<div>Cc myself: <div>
<input type="checkbox" name="cc_myself" checked> <label for="id_cc_myself">Cc myself:</label>
<input type="checkbox" name="cc_myself" id="id_cc_myself" checked>
</div> </div>
Django's default form templates will associate validation errors with their
input by using the ``aria-describedby`` HTML attribute when the field has an
``auto_id`` and a custom ``aria-describedby`` is not provided. If a custom
``aria-describedby`` is set when defining the widget this will override the
default value.
If the widget is rendered in a ``<fieldset>`` then ``aria-describedby`` is
added to this element, otherwise it is added to the widget's ``<input>``.
.. versionchanged:: 5.2
``aria-describedby`` was added to associate errors with its input.
.. _ref-forms-error-list-format: .. _ref-forms-error-list-format:
Customizing the error list format Customizing the error list format
@ -1166,7 +1189,7 @@ Attributes of ``BoundField``
.. versionadded:: 5.2 .. versionadded:: 5.2
Returns an ``aria-describedby`` reference to associate a field with its Returns an ``aria-describedby`` reference to associate a field with its
help text. Returns ``None`` if ``aria-describedby`` is set in help text and errors. Returns ``None`` if ``aria-describedby`` is set in
:attr:`Widget.attrs` to preserve the user defined attribute when rendering :attr:`Widget.attrs` to preserve the user defined attribute when rendering
the form. the form.

View File

@ -259,6 +259,10 @@ Forms
* An :attr:`~django.forms.BoundField.aria_describedby` property is added to * An :attr:`~django.forms.BoundField.aria_describedby` property is added to
``BoundField`` to ease use of this HTML attribute in templates. ``BoundField`` to ease use of this HTML attribute in templates.
* To improve accessibility for screen reader users ``aria-describedby`` is used
to associated form fields with their error messages. See
:ref:`how form errors are displayed <form-error-display>` for details.
Generic Views Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -183,27 +183,30 @@ class FormsTestCase(SimpleTestCase):
'<div><label for="id_first_name">First name:</label>' '<div><label for="id_first_name">First name:</label>'
'<ul class="errorlist" id="id_first_name_error"><li>This field is required.' '<ul class="errorlist" id="id_first_name_error"><li>This field is required.'
'</li></ul><input type="text" name="first_name" aria-invalid="true" ' '</li></ul><input type="text" name="first_name" aria-invalid="true" '
'required id="id_first_name"></div>' 'required id="id_first_name" aria-describedby="id_first_name_error"></div>'
'<div><label for="id_last_name">Last name:</label>' '<div><label for="id_last_name">Last name:</label>'
'<ul class="errorlist" id="id_last_name_error"><li>This field is required.' '<ul class="errorlist" id="id_last_name_error"><li>This field is required.'
'</li></ul><input type="text" name="last_name" aria-invalid="true" ' '</li></ul><input type="text" name="last_name" aria-invalid="true" '
'required id="id_last_name"></div><div>' 'required id="id_last_name" aria-describedby="id_last_name_error"></div>'
'<label for="id_birthday">Birthday:</label>' '<div><label for="id_birthday">Birthday:</label>'
'<ul class="errorlist" id="id_birthday_error"><li>This field is required.' '<ul class="errorlist" id="id_birthday_error"><li>This field is required.'
'</li></ul><input type="text" name="birthday" aria-invalid="true" required ' '</li></ul><input type="text" name="birthday" aria-invalid="true" required '
'id="id_birthday"></div>', 'id="id_birthday" aria-describedby="id_birthday_error"></div>',
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_table(), p.as_table(),
"""<tr><th><label for="id_first_name">First name:</label></th><td> """<tr><th><label for="id_first_name">First name:</label></th><td>
<ul class="errorlist" id="id_first_name_error"><li>This field is required.</li></ul> <ul class="errorlist" id="id_first_name_error"><li>This field is required.</li></ul>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required
aria-describedby="id_first_name_error">
</td></tr><tr><th><label for="id_last_name">Last name:</label></th> </td></tr><tr><th><label for="id_last_name">Last name:</label></th>
<td><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required
aria-describedby="id_last_name_error">
</td></tr><tr><th><label for="id_birthday">Birthday:</label></th> </td></tr><tr><th><label for="id_birthday">Birthday:</label></th>
<td><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required
aria-describedby="id_birthday_error">
</td></tr>""", </td></tr>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
@ -211,13 +214,16 @@ class FormsTestCase(SimpleTestCase):
"""<li><ul class="errorlist" id="id_first_name_error"> """<li><ul class="errorlist" id="id_first_name_error">
<li>This field is required.</li></ul> <li>This field is required.</li></ul>
<label for="id_first_name">First name:</label> <label for="id_first_name">First name:</label>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required
aria-describedby="id_first_name_error">
</li><li><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li> </li><li><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li>
</ul><label for="id_last_name">Last name:</label> </ul><label for="id_last_name">Last name:</label>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required
aria-describedby="id_last_name_error">
</li><li><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li> </li><li><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li>
</ul><label for="id_birthday">Birthday:</label> </ul><label for="id_birthday">Birthday:</label>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required
aria-describedby="id_birthday_error">
</li>""", </li>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
@ -225,13 +231,16 @@ class FormsTestCase(SimpleTestCase):
"""<ul class="errorlist" id="id_first_name_error"><li> """<ul class="errorlist" id="id_first_name_error"><li>
This field is required.</li></ul> This field is required.</li></ul>
<p><label for="id_first_name">First name:</label> <p><label for="id_first_name">First name:</label>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required
aria-describedby="id_first_name_error">
</p><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul> </p><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul>
<p><label for="id_last_name">Last name:</label> <p><label for="id_last_name">Last name:</label>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required
aria-describedby="id_last_name_error">
</p><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul> </p><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul>
<p><label for="id_birthday">Birthday:</label> <p><label for="id_birthday">Birthday:</label>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required
aria-describedby="id_birthday_error">
</p>""", </p>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
@ -239,15 +248,15 @@ This field is required.</li></ul>
'<div><label for="id_first_name">First name:</label>' '<div><label for="id_first_name">First name:</label>'
'<ul class="errorlist" id="id_first_name_error"><li>This field is required.' '<ul class="errorlist" id="id_first_name_error"><li>This field is required.'
'</li></ul><input type="text" name="first_name" aria-invalid="true" ' '</li></ul><input type="text" name="first_name" aria-invalid="true" '
'required id="id_first_name"></div>' 'required id="id_first_name" aria-describedby="id_first_name_error"></div>'
'<div><label for="id_last_name">Last name:</label>' '<div><label for="id_last_name">Last name:</label>'
'<ul class="errorlist" id="id_last_name_error"><li>This field is required.' '<ul class="errorlist" id="id_last_name_error"><li>This field is required.'
'</li></ul><input type="text" name="last_name" aria-invalid="true" ' '</li></ul><input type="text" name="last_name" aria-invalid="true" '
'required id="id_last_name"></div><div>' 'required id="id_last_name" aria-describedby="id_last_name_error"></div>'
'<label for="id_birthday">Birthday:</label>' '<div><label for="id_birthday">Birthday:</label>'
'<ul class="errorlist" id="id_birthday_error"><li>This field is required.' '<ul class="errorlist" id="id_birthday_error"><li>This field is required.'
'</li></ul><input type="text" name="birthday" aria-invalid="true" required ' '</li></ul><input type="text" name="birthday" aria-invalid="true" required '
'id="id_birthday"></div>', 'id="id_birthday" aria-describedby="id_birthday_error"></div>',
) )
def test_empty_querydict_args(self): def test_empty_querydict_args(self):
@ -3126,6 +3135,52 @@ Options: <select multiple name="options" aria-invalid="true" required>
'required aria-describedby="id_username_helptext"></div>', 'required aria-describedby="id_username_helptext"></div>',
) )
def test_select_aria_describedby(self):
class TestForm(Form):
colour = MultipleChoiceField(
choices=[("red", "Red"), ("green", "Green")],
help_text="Select Colour",
)
f = TestForm({"colour": "Blue"})
self.assertHTMLEqual(
str(f),
'<div><label for="id_colour">Colour:</label><div class="helptext" '
'id="id_colour_helptext">Select Colour</div>'
'<ul class="errorlist" id="id_colour_error"><li>Enter a list of values.'
'</li></ul><select name="colour" required aria-invalid="true" '
'aria-describedby="id_colour_helptext id_colour_error" id="id_colour" '
'multiple><option value="red">Red</option>'
'<option value="green">Green</option></select></div>',
)
def test_textarea_aria_describedby(self):
class TestForm(Form):
colour = CharField(widget=Textarea, max_length=5, help_text="Enter Colour")
f = TestForm({"colour": "Purple"})
self.assertHTMLEqual(
str(f),
'<div><label for="id_colour">Colour:</label>'
'<div class="helptext" id="id_colour_helptext">Enter Colour</div>'
'<ul class="errorlist" id="id_colour_error">'
"<li>Ensure this value has at most 5 characters (it has 6).</li></ul>"
'<textarea name="colour" cols="40" rows="10" maxlength="5" required '
'aria-invalid="true" aria-describedby="id_colour_helptext id_colour_error" '
'id="id_colour">Purple</textarea></div>',
)
def test_aria_describedby_called_multiple_times(self):
class TestForm(Form):
colour = CharField(widget=Textarea, help_text="Enter Colour")
f = TestForm({"colour": "Purple"})
self.assertEqual(f["colour"].aria_describedby, "id_colour_helptext")
f.add_error("colour", "An error about Purple.")
self.assertEqual(
f["colour"].aria_describedby, "id_colour_helptext id_colour_error"
)
def test_fieldset_aria_describedby(self): def test_fieldset_aria_describedby(self):
class FieldsetForm(Form): class FieldsetForm(Form):
checkbox = MultipleChoiceField( checkbox = MultipleChoiceField(
@ -3170,6 +3225,34 @@ Options: <select multiple name="options" aria-invalid="true" required>
'<input type="text" name="datetime_1" required id="id_datetime_1" />' '<input type="text" name="datetime_1" required id="id_datetime_1" />'
"</fieldset></div>", "</fieldset></div>",
) )
f = FieldsetForm({})
self.assertHTMLEqual(
'<div><fieldset aria-describedby="id_checkbox_helptext '
'id_checkbox_error"> <legend>Checkbox:</legend> <div class="helptext" '
'id="id_checkbox_helptext">Checkbox help text</div> <ul class="errorlist" '
'id="id_checkbox_error"> <li>This field is required.</li> </ul> '
'<div id="id_checkbox"> <div> <label for="id_checkbox_0"><input '
'type="checkbox" name="checkbox" value="a" aria-invalid="true" '
'id="id_checkbox_0" /> A</label> </div> <div> <label for="id_checkbox_1">'
'<input type="checkbox" name="checkbox" value="b" aria-invalid="true" '
'id="id_checkbox_1" /> B</label> </div> </div> </fieldset> </div> <div> '
'<fieldset aria-describedby="id_radio_helptext id_radio_error"> '
'<legend>Radio:</legend> <div class="helptext" id="id_radio_helptext">'
'Radio help text</div> <ul class="errorlist" id="id_radio_error"><li>'
'This field is required.</li> </ul> <div id="id_radio"><div><label '
'for="id_radio_0"><input type="radio" name="radio" value="a" required '
'aria-invalid="true" id="id_radio_0" />A</label></div><div><label '
'for="id_radio_1"><input type="radio" name="radio" value="b" required '
'aria-invalid="true" id="id_radio_1" />B</label></div></div></fieldset>'
'</div><div><fieldset aria-describedby="id_datetime_helptext '
'id_datetime_error"><legend>Datetime:</legend><div class="helptext" '
'id="id_datetime_helptext">Enter Date and Time</div><ul class="errorlist" '
'id="id_datetime_error"><li>This field is required.</li></ul><input '
'type="text" name="datetime_0" required aria-invalid="true" '
'id="id_datetime_0" /><input type="text" name="datetime_1" required '
'aria-invalid="true" id="id_datetime_1" /></fieldset></div>',
str(f),
)
f = FieldsetForm(auto_id=False) f = FieldsetForm(auto_id=False)
# aria-describedby is not included. # aria-describedby is not included.
self.assertIn("<fieldset>", str(f)) self.assertIn("<fieldset>", str(f))
@ -3712,7 +3795,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
<li class="required error"><ul class="errorlist" id="id_name_error"> <li class="required error"><ul class="errorlist" id="id_name_error">
<li>This field is required.</li></ul> <li>This field is required.</li></ul>
<label class="required" for="id_name">Name:</label> <label class="required" for="id_name">Name:</label>
<input type="text" name="name" id="id_name" aria-invalid="true" required> <input type="text" name="name" id="id_name" aria-invalid="true" required
aria-describedby="id_name_error">
</li><li class="required"> </li><li class="required">
<label class="required" for="id_is_cool">Is cool:</label> <label class="required" for="id_is_cool">Is cool:</label>
<select name="is_cool" id="id_is_cool"> <select name="is_cool" id="id_is_cool">
@ -3725,7 +3809,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
<li class="required error"><ul class="errorlist" id="id_age_error"> <li class="required error"><ul class="errorlist" id="id_age_error">
<li>This field is required.</li></ul> <li>This field is required.</li></ul>
<label class="required" for="id_age">Age:</label> <label class="required" for="id_age">Age:</label>
<input type="number" name="age" id="id_age" aria-invalid="true" required> <input type="number" name="age" id="id_age" aria-invalid="true" required
aria-describedby="id_age_error">
</li>""", </li>""",
) )
@ -3735,7 +3820,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
<ul class="errorlist" id="id_name_error"><li>This field is required.</li> <ul class="errorlist" id="id_name_error"><li>This field is required.</li>
</ul><p class="required error"> </ul><p class="required error">
<label class="required" for="id_name">Name:</label> <label class="required" for="id_name">Name:</label>
<input type="text" name="name" id="id_name" aria-invalid="true" required> <input type="text" name="name" id="id_name" aria-invalid="true" required
aria-describedby="id_name_error">
</p><p class="required"> </p><p class="required">
<label class="required" for="id_is_cool">Is cool:</label> <label class="required" for="id_is_cool">Is cool:</label>
<select name="is_cool" id="id_is_cool"> <select name="is_cool" id="id_is_cool">
@ -3748,7 +3834,7 @@ Options: <select multiple name="options" aria-invalid="true" required>
<ul class="errorlist" id="id_age_error"><li>This field is required.</li> <ul class="errorlist" id="id_age_error"><li>This field is required.</li>
</ul><p class="required error"><label class="required" for="id_age"> </ul><p class="required error"><label class="required" for="id_age">
Age:</label><input type="number" name="age" id="id_age" aria-invalid="true" Age:</label><input type="number" name="age" id="id_age" aria-invalid="true"
required></p>""", required aria-describedby="id_age_error"></p>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
@ -3756,7 +3842,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
"""<tr class="required error"> """<tr class="required error">
<th><label class="required" for="id_name">Name:</label></th> <th><label class="required" for="id_name">Name:</label></th>
<td><ul class="errorlist" id="id_name_error"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_name_error"><li>This field is required.</li></ul>
<input type="text" name="name" id="id_name" aria-invalid="true" required></td></tr> <input type="text" name="name" id="id_name" aria-invalid="true" required
aria-describedby="id_name_error"></td></tr>
<tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th> <tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th>
<td><select name="is_cool" id="id_is_cool"> <td><select name="is_cool" id="id_is_cool">
<option value="unknown" selected>Unknown</option> <option value="unknown" selected>Unknown</option>
@ -3767,14 +3854,15 @@ Options: <select multiple name="options" aria-invalid="true" required>
<input type="email" name="email" id="id_email" maxlength="320"></td></tr> <input type="email" name="email" id="id_email" maxlength="320"></td></tr>
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th> <tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
<td><ul class="errorlist" id="id_age_error"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_age_error"><li>This field is required.</li></ul>
<input type="number" name="age" id="id_age" aria-invalid="true" required></td></tr>""", <input type="number" name="age" id="id_age" aria-invalid="true" required
aria-describedby="id_age_error"></td></tr>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_div(), p.as_div(),
'<div class="required error"><label for="id_name" class="required">Name:' '<div class="required error"><label for="id_name" class="required">Name:'
'</label><ul class="errorlist" id="id_name_error"><li>This field is ' '</label><ul class="errorlist" id="id_name_error"><li>This field is '
'required.</li></ul><input type="text" name="name" required id="id_name" ' 'required.</li></ul><input type="text" name="name" required id="id_name" '
'aria-invalid="true" /></div>' 'aria-invalid="true" aria-describedby="id_name_error" /></div>'
'<div class="required"><label for="id_is_cool" class="required">Is cool:' '<div class="required"><label for="id_is_cool" class="required">Is cool:'
'</label><select name="is_cool" id="id_is_cool">' '</label><select name="is_cool" id="id_is_cool">'
'<option value="unknown" selected>Unknown</option>' '<option value="unknown" selected>Unknown</option>'
@ -3784,7 +3872,7 @@ Options: <select multiple name="options" aria-invalid="true" required>
'<div class="required error"><label for="id_age" class="required">Age:' '<div class="required error"><label for="id_age" class="required">Age:'
'</label><ul class="errorlist" id="id_age_error"><li>This field is ' '</label><ul class="errorlist" id="id_age_error"><li>This field is '
'required.</li></ul><input type="number" name="age" required id="id_age" ' 'required.</li></ul><input type="number" name="age" required id="id_age" '
'aria-invalid="true" /></div>', 'aria-invalid="true" aria-describedby="id_age_error" /></div>',
) )
def test_label_has_required_css_class(self): def test_label_has_required_css_class(self):
@ -4470,7 +4558,7 @@ Options: <select multiple name="options" aria-invalid="true" required>
"&quot;bar&quot;!</li></ul>" "&quot;bar&quot;!</li></ul>"
'<label for="id_visible">Visible:</label> ' '<label for="id_visible">Visible:</label> '
'<input type="text" name="visible" aria-invalid="true" value="b" ' '<input type="text" name="visible" aria-invalid="true" value="b" '
'id="id_visible" required>' 'id="id_visible" required aria-describedby="id_visible_error">'
'<input type="hidden" name="hidden" value="a" id="id_hidden"></li>', '<input type="hidden" name="hidden" value="a" id="id_hidden"></li>',
) )