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:
parent
5ed72087c4
commit
6fbdfe14f6
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user