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 %}
- {% block filters %}
- {% if cl.has_filters %}
+ {% block filters %}
+ {% if cl.has_filters %}
{% if cl.is_facets_optional or cl.has_active_filters %}{% endif %}
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
- {% endif %}
- {% endblock %}
+ {% endif %}
+ {% endblock %}
+
+ {% block search %}{% search_form cl %}{% endblock %}
+ {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
+
+
+
+
{% 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