diff options
| author | antoliny0919 <antoliny0919@gmail.com> | 2025-05-08 19:24:07 +0900 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2025-06-27 18:03:16 +0200 |
| commit | 3f59711581bd22ebd0f13fb040b15b69c0eee21f (patch) | |
| tree | 7b3eea333004013bbfdd1d48b6eb4644cd164c8f | |
| parent | 29f5e1e97d660855d34c1ee5edbd5cfe094b6ca7 (diff) | |
Fixed #36366 -- Improved accessibility of pagination in the admin.
| -rw-r--r-- | django/contrib/admin/static/admin/css/base.css | 27 | ||||
| -rw-r--r-- | django/contrib/admin/static/admin/css/changelists.css | 20 | ||||
| -rw-r--r-- | django/contrib/admin/static/admin/css/rtl.css | 2 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/change_list.html | 7 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/object_history.html | 13 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/pagination.html | 18 | ||||
| -rw-r--r-- | django/contrib/admin/templatetags/admin_list.py | 8 | ||||
| -rw-r--r-- | tests/admin_changelist/tests.py | 35 | ||||
| -rw-r--r-- | tests/admin_views/test_history_view.py | 21 |
9 files changed, 118 insertions, 33 deletions
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index 79ea073bda..c672a3051f 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -1164,7 +1164,19 @@ a.deletelink:focus, a.deletelink:hover { box-sizing: border-box; } +.paginator ul { + margin: 0; + margin-right: 6px; +} + +.paginator ul li { + display: inline-block; + line-height: 22px; + padding: 0; +} + .paginator a:link, .paginator a:visited { + display: inline-block; padding: 2px 6px; background: var(--button-bg); text-decoration: none; @@ -1182,18 +1194,15 @@ a.deletelink:focus, a.deletelink:hover { color: var(--link-hover-color); } -.paginator .end { - margin-right: 6px; -} - -.paginator .this-page { - padding: 2px 6px; +.paginator a[aria-current="page"] { + color: var(--body-quiet-color); + background: transparent; font-weight: bold; - font-size: 0.8125rem; - vertical-align: top; + cursor: default; } -.paginator a:focus, .paginator a:hover { +.paginator a:not([aria-current="page"]):focus, +.paginator a:not([aria-current="page"]):hover { color: white; background: var(--link-hover-color); } diff --git a/django/contrib/admin/static/admin/css/changelists.css b/django/contrib/admin/static/admin/css/changelists.css index 005b7768c8..1edca65698 100644 --- a/django/contrib/admin/static/admin/css/changelists.css +++ b/django/contrib/admin/static/admin/css/changelists.css @@ -43,6 +43,22 @@ border-bottom: 1px solid var(--hairline-color); } +#changelist .changelist-footer { + display: flex; + align-items: center; + padding: 10px; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +#changelist .changelist-footer .paginator { + color: var(--body-quiet-color); + background: var(--body-bg); + border: none; + padding: 0; + overflow: hidden; +} + #changelist .paginator { color: var(--body-quiet-color); border-bottom: 1px solid var(--hairline-color); @@ -50,6 +66,10 @@ overflow: hidden; } +#changelist .paginator ul { + padding: 0; +} + /* CHANGELIST TABLES */ #changelist table thead th { diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css index 0d843726bf..b824d7d0db 100644 --- a/django/contrib/admin/static/admin/css/rtl.css +++ b/django/contrib/admin/static/admin/css/rtl.css @@ -107,7 +107,7 @@ thead th.sorted .text { border-left: none; } -.paginator .end { +.paginator ul { margin-left: 6px; margin-right: 0; } diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 162560dabc..3b3ea408d3 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -69,7 +69,12 @@ {% result_list cl %} {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} {% endblock %} - {% block pagination %}{% pagination cl %}{% endblock %} + {% block pagination %} + <div class="changelist-footer"> + {% pagination cl %} + {% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %} + {% endblock %} + </div> </form> </div> {% block filters %} diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 2b3b06caa0..40aaecc40a 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -34,20 +34,23 @@ {% endfor %} </tbody> </table> - <p class="paginator"> + <nav class="paginator" aria-labelledby="pagination"> + <h2 id="pagination" class="visually-hidden">{% blocktranslate with name=opts.verbose_name %}Pagination {{ name }} entries{% endblocktranslate %}</h2> {% if pagination_required %} + <ul> {% for i in page_range %} {% if i == action_list.paginator.ELLIPSIS %} - {{ action_list.paginator.ELLIPSIS }} + <li>{{ action_list.paginator.ELLIPSIS }}</li> {% elif i == action_list.number %} - <span class="this-page">{{ i }}</span> + <li><a role="button" href="" aria-current="page">{{ i }}</a></li> {% else %} - <a role="button" href="?{{ page_var }}={{ i }}" {% if i == action_list.paginator.num_pages %} class="end" {% endif %}>{{ i }}</a> + <li><a role="button" href="?{{ page_var }}={{ i }}">{{ i }}</a></li> {% endif %} {% endfor %} + </ul> {% endif %} {{ action_list.paginator.count }} {% blocktranslate count counter=action_list.paginator.count %}entry{% plural %}entries{% endblocktranslate %} - </p> + </nav> {% else %} <p>{% translate 'This object doesn’t have a change history. It probably wasn’t added via this admin site.' %}</p> {% endif %} diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html index bc3117bfcb..954e710856 100644 --- a/django/contrib/admin/templates/admin/pagination.html +++ b/django/contrib/admin/templates/admin/pagination.html @@ -1,12 +1,14 @@ {% load admin_list %} {% load i18n %} -<p class="paginator"> -{% if pagination_required %} -{% for i in page_range %} - {% paginator_number cl i %} -{% endfor %} -{% endif %} +<nav class="paginator" aria-labelledby="pagination"> + <h2 id="pagination" class="visually-hidden">{% blocktranslate with name=cl.opts.verbose_name_plural %}Pagination {{ name }}{% endblocktranslate %}</h2> + {% if pagination_required %} + <ul> + {% for i in page_range %} + <li>{% paginator_number cl i %}</li> + {% endfor %} + </ul> + {% endif %} {{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endif %} {% if show_all_url %}<a href="{{ show_all_url }}" class="showall">{% translate 'Show all' %}</a>{% endif %} -{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %} -</p> +</nav> diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index e7cb244af1..1e6f8bf298 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -42,12 +42,14 @@ def paginator_number(cl, i): if i == cl.paginator.ELLIPSIS: return format_html("{} ", cl.paginator.ELLIPSIS) elif i == cl.page_num: - return format_html('<span class="this-page">{}</span> ', i) + return format_html( + '<a role="button" href="" aria-current="page">{}</a> ', + i, + ) else: return format_html( - '<a role="button" href="{}"{}>{}</a> ', + '<a role="button" href="{}">{}</a> ', cl.get_query_string({PAGE_VAR: i}), - mark_safe(' class="end"' if i == cl.paginator.num_pages else ""), i, ) diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 7203daa6b0..cd2fe7c645 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -946,6 +946,41 @@ class ChangeListTests(TestCase): self.assertEqual(cl.paginator.count, 30) self.assertEqual(list(cl.paginator.page_range), [1, 2, 3]) + def test_pagination_render(self): + objs = [Swallow(origin=f"Swallow {i}", load=i, speed=i) for i in range(1, 5)] + Swallow.objects.bulk_create(objs) + + request = self.factory.get("/child/") + request.user = self.superuser + + admin = SwallowAdmin(Swallow, custom_site) + cl = admin.get_changelist_instance(request) + template = Template( + "{% load admin_list %}{% spaceless %}{% pagination cl %}{% endspaceless %}" + ) + context = Context({"cl": cl, "opts": cl.opts}) + pagination_output = template.render(context) + self.assertTrue( + pagination_output.startswith( + '<nav class="paginator" aria-labelledby="pagination">' + ) + ) + self.assertInHTML( + '<h2 id="pagination" class="visually-hidden">Pagination swallows</h2>', + pagination_output, + ) + self.assertTrue(pagination_output.endswith("</nav>")) + self.assertInHTML( + '<li><a role="button" href="" aria-current="page">1</a></li>', + pagination_output, + ) + self.assertInHTML( + '<li><a role="button" href="?p=2">2</a></li>', + pagination_output, + ) + self.assertEqual(pagination_output.count('aria-current="page"'), 1) + self.assertEqual(pagination_output.count('href=""'), 1) + def test_computed_list_display_localization(self): """ Regression test for #13196: output of functions should be localized diff --git a/tests/admin_views/test_history_view.py b/tests/admin_views/test_history_view.py index 7079c1d0d8..49a60aa306 100644 --- a/tests/admin_views/test_history_view.py +++ b/tests/admin_views/test_history_view.py @@ -80,20 +80,29 @@ class SeleniumTests(AdminSeleniumTestCase): self.selenium.get(self.live_server_url + user_history_url) paginator = self.selenium.find_element(By.CSS_SELECTOR, ".paginator") + self.assertEqual(paginator.tag_name, "nav") + labelledby = paginator.get_attribute("aria-labelledby") + description = self.selenium.find_element(By.CSS_SELECTOR, "#%s" % labelledby) + self.assertHTMLEqual( + description.get_attribute("outerHTML"), + '<h2 id="pagination" class="visually-hidden">Pagination user entries</h2>', + ) self.assertTrue(paginator.is_displayed()) + aria_current_link = paginator.find_elements(By.CSS_SELECTOR, "[aria-current]") + self.assertEqual(len(aria_current_link), 1) + # The current page. + current_page_link = aria_current_link[0] + self.assertEqual(current_page_link.get_attribute("aria-current"), "page") + self.assertEqual(current_page_link.get_attribute("href"), "") self.assertIn("%s entries" % LogEntry.objects.count(), paginator.text) self.assertIn(str(Paginator.ELLIPSIS), paginator.text) - # The current page. - current_page_link = self.selenium.find_element( - By.CSS_SELECTOR, "span.this-page" - ) self.assertEqual(current_page_link.text, "1") # The last page. - last_page_link = self.selenium.find_element(By.CSS_SELECTOR, ".end") + last_page_link = self.selenium.find_element(By.XPATH, "//ul/li[last()]/a") self.assertTrue(last_page_link.text, "20") # Select the second page. pages = paginator.find_elements(By.TAG_NAME, "a") - second_page_link = pages[0] + second_page_link = pages[1] self.assertEqual(second_page_link.text, "2") second_page_link.click() self.assertIn("?p=2", self.selenium.current_url) |
