mirror of
https://github.com/django/django.git
synced 2024-12-23 01:25:58 +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;
|
||||
}
|
||||
.form-row div.help {
|
||||
padding: 2px 3px;
|
||||
padding: 2px 3px 2px 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -574,6 +574,7 @@ ul.timelist, .timelist li {
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link {
|
||||
|
@ -10,15 +10,22 @@
|
||||
{% endif %}
|
||||
{% 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 %}">
|
||||
{% 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 %}
|
||||
<div>
|
||||
{% 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 %}">
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% else %}
|
||||
@ -26,7 +33,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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>{{ field.field.help_text|safe }}</div>
|
||||
</div>
|
||||
|
@ -41,6 +41,17 @@ Minor features
|
||||
:ref:`extrabody <extrabody>` for adding custom code before the closing
|
||||
``</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`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -384,15 +384,15 @@ class TestInline(TestDataMixin, TestCase):
|
||||
self.assertInHTML(
|
||||
'<div class="flex-container fieldBox field-position hidden">'
|
||||
'<label class="inline">Position:</label>'
|
||||
'<div class="readonly">0</div></div>'
|
||||
'<div class="help hidden"><div>Position help_text.</div></div>',
|
||||
'<div class="help"><div>Position help_text.</div></div>'
|
||||
'<div class="readonly">0</div></div>',
|
||||
response.rendered_content,
|
||||
)
|
||||
self.assertInHTML(
|
||||
'<div class="flex-container fieldBox field-position hidden">'
|
||||
'<label class="inline">Position:</label>'
|
||||
'<div class="readonly">1</div></div>'
|
||||
'<div class="help hidden"><div>Position help_text.</div></div>',
|
||||
'<div class="help"><div>Position help_text.</div></div>'
|
||||
'<div class="readonly">1</div></div>',
|
||||
response.rendered_content,
|
||||
)
|
||||
|
||||
@ -414,16 +414,16 @@ class TestInline(TestDataMixin, TestCase):
|
||||
self.assertInHTML(
|
||||
'<div class="form-row hidden field-position">'
|
||||
'<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="help hidden"><div>Position help_text.</div></div>'
|
||||
"</div></div>",
|
||||
response.rendered_content,
|
||||
)
|
||||
self.assertInHTML(
|
||||
'<div class="form-row hidden field-position">'
|
||||
'<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="help hidden"><div>Position help_text.</div></div>'
|
||||
"</div></div>",
|
||||
response.rendered_content,
|
||||
)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user