summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorantoliny0919 <antoliny0919@gmail.com>2025-07-20 22:12:15 +0900
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-07-30 17:32:39 +0200
commit6ea331907996a51842da55c1f8d65eea7b367c7d (patch)
tree3deaa0a318b80298438a366dd1808d4ef5be61ba
parent792ca148a2d6da6cf0778f6a866c899208ab94f9 (diff)
Fixed #36511 -- Ensured filters came before table in keyboard navigation in admin changelist.
-rw-r--r--django/contrib/admin/static/admin/css/changelists.css16
-rw-r--r--django/contrib/admin/static/admin/css/responsive.css11
-rw-r--r--django/contrib/admin/templates/admin/change_list.html52
-rw-r--r--tests/admin_views/admin.py2
-rw-r--r--tests/admin_views/test_skip_link_to_content.py44
5 files changed, 92 insertions, 33 deletions
diff --git a/django/contrib/admin/static/admin/css/changelists.css b/django/contrib/admin/static/admin/css/changelists.css
index ae90943e93..141b79040d 100644
--- a/django/contrib/admin/static/admin/css/changelists.css
+++ b/django/contrib/admin/static/admin/css/changelists.css
@@ -1,14 +1,22 @@
/* CHANGELISTS */
-#changelist {
+#changelist .changelist-form-container {
display: flex;
+ flex-wrap: wrap;
align-items: flex-start;
- justify-content: space-between;
+ width: 100%;
}
-#changelist .changelist-form-container {
+#changelist .changelist-form-container > div {
flex: 1 1 auto;
- min-width: 0;
+}
+
+#changelist .changelist-form-container:not(:has(#changelist-filter)) > div {
+ width: 100%;
+}
+
+#changelist .changelist-form-container:has(#changelist-filter) > div {
+ max-width: calc(100% - 270px);
}
#changelist table {
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index 9aa895316c..bf6cd6a56c 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -409,11 +409,15 @@ input[type="submit"], button {
/* Changelist */
- #changelist {
- align-items: stretch;
+ #changelist .changelist-form-container {
flex-direction: column;
}
+ #changelist .changelist-form-container:has(#changelist-filter) > div {
+ max-width: 100%;
+ width: 100%;
+ }
+
#toolbar {
padding: 10px;
}
@@ -436,8 +440,7 @@ input[type="submit"], button {
}
#changelist-filter {
- position: static;
- width: auto;
+ width: 100%;
margin-top: 30px;
}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 3b3ea408d3..b12f8ec583 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -56,29 +56,8 @@
{% endif %}
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
<div class="changelist-form-container">
- {% block search %}{% search_form cl %}{% endblock %}
- {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
-
- <form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
- {% if cl.formset %}
- <div>{{ cl.formset.management_form }}</div>
- {% endif %}
-
- {% block result_list %}
- {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
- {% result_list cl %}
- {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
- {% 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 %}
- {% if cl.has_filters %}
+ {% block filters %}
+ {% if cl.has_filters %}
<search id="changelist-filter" aria-labelledby="changelist-filter-header">
<h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
{% if cl.is_facets_optional or cl.has_active_filters %}<div id="changelist-filter-extra-actions">
@@ -92,8 +71,31 @@
</div>{% endif %}
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
</search>
- {% endif %}
- {% endblock %}
+ {% endif %}
+ {% endblock %}
+ <div>
+ {% block search %}{% search_form cl %}{% endblock %}
+ {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
+
+ <form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
+ {% if cl.formset %}
+ <div>{{ cl.formset.management_form }}</div>
+ {% endif %}
+
+ {% block result_list %}
+ {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
+ {% result_list cl %}
+ {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
+ {% 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>
+ </div>
</div>
</div>
{% endblock %}
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 95d16a4770..b6d009b388 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -431,6 +431,8 @@ class PodcastAdmin(admin.ModelAdmin):
list_display = ("name", "release_date")
list_editable = ("release_date",)
date_hierarchy = "release_date"
+ list_filter = ("name",)
+ search_fields = ("name",)
ordering = ("name",)
diff --git a/tests/admin_views/test_skip_link_to_content.py b/tests/admin_views/test_skip_link_to_content.py
index 3284b76495..5cec63307b 100644
--- a/tests/admin_views/test_skip_link_to_content.py
+++ b/tests/admin_views/test_skip_link_to_content.py
@@ -3,6 +3,8 @@ from django.contrib.auth.models import User
from django.test import override_settings
from django.urls import reverse
+from .models import Podcast
+
@override_settings(ROOT_URLCONF="admin_views.urls")
class SeleniumTests(AdminSeleniumTestCase):
@@ -125,3 +127,45 @@ class SeleniumTests(AdminSeleniumTestCase):
)
self.assertTrue(is_vertical_scrolleable)
self.assertFalse(is_horizontal_scrolleable)
+
+ def test_skip_link_keyboard_navigation_in_changelist(self):
+ from selenium.webdriver.common.by import By
+ from selenium.webdriver.common.keys import Keys
+
+ Podcast.objects.create(name="apple", release_date="2000-09-19")
+ self.admin_login(
+ username="super", password="secret", login_url=reverse("admin:index")
+ )
+ self.selenium.get(
+ self.live_server_url + reverse("admin:admin_views_podcast_changelist")
+ )
+ selectors = [
+ "ul.object-tools", # object_tools.
+ "search#changelist-filter", # list_filter.
+ "form#changelist-search", # search_fields.
+ "nav.toplinks", # date_hierarchy.
+ "form#changelist-form div.actions", # action.
+ "table#result_list", # table.
+ "div.changelist-footer", # footer.
+ ]
+ content = self.selenium.find_element(By.ID, "content-start")
+ content.send_keys(Keys.TAB)
+
+ for selector in selectors:
+ with self.subTest(selector=selector):
+ # Currently focused element.
+ focused_element = self.selenium.switch_to.active_element
+ expected_element = self.selenium.find_element(By.CSS_SELECTOR, selector)
+ element_points = self.selenium.find_elements(
+ By.CSS_SELECTOR,
+ f"{selector} a, {selector} input, {selector} button",
+ )
+ self.assertIn(
+ focused_element.get_attribute("outerHTML"),
+ expected_element.get_attribute("innerHTML"),
+ )
+ # Move to the next container element via TAB.
+ for point in element_points[::-1]:
+ if point.is_displayed():
+ point.send_keys(Keys.TAB)
+ break