From 1520870c4a6c47df3ed2f597785a8d35a1710bc4 Mon Sep 17 00:00:00 2001
From: michalpokusa <72110769+michalpokusa@users.noreply.github.com>
Date: Thu, 5 Jun 2025 13:04:00 +0200
Subject: [PATCH] Fixed #36437 -- Improved accessibility of messages in admin.
---
.../contrib/admin/static/admin/css/base.css | 24 ++++++++++----
.../admin/static/admin/css/dark_mode.css | 6 ++++
.../admin/static/admin/css/responsive.css | 24 +++-----------
.../admin/static/admin/css/responsive_rtl.css | 10 ++++++
django/contrib/admin/static/admin/css/rtl.css | 5 +++
.../static/admin/img/icon-alert-dark.svg | 9 ++++++
.../admin/static/admin/img/icon-alert.svg | 2 +-
.../admin/static/admin/img/icon-no-dark.svg | 9 ++++++
.../admin/static/admin/img/icon-no.svg | 2 +-
.../admin/static/admin/img/icon-yes-dark.svg | 9 ++++++
.../admin/static/admin/img/icon-yes.svg | 2 +-
tests/admin_views/tests.py | 31 +++++++++++++++++++
12 files changed, 104 insertions(+), 29 deletions(-)
create mode 100644 django/contrib/admin/static/admin/img/icon-alert-dark.svg
create mode 100644 django/contrib/admin/static/admin/img/icon-no-dark.svg
create mode 100644 django/contrib/admin/static/admin/img/icon-yes-dark.svg
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index ae0a55c496..62fe9868b7 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -35,8 +35,11 @@ html[data-theme="light"],
--error-fg: #ba2121;
--message-success-bg: #dfd;
+ --message-success-icon: url(../img/icon-yes.svg);
--message-warning-bg: #ffc;
+ --message-warning-icon: url(../img/icon-alert.svg);
--message-error-bg: #ffefef;
+ --message-error-icon: url(../img/icon-no.svg);
--darkened-bg: #f8f8f8; /* A bit darker than --body-bg */
--selected-bg: #e4e4e4; /* E.g. selected table cells */
@@ -637,20 +640,29 @@ ul.messagelist li {
font-size: 0.8125rem;
padding: 10px 10px 10px 65px;
margin: 0 0 10px 0;
- background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat;
- background-size: 16px auto;
color: var(--body-fg);
word-break: break-word;
+ background-color: var(--message-success-bg);
+ background-image: var(--message-success-icon);
+ background-position: 40px 12px;
+ background-repeat: no-repeat;
+ background-size: 16px auto;
}
ul.messagelist li.warning {
- background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat;
- background-size: 14px auto;
+ background-color: var(--message-warning-bg);
+ background-image: var(--message-warning-icon);
}
ul.messagelist li.error {
- background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat;
- background-size: 16px auto;
+ background-color: var(--message-error-bg);
+ background-image: var(--message-error-icon);
+}
+
+@media (forced-colors: active) {
+ ul.messagelist li {
+ border: 1px solid;
+ }
}
.errornote {
diff --git a/django/contrib/admin/static/admin/css/dark_mode.css b/django/contrib/admin/static/admin/css/dark_mode.css
index 65b58d035f..50f11affc0 100644
--- a/django/contrib/admin/static/admin/css/dark_mode.css
+++ b/django/contrib/admin/static/admin/css/dark_mode.css
@@ -21,8 +21,11 @@
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
+ --message-success-icon: url(../img/icon-yes-dark.svg);
--message-warning-bg: #583305;
+ --message-warning-icon: url(../img/icon-alert-dark.svg);
--message-error-bg: #570808;
+ --message-error-icon: url(../img/icon-no-dark.svg);
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
@@ -58,8 +61,11 @@ html[data-theme="dark"] {
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
+ --message-success-icon: url(../img/icon-yes-dark.svg);
--message-warning-bg: #583305;
+ --message-warning-icon: url(../img/icon-alert-dark.svg);
--message-error-bg: #570808;
+ --message-error-icon: url(../img/icon-no-dark.svg);
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index f0fcade41c..7a2c321ceb 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -337,16 +337,8 @@ input[type="submit"], button {
/* Messages */
ul.messagelist li {
- padding-left: 55px;
- background-position: 30px 12px;
- }
-
- ul.messagelist li.error {
- background-position: 30px 12px;
- }
-
- ul.messagelist li.warning {
- background-position: 30px 14px;
+ padding: 10px 10px 10px 55px;
+ background-position-x: 30px;
}
/* Login */
@@ -739,16 +731,8 @@ input[type="submit"], button {
/* Messages */
ul.messagelist li {
- padding-left: 40px;
- background-position: 15px 12px;
- }
-
- ul.messagelist li.error {
- background-position: 15px 12px;
- }
-
- ul.messagelist li.warning {
- background-position: 15px 14px;
+ padding: 10px 10px 10px 40px;
+ background-position-x: 15px;
}
/* Paginator */
diff --git a/django/contrib/admin/static/admin/css/responsive_rtl.css b/django/contrib/admin/static/admin/css/responsive_rtl.css
index 5e8f5c5943..acad9e8b6c 100644
--- a/django/contrib/admin/static/admin/css/responsive_rtl.css
+++ b/django/contrib/admin/static/admin/css/responsive_rtl.css
@@ -47,6 +47,11 @@
padding-left: 0;
padding-right: 16px;
}
+
+ [dir="rtl"] ul.messagelist li {
+ padding: 10px 55px 10px 10px;
+ background-position-x: calc(100% - 30px);
+ }
}
/* MOBILE */
@@ -86,4 +91,9 @@
[dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -72px;
}
+
+ [dir="rtl"] ul.messagelist li {
+ padding: 10px 40px 10px 10px;
+ background-position-x: calc(100% - 15px);
+ }
}
diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css
index a2556d0478..0d843726bf 100644
--- a/django/contrib/admin/static/admin/css/rtl.css
+++ b/django/contrib/admin/static/admin/css/rtl.css
@@ -291,3 +291,8 @@ form .form-row p.datetime {
.selector .selector-chooser {
margin: 0;
}
+
+ul.messagelist li {
+ padding: 10px 65px 10px 10px;
+ background-position-x: calc(100% - 40px);
+}
diff --git a/django/contrib/admin/static/admin/img/icon-alert-dark.svg b/django/contrib/admin/static/admin/img/icon-alert-dark.svg
new file mode 100644
index 0000000000..a6365f5ac8
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/icon-alert-dark.svg
@@ -0,0 +1,9 @@
+
diff --git a/django/contrib/admin/static/admin/img/icon-alert.svg b/django/contrib/admin/static/admin/img/icon-alert.svg
index a6365f5ac8..9b4ee36750 100644
--- a/django/contrib/admin/static/admin/img/icon-alert.svg
+++ b/django/contrib/admin/static/admin/img/icon-alert.svg
@@ -5,5 +5,5 @@
Icon Family: classic
Icon Style: solid
-->
-
+
diff --git a/django/contrib/admin/static/admin/img/icon-no-dark.svg b/django/contrib/admin/static/admin/img/icon-no-dark.svg
new file mode 100644
index 0000000000..bb55c52686
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/icon-no-dark.svg
@@ -0,0 +1,9 @@
+
diff --git a/django/contrib/admin/static/admin/img/icon-no.svg b/django/contrib/admin/static/admin/img/icon-no.svg
index 27089244ce..6c5b15df05 100644
--- a/django/contrib/admin/static/admin/img/icon-no.svg
+++ b/django/contrib/admin/static/admin/img/icon-no.svg
@@ -5,5 +5,5 @@
Icon Family: classic
Icon Style: solid
-->
-
+
diff --git a/django/contrib/admin/static/admin/img/icon-yes-dark.svg b/django/contrib/admin/static/admin/img/icon-yes-dark.svg
new file mode 100644
index 0000000000..482292c627
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/icon-yes-dark.svg
@@ -0,0 +1,9 @@
+
diff --git a/django/contrib/admin/static/admin/img/icon-yes.svg b/django/contrib/admin/static/admin/img/icon-yes.svg
index 4ee7cbae90..71683dcc3a 100644
--- a/django/contrib/admin/static/admin/img/icon-yes.svg
+++ b/django/contrib/admin/static/admin/img/icon-yes.svg
@@ -5,5 +5,5 @@
Icon Family: classic
Icon Style: solid
-->
-
+
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 9ea603423c..2029909df9 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -135,6 +135,7 @@ from .models import (
UnchangeableObject,
UndeletableObject,
UnorderedObject,
+ UserMessenger,
UserProxy,
Villain,
Vodcast,
@@ -6876,6 +6877,36 @@ class SeleniumTests(AdminSeleniumTestCase):
name_input_value = name_input.get_attribute("value")
self.assertEqual(name_input_value, "Test section 1")
+ @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
+ @override_settings(MESSAGE_LEVEL=10)
+ def test_messages(self):
+ from selenium.webdriver.common.by import By
+ from selenium.webdriver.support.ui import Select
+
+ with override_settings(MESSAGE_LEVEL=10):
+ self.admin_login(
+ username="super", password="secret", login_url=reverse("admin:index")
+ )
+ UserMessenger.objects.create()
+ for level in ["warning", "info", "error", "success", "debug"]:
+ self.selenium.get(
+ self.live_server_url
+ + reverse("admin:admin_views_usermessenger_changelist"),
+ )
+ checkbox = self.selenium.find_element(
+ By.CSS_SELECTOR, "tr input.action-select"
+ )
+ checkbox.click()
+ Select(self.selenium.find_element(By.NAME, "action")).select_by_value(
+ f"message_{level}"
+ )
+ self.selenium.find_element(By.XPATH, '//button[text()="Run"]').click()
+ message = self.selenium.find_element(
+ By.CSS_SELECTOR, "ul.messagelist li"
+ )
+ self.assertEqual(message.get_attribute("innerText"), f"Test {level}")
+ self.take_screenshot(level)
+
@override_settings(ROOT_URLCONF="admin_views.urls")
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):