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 search %}{% search_form cl %}{% endblock %} - {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %} - -
{% csrf_token %} - {% if cl.formset %} -
{{ cl.formset.management_form }}
- {% 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 %} - -
-
- {% block filters %} - {% if cl.has_filters %} + {% block filters %} + {% if cl.has_filters %}

{% translate 'Filter' %}

{% if cl.is_facets_optional or cl.has_active_filters %}
@@ -92,8 +71,31 @@
{% 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 %} + +
{% csrf_token %} + {% if cl.formset %} +
{{ cl.formset.management_form }}
+ {% 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 %} + +
+
+
{% 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