Prechádzať zdrojové kódy

Fixed #32820 -- Added aria-invalid="true" to fields with errors.

Co-authored-by: Demetris Stavrou <demestav@gmail.com>
Co-authored-by: David Smith <smithdc@gmail.com>
Gregor Jerše 1 rok pred
rodič
commit
10725a3187

+ 2 - 0
django/forms/boundfield.py

@@ -287,6 +287,8 @@ class BoundField(RenderableFieldMixin):
                 attrs["required"] = True
         if self.field.disabled:
             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.

+ 22 - 5
docs/ref/forms/api.txt

@@ -992,10 +992,20 @@ method you're using:
     ... }
     >>> f = ContactForm(data, auto_id=False)
     >>> print(f)
-    <div>Subject:<ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="subject" maxlength="100" required></div>
-    <div>Message:<textarea name="message" cols="40" rows="10" required>Hi there</textarea></div>
-    <div>Sender:<ul class="errorlist"><li>Enter a valid email address.</li></ul><input type="email" name="sender" value="invalid email address" required></div>
-    <div>Cc myself:<input type="checkbox" name="cc_myself" checked></div>
+    <div>Subject:
+      <ul class="errorlist"><li>This field is required.</li></ul>
+      <input type="text" name="subject" maxlength="100" required aria-invalid="true">
+    </div>
+    <div>Message:
+      <textarea name="message" cols="40" rows="10" required>Hi there</textarea>
+    </div>
+    <div>Sender:
+      <ul class="errorlist"><li>Enter a valid email address.</li></ul>
+      <input type="email" name="sender" value="invalid email address" required aria-invalid="true">
+    </div>
+    <div>Cc myself:
+      <input type="checkbox" name="cc_myself" checked>
+    </div>
 
 .. _ref-forms-error-list-format:
 
@@ -1154,7 +1164,7 @@ Attributes of ``BoundField``
         >>> data = {"subject": "hi", "message": "", "sender": "", "cc_myself": ""}
         >>> f = ContactForm(data, auto_id=False)
         >>> print(f["message"])
-        <input type="text" name="message" required>
+        <input type="text" name="message" required aria-invalid="true">
         >>> f["message"].errors
         ['This field is required.']
         >>> print(f["message"].errors)
@@ -1166,6 +1176,13 @@ Attributes of ``BoundField``
         >>> str(f["subject"].errors)
         ''
 
+    When rendering a field with errors, ``aria-invalid="true"`` will be set on
+    the field's widget to indicate there is an error to screen reader users.
+
+    .. versionchanged:: 5.0
+
+        The ``aria-invalid="true"`` was added when a field has errors.
+
 .. attribute:: BoundField.field
 
     The form :class:`~django.forms.Field` instance from the form class that

+ 11 - 3
docs/ref/forms/fields.txt

@@ -196,9 +196,17 @@ and the HTML output will include any validation errors:
     >>> default_data = {"name": "Your name", "url": "http://"}
     >>> f = CommentForm(default_data, auto_id=False)
     >>> print(f)
-    <div>Name:<input type="text" name="name" value="Your name" required></div>
-    <div>Url:<ul class="errorlist"><li>Enter a valid URL.</li></ul><input type="url" name="url" value="http://" required></div>
-    <div>Comment:<ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="comment" required></div>
+    <div>Name:
+      <input type="text" name="name" value="Your name" required>
+    </div>
+    <div>Url:
+      <ul class="errorlist"><li>Enter a valid URL.</li></ul>
+      <input type="url" name="url" value="http://" required aria-invalid="true">
+    </div>
+    <div>Comment:
+      <ul class="errorlist"><li>This field is required.</li></ul>
+      <input type="text" name="comment" required aria-invalid="true">
+    </div>
 
 This is why ``initial`` values are only displayed for unbound forms. For bound
 forms, the HTML output will use the bound data.

+ 3 - 0
docs/releases/5.0.txt

@@ -310,6 +310,9 @@ Forms
   fields with their help text, the form field now includes the
   ``aria-describedby`` HTML attribute.
 
+* In order to improve accessibility, the invalid form field now includes the
+  ``aria-invalid="true"`` HTML attribute.
+
 Generic Views
 ~~~~~~~~~~~~~
 

+ 111 - 83
tests/forms_tests/tests/test_forms.py

@@ -182,61 +182,70 @@ class FormsTestCase(SimpleTestCase):
             str(p),
             '<div><label for="id_first_name">First name:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="first_name" required id="id_first_name"></div>'
+            '<input type="text" name="first_name" aria-invalid="true" required '
+            'id="id_first_name"></div>'
             '<div><label for="id_last_name">Last name:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="last_name" required id="id_last_name"></div><div>'
+            '<input type="text" name="last_name" aria-invalid="true" required '
+            'id="id_last_name"></div><div>'
             '<label for="id_birthday">Birthday:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="birthday" required id="id_birthday"></div>',
+            '<input type="text" name="birthday" aria-invalid="true" required '
+            'id="id_birthday"></div>',
         )
         self.assertHTMLEqual(
             p.as_table(),
             """<tr><th><label for="id_first_name">First name:</label></th><td>
 <ul class="errorlist"><li>This field is required.</li></ul>
-<input type="text" name="first_name" id="id_first_name" required></td></tr>
-<tr><th><label for="id_last_name">Last name:</label></th>
+<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
+</td></tr><tr><th><label for="id_last_name">Last name:</label></th>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="text" name="last_name" id="id_last_name" required></td></tr>
-<tr><th><label for="id_birthday">Birthday:</label></th>
+<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
+</td></tr><tr><th><label for="id_birthday">Birthday:</label></th>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="text" name="birthday" id="id_birthday" required></td></tr>""",
+<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
+</td></tr>""",
         )
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
 <label for="id_first_name">First name:</label>
-<input type="text" name="first_name" id="id_first_name" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
+<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
+</li><li><ul class="errorlist"><li>This field is required.</li></ul>
 <label for="id_last_name">Last name:</label>
-<input type="text" name="last_name" id="id_last_name" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
+<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
+</li><li><ul class="errorlist"><li>This field is required.</li></ul>
 <label for="id_birthday">Birthday:</label>
-<input type="text" name="birthday" id="id_birthday" required></li>""",
+<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
+</li>""",
         )
         self.assertHTMLEqual(
             p.as_p(),
             """<ul class="errorlist"><li>This field is required.</li></ul>
 <p><label for="id_first_name">First name:</label>
-<input type="text" name="first_name" id="id_first_name" required></p>
-<ul class="errorlist"><li>This field is required.</li></ul>
+<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
+</p><ul class="errorlist"><li>This field is required.</li></ul>
 <p><label for="id_last_name">Last name:</label>
-<input type="text" name="last_name" id="id_last_name" required></p>
-<ul class="errorlist"><li>This field is required.</li></ul>
+<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
+</p><ul class="errorlist"><li>This field is required.</li></ul>
 <p><label for="id_birthday">Birthday:</label>
-<input type="text" name="birthday" id="id_birthday" required></p>""",
+<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
+</p>""",
         )
         self.assertHTMLEqual(
             p.as_div(),
             '<div><label for="id_first_name">First name:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="first_name" required id="id_first_name"></div>'
+            '<input type="text" name="first_name" aria-invalid="true" required '
+            'id="id_first_name"></div>'
             '<div><label for="id_last_name">Last name:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="last_name" required id="id_last_name"></div><div>'
+            '<input type="text" name="last_name" aria-invalid="true" required '
+            'id="id_last_name"></div><div>'
             '<label for="id_birthday">Birthday:</label>'
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="birthday" required id="id_birthday"></div>',
+            '<input type="text" name="birthday" aria-invalid="true" required '
+            'id="id_birthday"></div>',
         )
 
     def test_empty_querydict_args(self):
@@ -582,14 +591,16 @@ class FormsTestCase(SimpleTestCase):
             {"email": "test@example.com", "get_spam": "False"}, auto_id=False
         )
         self.assertHTMLEqual(
-            str(f["get_spam"]), '<input type="checkbox" name="get_spam" required>'
+            str(f["get_spam"]),
+            '<input type="checkbox" name="get_spam" aria-invalid="true" required>',
         )
 
         f = SignupForm(
             {"email": "test@example.com", "get_spam": "false"}, auto_id=False
         )
         self.assertHTMLEqual(
-            str(f["get_spam"]), '<input type="checkbox" name="get_spam" required>'
+            str(f["get_spam"]),
+            '<input type="checkbox" name="get_spam" aria-invalid="true" required>',
         )
 
         # A value of '0' should be interpreted as a True value (#16820)
@@ -1406,13 +1417,13 @@ class FormsTestCase(SimpleTestCase):
             <tr><th>&lt;em&gt;Special&lt;/em&gt; Field:</th><td>
             <ul class="errorlist">
             <li>Something&#x27;s wrong with &#x27;Nothing to escape&#x27;</li></ul>
-            <input type="text" name="special_name" value="Nothing to escape" required>
-            </td></tr>
+            <input type="text" name="special_name" value="Nothing to escape"
+            aria-invalid="true" required></td></tr>
             <tr><th><em>Special</em> Field:</th><td>
             <ul class="errorlist">
             <li>'<b>Nothing to escape</b>' is a safe string</li></ul>
             <input type="text" name="special_safe_name" value="Nothing to escape"
-                required></td></tr>
+            aria-invalid="true" required></td></tr>
             """,
         )
         f = EscapingForm(
@@ -1429,13 +1440,14 @@ class FormsTestCase(SimpleTestCase):
             "Something&#x27;s wrong with &#x27;Should escape &lt; &amp; &gt; and "
             "&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;&#x27;</li></ul>"
             '<input type="text" name="special_name" value="Should escape &lt; &amp; '
-            '&gt; and &lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;" required>'
-            "</td></tr>"
+            '&gt; and &lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;" '
+            'aria-invalid="true" required></td></tr>'
             "<tr><th><em>Special</em> Field:</th><td>"
             '<ul class="errorlist">'
             "<li>'<b><i>Do not escape</i></b>' is a safe string</li></ul>"
             '<input type="text" name="special_safe_name" '
-            'value="&lt;i&gt;Do not escape&lt;/i&gt;" required></td></tr>',
+            'value="&lt;i&gt;Do not escape&lt;/i&gt;" aria-invalid="true" required>'
+            "</td></tr>",
         )
 
     def test_validating_multiple_fields(self):
@@ -1537,11 +1549,12 @@ class FormsTestCase(SimpleTestCase):
             f.as_table(),
             """<tr><th>Username:</th><td>
 <ul class="errorlist"><li>This field is required.</li></ul>
-<input type="text" name="username" maxlength="10" required></td></tr>
+<input type="text" name="username" maxlength="10" aria-invalid="true" required>
+</td></tr>
 <tr><th>Password1:</th><td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="password" name="password1" required></td></tr>
+<input type="password" name="password1" aria-invalid="true" required></td></tr>
 <tr><th>Password2:</th><td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="password" name="password2" required></td></tr>""",
+<input type="password" name="password2" aria-invalid="true" required></td></tr>""",
         )
         self.assertEqual(f.errors["username"], ["This field is required."])
         self.assertEqual(f.errors["password1"], ["This field is required."])
@@ -2412,17 +2425,17 @@ class FormsTestCase(SimpleTestCase):
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-Username: <input type="text" name="username" maxlength="10" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>""",
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true" required></li>""",
         )
         p = UserRegistration({"username": ""}, auto_id=False)
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-Username: <input type="text" name="username" maxlength="10" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>""",
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true" required></li>""",
         )
         p = UserRegistration({"username": "foo"}, auto_id=False)
         self.assertHTMLEqual(
@@ -2431,7 +2444,8 @@ Password: <input type="password" name="password" required></li>""",
             <li>Username: <input type="text" name="username" value="foo" maxlength="10"
                 required></li>
             <li><ul class="errorlist"><li>This field is required.</li></ul>
-            Password: <input type="password" name="password" required></li>
+            Password: <input type="password" name="password" aria-invalid="true"
+            required></li>
             """,
         )
 
@@ -2478,9 +2492,9 @@ Password: <input type="password" name="password" required></li>""",
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-Username: <input type="text" name="username" maxlength="10" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>""",
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true" required></li>""",
         )
         p = UserRegistration(
             {"username": ""}, initial={"username": "django"}, auto_id=False
@@ -2488,9 +2502,9 @@ Password: <input type="password" name="password" required></li>""",
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-Username: <input type="text" name="username" maxlength="10" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>""",
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true" required></li>""",
         )
         p = UserRegistration(
             {"username": "foo"}, initial={"username": "django"}, auto_id=False
@@ -2501,7 +2515,8 @@ Password: <input type="password" name="password" required></li>""",
             <li>Username: <input type="text" name="username" value="foo" maxlength="10"
                 required></li>
             <li><ul class="errorlist"><li>This field is required.</li></ul>
-            Password: <input type="password" name="password" required></li>
+            Password: <input type="password" name="password" aria-invalid="true"
+            required></li>
             """,
         )
 
@@ -2580,11 +2595,11 @@ Password: <input type="password" name="password" required></li>""",
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-Username: <input type="text" name="username" maxlength="10" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Options: <select multiple name="options" required>
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Options: <select multiple name="options" aria-invalid="true" required>
 <option value="f">foo</option>
 <option value="b">bar</option>
 <option value="w">whiz</option>
@@ -2596,11 +2611,11 @@ Options: <select multiple name="options" required>
         self.assertHTMLEqual(
             p.as_ul(),
             """<li><ul class="errorlist"><li>This field is required.</li></ul>
-            Username: <input type="text" name="username" maxlength="10" required></li>
+Username: <input type="text" name="username" maxlength="10" aria-invalid="true"
+required></li><li><ul class="errorlist"><li>This field is required.</li></ul>
+Password: <input type="password" name="password" aria-invalid="true" required></li>
 <li><ul class="errorlist"><li>This field is required.</li></ul>
-Password: <input type="password" name="password" required></li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>
-Options: <select multiple name="options" required>
+Options: <select multiple name="options" aria-invalid="true" required>
 <option value="f">foo</option>
 <option value="b">bar</option>
 <option value="w">whiz</option>
@@ -2617,8 +2632,8 @@ Options: <select multiple name="options" required>
             <li>Username: <input type="text" name="username" value="foo" maxlength="10"
                 required></li>
             <li><ul class="errorlist"><li>This field is required.</li></ul>
-            Password: <input type="password" name="password" required></li>
-            <li>Options: <select multiple name="options" required>
+            Password: <input type="password" name="password" aria-invalid="true"
+            required></li><li>Options: <select multiple name="options" required>
             <option value="f" selected>foo</option>
             <option value="b" selected>bar</option>
             <option value="w">whiz</option>
@@ -2958,8 +2973,8 @@ Options: <select multiple name="options" required>
             'maxlength="10" required>'
             '<span class="helptext">e.g., user@example.com</span></li>'
             '<li><ul class="errorlist"><li>This field is required.</li></ul>'
-            'Password: <input type="password" name="password" required>'
-            '<span class="helptext">Wählen Sie mit Bedacht.</span></li>',
+            'Password: <input type="password" name="password" aria-invalid="true" '
+            'required><span class="helptext">Wählen Sie mit Bedacht.</span></li>',
         )
 
         # help_text is not displayed for hidden fields. It can be used for documentation
@@ -3405,7 +3420,7 @@ Options: <select multiple name="options" required>
             f.as_table(),
             "<tr><th>File1:</th><td>"
             '<ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="file" name="file1" required></td></tr>',
+            '<input type="file" name="file1" aria-invalid="true" required></td></tr>',
         )
 
         f = FileForm(
@@ -3415,7 +3430,7 @@ Options: <select multiple name="options" required>
             f.as_table(),
             "<tr><th>File1:</th><td>"
             '<ul class="errorlist"><li>The submitted file is empty.</li></ul>'
-            '<input type="file" name="file1" required></td></tr>',
+            '<input type="file" name="file1" aria-invalid="true" required></td></tr>',
         )
 
         f = FileForm(
@@ -3426,7 +3441,7 @@ Options: <select multiple name="options" required>
             "<tr><th>File1:</th><td>"
             '<ul class="errorlist"><li>No file was submitted. Check the '
             "encoding type on the form.</li></ul>"
-            '<input type="file" name="file1" required></td></tr>',
+            '<input type="file" name="file1" aria-invalid="true" required></td></tr>',
         )
 
         f = FileForm(
@@ -3591,8 +3606,8 @@ Options: <select multiple name="options" required>
             <li class="required error"><ul class="errorlist">
             <li>This field is required.</li></ul>
             <label class="required" for="id_name">Name:</label>
-            <input type="text" name="name" id="id_name" required></li>
-            <li class="required">
+            <input type="text" name="name" id="id_name" aria-invalid="true" required>
+            </li><li class="required">
             <label class="required" for="id_is_cool">Is cool:</label>
             <select name="is_cool" id="id_is_cool">
             <option value="unknown" selected>Unknown</option>
@@ -3604,7 +3619,8 @@ Options: <select multiple name="options" required>
             <li class="required error"><ul class="errorlist">
             <li>This field is required.</li></ul>
             <label class="required" for="id_age">Age:</label>
-            <input type="number" name="age" id="id_age" required></li>""",
+            <input type="number" name="age" id="id_age" aria-invalid="true" required>
+            </li>""",
         )
 
         self.assertHTMLEqual(
@@ -3613,8 +3629,8 @@ Options: <select multiple name="options" required>
             <ul class="errorlist"><li>This field is required.</li></ul>
             <p class="required error">
             <label class="required" for="id_name">Name:</label>
-            <input type="text" name="name" id="id_name" required></p>
-            <p class="required">
+            <input type="text" name="name" id="id_name" aria-invalid="true" required>
+            </p><p class="required">
             <label class="required" for="id_is_cool">Is cool:</label>
             <select name="is_cool" id="id_is_cool">
             <option value="unknown" selected>Unknown</option>
@@ -3625,8 +3641,8 @@ Options: <select multiple name="options" required>
             <input type="email" name="email" id="id_email" maxlength="320"></p>
             <ul class="errorlist"><li>This field is required.</li></ul>
             <p class="required error"><label class="required" for="id_age">Age:</label>
-            <input type="number" name="age" id="id_age" required></p>
-            """,
+            <input type="number" name="age" id="id_age" aria-invalid="true" required>
+            </p>""",
         )
 
         self.assertHTMLEqual(
@@ -3634,7 +3650,7 @@ Options: <select multiple name="options" required>
             """<tr class="required error">
 <th><label class="required" for="id_name">Name:</label></th>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="text" name="name" id="id_name" required></td></tr>
+<input type="text" name="name" id="id_name" aria-invalid="true" required></td></tr>
 <tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th>
 <td><select name="is_cool" id="id_is_cool">
 <option value="unknown" selected>Unknown</option>
@@ -3645,13 +3661,14 @@ Options: <select multiple name="options" required>
 <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>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
-<input type="number" name="age" id="id_age" required></td></tr>""",
+<input type="number" name="age" id="id_age" aria-invalid="true" required></td></tr>""",
         )
         self.assertHTMLEqual(
             p.as_div(),
             '<div class="required error"><label for="id_name" class="required">Name:'
             '</label><ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="text" name="name" required id="id_name" /></div>'
+            '<input type="text" name="name" required id="id_name" '
+            'aria-invalid="true" /></div>'
             '<div class="required"><label for="id_is_cool" class="required">Is cool:'
             '</label><select name="is_cool" id="id_is_cool">'
             '<option value="unknown" selected>Unknown</option>'
@@ -3660,7 +3677,8 @@ Options: <select multiple name="options" required>
             '<input type="email" name="email" id="id_email" maxlength="320"/></div>'
             '<div class="required error"><label for="id_age" class="required">Age:'
             '</label><ul class="errorlist"><li>This field is required.</li></ul>'
-            '<input type="number" name="age" required id="id_age" /></div>',
+            '<input type="number" name="age" required id="id_age" '
+            'aria-invalid="true" /></div>',
         )
 
     def test_label_has_required_css_class(self):
@@ -4342,7 +4360,8 @@ Options: <select multiple name="options" required>
             "<li>(Hidden field hidden) Foo &amp; &quot;bar&quot;!</li></ul></li>"
             '<li><ul class="errorlist"><li>Foo &amp; &quot;bar&quot;!</li></ul>'
             '<label for="id_visible">Visible:</label> '
-            '<input type="text" name="visible" value="b" id="id_visible" required>'
+            '<input type="text" name="visible" aria-invalid="true" value="b" '
+            'id="id_visible" required>'
             '<input type="hidden" name="hidden" value="a" id="id_hidden"></li>',
         )
 
@@ -4813,10 +4832,12 @@ class TemplateTests(SimpleTestCase):
             "</label></p>"
             '<ul class="errorlist"><li>This field is required.</li></ul><p>'
             "<label>Password: "
-            '<input type="password" name="password1" required></label></p>'
+            '<input type="password" name="password1" aria-invalid="true" required>'
+            "</label></p>"
             '<ul class="errorlist"><li>This field is required.</li></ul>'
             "<p><label>Password (again): "
-            '<input type="password" name="password2" required></label></p>'
+            '<input type="password" name="password2" aria-invalid="true" required>'
+            "</label></p>"
             '<input type="submit" required>'
             "</form>",
         )
@@ -4851,7 +4872,10 @@ class TemplateTests(SimpleTestCase):
         # Form gives each field an "id" attribute.
         t = Template(
             "<form>"
-            "<p>{{ form.username.label_tag }} {{ form.username }}</p>"
+            "<p>{{ form.username.label_tag }} {{ form.username }}"
+            '<span {% if form.username.id_for_label %}id="'
+            '{{ form.username.id_for_label }}_helptext"{% endif %}>'
+            "{{ form.username.help_text}}</span></p>"
             "<p>{{ form.password1.label_tag }} {{ form.password1 }}</p>"
             "<p>{{ form.password2.label_tag }} {{ form.password2 }}</p>"
             '<input type="submit" required>'
@@ -4861,7 +4885,8 @@ class TemplateTests(SimpleTestCase):
             t.render(Context({"form": f})),
             "<form>"
             "<p>Username: "
-            '<input type="text" name="username" maxlength="10" required></p>'
+            '<input type="text" name="username" maxlength="10" required>'
+            "<span>Good luck picking a username that doesn't already exist.</span></p>"
             '<p>Password1: <input type="password" name="password1" required></p>'
             '<p>Password2: <input type="password" name="password2" required></p>'
             '<input type="submit" required>'
@@ -4873,7 +4898,9 @@ class TemplateTests(SimpleTestCase):
             "<form>"
             '<p><label for="id_username">Username:</label>'
             '<input id="id_username" type="text" name="username" maxlength="10" '
-            'aria-describedby="id_username_helptext" required></p>'
+            'aria-describedby="id_username_helptext" required>'
+            '<span id="id_username_helptext">'
+            "Good luck picking a username that doesn't already exist.</span></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>'
@@ -5066,7 +5093,7 @@ class TemplateTests(SimpleTestCase):
             "<li>Please make sure your passwords match.</li></ul>"
             '<div>Username:<ul class="errorlist">'
             "<li>Ensure this value has at most 10 characters (it has 23).</li></ul>"
-            '<input type="text" name="username" '
+            '<input type="text" name="username" aria-invalid="true" '
             'value="this-is-a-long-username" maxlength="10" required></div>'
             "<div>Password1:"
             '<input type="password" name="password1" required></div>'
@@ -5174,10 +5201,11 @@ class OverrideTests(SimpleTestCase):
             '<div class="errorlist">'
             '<div class="error">Enter a valid email address.</div></div>'
             "<p>Email: "
-            '<input type="email" name="email" value="invalid" maxlength="320" required>'
-            '</p><div class="errorlist">'
+            '<input type="email" name="email" value="invalid" maxlength="320" '
+            'aria-invalid="true" required></p><div class="errorlist">'
             '<div class="error">This field is required.</div></div>'
-            '<p>Comment: <input type="text" name="comment" required></p>',
+            '<p>Comment: <input type="text" name="comment" aria-invalid="true" '
+            "required></p>",
         )
 
     def test_cyclic_context_boundfield_render(self):

+ 6 - 3
tests/forms_tests/tests/test_i18n.py

@@ -103,13 +103,16 @@ class FormsI18nTests(SimpleTestCase):
                 "<p><label>\xc5\xf8\xdf:</label>"
                 ' <div id="id_somechoice">\n<div><label for="id_somechoice_0">'
                 '<input type="radio" id="id_somechoice_0" value="\xc5" '
-                'name="somechoice" required> En tied\xe4</label></div>\n'
+                'name="somechoice" aria-invalid="true" required>'
+                "En tied\xe4</label></div>\n"
                 '<div><label for="id_somechoice_1">'
                 '<input type="radio" id="id_somechoice_1" value="\xf8" '
-                'name="somechoice" required> Mies</label></div>\n<div>'
+                'name="somechoice" aria-invalid="true" required>'
+                "Mies</label></div>\n<div>"
                 '<label for="id_somechoice_2">'
                 '<input type="radio" id="id_somechoice_2" value="\xdf" '
-                'name="somechoice" required> Nainen</label></div>\n</div></p>',
+                'name="somechoice" aria-invalid="true" required>'
+                "Nainen</label></div>\n</div></p>",
             )
 
     def test_select_translated_text(self):

+ 1 - 1
tests/gis_tests/geoadmin/tests.py

@@ -30,7 +30,7 @@ class GeoAdminTest(SimpleTestCase):
             ' rows="10" name="point" hidden></textarea>',
             output,
         )
-        self.assertEqual(len(cm.records), 1)
+        self.assertEqual(len(cm.records), 2)
         self.assertEqual(
             cm.records[0].getMessage(),
             "Error creating geometry from value 'INVALID()' (String input "