1
0
mirror of https://github.com/django/django.git synced 2025-07-05 10:19:20 +00:00

Fixed #34643 -- Moved admin form labels above fields.

This allows screen magnifier users to see the label and field
together, improving accessibility.
This commit is contained in:
Hrushikesh 2024-09-26 17:14:33 +02:00 committed by Sarah Boyce
parent 5ed72087c4
commit 6fbdfe14f6
9 changed files with 83 additions and 61 deletions

View File

@ -482,7 +482,7 @@ input, textarea, select, .form-row p, form .button {
font-size: 0.8125rem; font-size: 0.8125rem;
} }
.form-row div.help { .form-row div.help {
padding: 2px 3px; padding: 2px 3px 2px 0;
} }
textarea { textarea {

View File

@ -24,10 +24,13 @@ form .form-row p {
.flex-container { .flex-container {
display: flex; display: flex;
flex-flow: column wrap;
align-items: flex-start;
} }
.form-multiline { .flex-container.checkbox-row,
flex-wrap: wrap; .flex-container.form-multiline {
flex-flow: row wrap;
} }
.form-multiline > div { .form-multiline > div {
@ -94,11 +97,15 @@ fieldset .inline-heading,
.aligned label { .aligned label {
display: block; display: block;
padding: 4px 10px 0 0; padding: 4px 10px 0 0;
min-width: 160px; margin-bottom: 6px;
width: 160px;
word-wrap: break-word; word-wrap: break-word;
} }
.aligned label:has(+.help),
.aligned label:has(+.errorlist) {
margin-bottom: 0;
}
.aligned label:not(.vCheckboxLabel):after { .aligned label:not(.vCheckboxLabel):after {
content: ''; content: '';
display: inline-block; display: inline-block;
@ -127,11 +134,6 @@ fieldset .inline-heading,
width: 350px; width: 350px;
} }
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned div.radiolist { form .aligned div.radiolist {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
@ -140,9 +142,12 @@ form .aligned div.radiolist {
form .aligned p.help, form .aligned p.help,
form .aligned div.help { form .aligned div.help {
margin-top: 0; margin-bottom: 8px;
margin-left: 160px; }
padding-left: 10px;
form .aligned .errors p.help,
form .aligned .errors div.help {
margin-bottom: 2px;
} }
form .aligned p.date div.help.timezonewarning, form .aligned p.date div.help.timezonewarning,
@ -159,16 +164,6 @@ form .aligned div.help:last-child {
padding-bottom: 0; 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 { form .aligned ul li {
list-style: none; list-style: none;
} }
@ -195,16 +190,14 @@ fieldset .fieldBox {
margin-right: 20px; margin-right: 20px;
} }
/* WIDE FIELDSETS */ .form-multiline .fieldBox {
margin-right: 40px;
.wide label {
width: 200px;
} }
form .wide p.help, /* WIDE FIELDSETS */
form .wide ul.errorlist,
form .wide div.help { .wide .fieldBox {
padding-left: 50px; margin-right: 80px;
} }
form div.help ul { form div.help ul {

View File

@ -545,11 +545,7 @@ input[type="submit"], button {
} }
.flex-container { .flex-container {
flex-flow: column; align-items: stretch;
}
.flex-container.checkbox-row {
flex-flow: row;
} }
textarea { textarea {
@ -592,6 +588,10 @@ input[type="submit"], button {
padding: 1px 0 0 5px; padding: 1px 0 0 5px;
} }
.aligned .checkbox-row + div.help {
margin-top: 5px;
}
.aligned label + p, .aligned label + p,
.aligned label + div.help, .aligned label + div.help,
.aligned label + div.readonly { .aligned label + div.readonly {
@ -619,7 +619,6 @@ input[type="submit"], button {
form .aligned .form-row div.help { form .aligned .form-row div.help {
width: 100%; width: 100%;
margin: 5px 0 0;
padding: 0; padding: 0;
} }

View File

@ -137,8 +137,7 @@ thead th.sorted .text {
} }
form .aligned ul { form .aligned ul {
margin-right: 163px; margin-right: 0;
padding-right: 10px;
margin-left: 0; margin-left: 0;
padding-left: 0; padding-left: 0;
} }
@ -149,13 +148,6 @@ form ul.inline li {
padding-left: 7px; 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 div.help ul,
form .aligned .checkbox-row + .help, form .aligned .checkbox-row + .help,
form .aligned p.date div.help.timezonewarning, form .aligned p.date div.help.timezonewarning,
@ -165,13 +157,6 @@ form .aligned p.time div.help.timezonewarning {
padding-right: 0; padding-right: 0;
} }
form .wide p.help,
form .wide ul.errorlist,
form .wide div.help {
padding-left: 0;
padding-right: 50px;
}
.submit-row { .submit-row {
text-align: right; text-align: right;
} }
@ -191,6 +176,16 @@ fieldset .fieldBox {
padding: 10px 12px; padding: 10px 12px;
} }
.form-multiline .fieldBox {
margin-left: 40px;
margin-right: 0;
}
.wide .fieldBox {
margin-left: 80px;
margin-right: 0;
}
/* WIDGETS */ /* WIDGETS */
.calendarnav-previous { .calendarnav-previous {

View File

@ -574,6 +574,7 @@ ul.timelist, .timelist li {
flex-grow: 1; flex-grow: 1;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 5px; margin-bottom: 5px;
width: 100%;
} }
.related-widget-wrapper-link { .related-widget-wrapper-link {

View File

@ -10,15 +10,22 @@
{% endif %} {% endif %}
{% for line in fieldset %} {% for line in fieldset %}
<div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}"> <div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
{% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %} {% if not line.fields|length == 1 %}<div class="flex-container form-multiline">{% endif %}
{% for field in line %} {% for field in line %}
<div> <div>
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}"> <div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% endif %}{% if field.is_checkbox %} checkbox-row{% endif %}">
{% if field.is_checkbox %} {% if field.is_checkbox %}
{% if line.fields|length == 1 %}{{ line.errors }}{% endif %}
{{ field.field }}{{ field.label_tag }} {{ field.field }}{{ field.label_tag }}
{% else %} {% else %}
{{ field.label_tag }} {{ field.label_tag }}
{% if field.field.help_text %}
<div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endif %}
{% if line.fields|length == 1 %}{{ line.errors }}{% endif %}
{% if field.is_readonly %} {% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div> <div class="readonly">{{ field.contents }}</div>
{% else %} {% else %}
@ -26,7 +33,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if field.field.help_text %} {% if field.is_checkbox and field.field.help_text %}
<div class="help{% if field.field.is_hidden %} hidden{% endif %}"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}> <div class="help{% if field.field.is_hidden %} hidden{% endif %}"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div> <div>{{ field.field.help_text|safe }}</div>
</div> </div>

View File

@ -41,6 +41,17 @@ Minor features
:ref:`extrabody <extrabody>` for adding custom code before the closing :ref:`extrabody <extrabody>` for adding custom code before the closing
``</body>`` tag. ``</body>`` 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` :mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -384,15 +384,15 @@ class TestInline(TestDataMixin, TestCase):
self.assertInHTML( self.assertInHTML(
'<div class="flex-container fieldBox field-position hidden">' '<div class="flex-container fieldBox field-position hidden">'
'<label class="inline">Position:</label>' '<label class="inline">Position:</label>'
'<div class="readonly">0</div></div>' '<div class="help"><div>Position help_text.</div></div>'
'<div class="help hidden"><div>Position help_text.</div></div>', '<div class="readonly">0</div></div>',
response.rendered_content, response.rendered_content,
) )
self.assertInHTML( self.assertInHTML(
'<div class="flex-container fieldBox field-position hidden">' '<div class="flex-container fieldBox field-position hidden">'
'<label class="inline">Position:</label>' '<label class="inline">Position:</label>'
'<div class="readonly">1</div></div>' '<div class="help"><div>Position help_text.</div></div>'
'<div class="help hidden"><div>Position help_text.</div></div>', '<div class="readonly">1</div></div>',
response.rendered_content, response.rendered_content,
) )
@ -414,16 +414,16 @@ class TestInline(TestDataMixin, TestCase):
self.assertInHTML( self.assertInHTML(
'<div class="form-row hidden field-position">' '<div class="form-row hidden field-position">'
'<div><div class="flex-container"><label>Position:</label>' '<div><div class="flex-container"><label>Position:</label>'
'<div class="help"><div>Position help_text.</div></div>'
'<div class="readonly">0</div></div>' '<div class="readonly">0</div></div>'
'<div class="help hidden"><div>Position help_text.</div></div>'
"</div></div>", "</div></div>",
response.rendered_content, response.rendered_content,
) )
self.assertInHTML( self.assertInHTML(
'<div class="form-row hidden field-position">' '<div class="form-row hidden field-position">'
'<div><div class="flex-container"><label>Position:</label>' '<div><div class="flex-container"><label>Position:</label>'
'<div class="help"><div>Position help_text.</div></div>'
'<div class="readonly">1</div></div>' '<div class="readonly">1</div></div>'
'<div class="help hidden"><div>Position help_text.</div></div>'
"</div></div>", "</div></div>",
response.rendered_content, response.rendered_content,
) )

View File

@ -6206,6 +6206,22 @@ class SeleniumTests(AdminSeleniumTestCase):
) )
self.take_screenshot("focus-multi-widget") 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): def test_cancel_delete_confirmation(self):
"Cancelling the deletion of an object takes the user back one page." "Cancelling the deletion of an object takes the user back one page."
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By