diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index 37910431a0..16ce07cc09 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -482,7 +482,7 @@ input, textarea, select, .form-row p, form .button { font-size: 0.8125rem; } .form-row div.help { - padding: 2px 3px; + padding: 2px 3px 2px 0; } textarea { diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index 776a920e25..03914f2d59 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -24,10 +24,13 @@ form .form-row p { .flex-container { display: flex; + flex-flow: column wrap; + align-items: flex-start; } -.form-multiline { - flex-wrap: wrap; +.flex-container.checkbox-row, +.flex-container.form-multiline { + flex-flow: row wrap; } .form-multiline > div { @@ -94,11 +97,15 @@ fieldset .inline-heading, .aligned label { display: block; padding: 4px 10px 0 0; - min-width: 160px; - width: 160px; + margin-bottom: 6px; word-wrap: break-word; } +.aligned label:has(+.help), +.aligned label:has(+.errorlist) { + margin-bottom: 0; +} + .aligned label:not(.vCheckboxLabel):after { content: ''; display: inline-block; @@ -127,11 +134,6 @@ fieldset .inline-heading, width: 350px; } -form .aligned ul { - margin-left: 160px; - padding-left: 10px; -} - form .aligned div.radiolist { display: inline-block; margin: 0; @@ -140,9 +142,12 @@ form .aligned div.radiolist { form .aligned p.help, form .aligned div.help { - margin-top: 0; - margin-left: 160px; - padding-left: 10px; + margin-bottom: 8px; +} + +form .aligned .errors p.help, +form .aligned .errors div.help { + margin-bottom: 2px; } form .aligned p.date div.help.timezonewarning, @@ -159,16 +164,6 @@ form .aligned div.help:last-child { padding-bottom: 0; } -form .aligned input + p.help, -form .aligned textarea + p.help, -form .aligned select + p.help, -form .aligned input + div.help, -form .aligned textarea + div.help, -form .aligned select + div.help { - margin-left: 160px; - padding-left: 10px; -} - form .aligned ul li { list-style: none; } @@ -195,16 +190,14 @@ fieldset .fieldBox { margin-right: 20px; } -/* WIDE FIELDSETS */ - -.wide label { - width: 200px; +.form-multiline .fieldBox { + margin-right: 40px; } -form .wide p.help, -form .wide ul.errorlist, -form .wide div.help { - padding-left: 50px; +/* WIDE FIELDSETS */ + +.wide .fieldBox { + margin-right: 80px; } form div.help ul { diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index 932e824c1c..f439bbd524 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -545,11 +545,7 @@ input[type="submit"], button { } .flex-container { - flex-flow: column; - } - - .flex-container.checkbox-row { - flex-flow: row; + align-items: stretch; } textarea { @@ -592,6 +588,10 @@ input[type="submit"], button { padding: 1px 0 0 5px; } + .aligned .checkbox-row + div.help { + margin-top: 5px; + } + .aligned label + p, .aligned label + div.help, .aligned label + div.readonly { @@ -619,7 +619,6 @@ input[type="submit"], button { form .aligned .form-row div.help { width: 100%; - margin: 5px 0 0; padding: 0; } diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css index b8f60e0a34..77843eb585 100644 --- a/django/contrib/admin/static/admin/css/rtl.css +++ b/django/contrib/admin/static/admin/css/rtl.css @@ -137,8 +137,7 @@ thead th.sorted .text { } form .aligned ul { - margin-right: 163px; - padding-right: 10px; + margin-right: 0; margin-left: 0; padding-left: 0; } @@ -149,13 +148,6 @@ form ul.inline li { padding-left: 7px; } -form .aligned p.help, -form .aligned div.help { - margin-left: 0; - margin-right: 160px; - padding-right: 10px; -} - form div.help ul, form .aligned .checkbox-row + .help, form .aligned p.date div.help.timezonewarning, @@ -165,13 +157,6 @@ form .aligned p.time div.help.timezonewarning { padding-right: 0; } -form .wide p.help, -form .wide ul.errorlist, -form .wide div.help { - padding-left: 0; - padding-right: 50px; -} - .submit-row { text-align: right; } @@ -191,6 +176,16 @@ fieldset .fieldBox { padding: 10px 12px; } +.form-multiline .fieldBox { + margin-left: 40px; + margin-right: 0; +} + +.wide .fieldBox { + margin-left: 80px; + margin-right: 0; +} + /* WIDGETS */ .calendarnav-previous { diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index cc64811a2b..ea8cefcb45 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -574,6 +574,7 @@ ul.timelist, .timelist li { flex-grow: 1; flex-wrap: wrap; margin-bottom: 5px; + width: 100%; } .related-widget-wrapper-link { diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index 8c1830da62..48f2713c66 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -10,15 +10,22 @@ {% endif %} {% for line in fieldset %}
- {% if line.fields|length == 1 %}{{ line.errors }}{% else %}
{% endif %} + {% if not line.fields|length == 1 %}
{% endif %} {% for field in line %}
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
{% if field.is_checkbox %} + {% if line.fields|length == 1 %}{{ line.errors }}{% endif %} {{ field.field }}{{ field.label_tag }} {% else %} {{ field.label_tag }} + {% if field.field.help_text %} +
+
{{ field.field.help_text|safe }}
+
+ {% endif %} + {% if line.fields|length == 1 %}{{ line.errors }}{% endif %} {% if field.is_readonly %}
{{ field.contents }}
{% else %} @@ -26,7 +33,7 @@ {% endif %} {% endif %}
- {% if field.field.help_text %} + {% if field.is_checkbox and field.field.help_text %}
{{ field.field.help_text|safe }}
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 14ad32b706..c6b6193b9e 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -41,6 +41,17 @@ Minor features :ref:`extrabody ` for adding custom code before the closing ```` tag. +* In order to improve accessibility: + + * Form fields in the change form are now shown below their respective labels + instead of next to them. + + * Help text in the change form is now shown after the field label and before + the field input. + + * Validation errors in the change form are now shown after the help text and + before the field input. + :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 8e69edb841..9d615b9d5f 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -384,15 +384,15 @@ class TestInline(TestDataMixin, TestCase): self.assertInHTML( '' - '', + '
Position help_text.
' + '
0
', response.rendered_content, ) self.assertInHTML( '' - '', + '
Position help_text.
' + '
1
', response.rendered_content, ) @@ -414,16 +414,16 @@ class TestInline(TestDataMixin, TestCase): self.assertInHTML( '", response.rendered_content, ) self.assertInHTML( '", response.rendered_content, ) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index fc1bb86d85..43ef7f92e7 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -6206,6 +6206,22 @@ class SeleniumTests(AdminSeleniumTestCase): ) self.take_screenshot("focus-multi-widget") + @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"]) + def test_error_message_position(self): + """Error messages are shown above the field and below the label""" + from selenium.webdriver.common.by import By + + url = reverse("admin:admin_views_article_add") + full_url = self.live_server_url + url + self.admin_login( + username="super", password="secret", login_url=reverse("admin:index") + ) + self.selenium.get(full_url) + # Submit an empty form to trigger validation errors + self.selenium.find_element(By.CSS_SELECTOR, 'input[type="submit"]').click() + self.wait_page_ready() + self.take_screenshot("error-message-position") + def test_cancel_delete_confirmation(self): "Cancelling the deletion of an object takes the user back one page." from selenium.webdriver.common.by import By