Skip to content

Commit 6ea3319

Browse files
Antoliny0919sarahboyce
authored andcommitted
Fixed #36511 -- Ensured filters came before table in keyboard navigation in admin changelist.
1 parent 792ca14 commit 6ea3319

5 files changed

Lines changed: 92 additions & 33 deletions

File tree

django/contrib/admin/static/admin/css/changelists.css

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
/* CHANGELISTS */
22

3-
#changelist {
3+
#changelist .changelist-form-container {
44
display: flex;
5+
flex-wrap: wrap;
56
align-items: flex-start;
6-
justify-content: space-between;
7+
width: 100%;
78
}
89

9-
#changelist .changelist-form-container {
10+
#changelist .changelist-form-container > div {
1011
flex: 1 1 auto;
11-
min-width: 0;
12+
}
13+
14+
#changelist .changelist-form-container:not(:has(#changelist-filter)) > div {
15+
width: 100%;
16+
}
17+
18+
#changelist .changelist-form-container:has(#changelist-filter) > div {
19+
max-width: calc(100% - 270px);
1220
}
1321

1422
#changelist table {

django/contrib/admin/static/admin/css/responsive.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,15 @@ input[type="submit"], button {
409409

410410
/* Changelist */
411411

412-
#changelist {
413-
align-items: stretch;
412+
#changelist .changelist-form-container {
414413
flex-direction: column;
415414
}
416415

416+
#changelist .changelist-form-container:has(#changelist-filter) > div {
417+
max-width: 100%;
418+
width: 100%;
419+
}
420+
417421
#toolbar {
418422
padding: 10px;
419423
}
@@ -436,8 +440,7 @@ input[type="submit"], button {
436440
}
437441

438442
#changelist-filter {
439-
position: static;
440-
width: auto;
443+
width: 100%;
441444
margin-top: 30px;
442445
}
443446

django/contrib/admin/templates/admin/change_list.html

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,29 +56,8 @@
5656
{% endif %}
5757
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
5858
<div class="changelist-form-container">
59-
{% block search %}{% search_form cl %}{% endblock %}
60-
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
61-
62-
<form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
63-
{% if cl.formset %}
64-
<div>{{ cl.formset.management_form }}</div>
65-
{% endif %}
66-
67-
{% block result_list %}
68-
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
69-
{% result_list cl %}
70-
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
71-
{% endblock %}
72-
{% block pagination %}
73-
<div class="changelist-footer">
74-
{% pagination cl %}
75-
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
76-
{% endblock %}
77-
</div>
78-
</form>
79-
</div>
80-
{% block filters %}
81-
{% if cl.has_filters %}
59+
{% block filters %}
60+
{% if cl.has_filters %}
8261
<search id="changelist-filter" aria-labelledby="changelist-filter-header">
8362
<h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
8463
{% if cl.is_facets_optional or cl.has_active_filters %}<div id="changelist-filter-extra-actions">
@@ -92,8 +71,31 @@ <h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
9271
</div>{% endif %}
9372
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
9473
</search>
95-
{% endif %}
96-
{% endblock %}
74+
{% endif %}
75+
{% endblock %}
76+
<div>
77+
{% block search %}{% search_form cl %}{% endblock %}
78+
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
79+
80+
<form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
81+
{% if cl.formset %}
82+
<div>{{ cl.formset.management_form }}</div>
83+
{% endif %}
84+
85+
{% block result_list %}
86+
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
87+
{% result_list cl %}
88+
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
89+
{% endblock %}
90+
{% block pagination %}
91+
<div class="changelist-footer">
92+
{% pagination cl %}
93+
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
94+
{% endblock %}
95+
</div>
96+
</form>
97+
</div>
98+
</div>
9799
</div>
98100
</div>
99101
{% endblock %}

tests/admin_views/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ class PodcastAdmin(admin.ModelAdmin):
431431
list_display = ("name", "release_date")
432432
list_editable = ("release_date",)
433433
date_hierarchy = "release_date"
434+
list_filter = ("name",)
435+
search_fields = ("name",)
434436
ordering = ("name",)
435437

436438

tests/admin_views/test_skip_link_to_content.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.test import override_settings
44
from django.urls import reverse
55

6+
from .models import Podcast
7+
68

79
@override_settings(ROOT_URLCONF="admin_views.urls")
810
class SeleniumTests(AdminSeleniumTestCase):
@@ -125,3 +127,45 @@ def test_skip_link_with_RTL_language_doesnt_create_horizontal_scrolling(self):
125127
)
126128
self.assertTrue(is_vertical_scrolleable)
127129
self.assertFalse(is_horizontal_scrolleable)
130+
131+
def test_skip_link_keyboard_navigation_in_changelist(self):
132+
from selenium.webdriver.common.by import By
133+
from selenium.webdriver.common.keys import Keys
134+
135+
Podcast.objects.create(name="apple", release_date="2000-09-19")
136+
self.admin_login(
137+
username="super", password="secret", login_url=reverse("admin:index")
138+
)
139+
self.selenium.get(
140+
self.live_server_url + reverse("admin:admin_views_podcast_changelist")
141+
)
142+
selectors = [
143+
"ul.object-tools", # object_tools.
144+
"search#changelist-filter", # list_filter.
145+
"form#changelist-search", # search_fields.
146+
"nav.toplinks", # date_hierarchy.
147+
"form#changelist-form div.actions", # action.
148+
"table#result_list", # table.
149+
"div.changelist-footer", # footer.
150+
]
151+
content = self.selenium.find_element(By.ID, "content-start")
152+
content.send_keys(Keys.TAB)
153+
154+
for selector in selectors:
155+
with self.subTest(selector=selector):
156+
# Currently focused element.
157+
focused_element = self.selenium.switch_to.active_element
158+
expected_element = self.selenium.find_element(By.CSS_SELECTOR, selector)
159+
element_points = self.selenium.find_elements(
160+
By.CSS_SELECTOR,
161+
f"{selector} a, {selector} input, {selector} button",
162+
)
163+
self.assertIn(
164+
focused_element.get_attribute("outerHTML"),
165+
expected_element.get_attribute("innerHTML"),
166+
)
167+
# Move to the next container element via TAB.
168+
for point in element_points[::-1]:
169+
if point.is_displayed():
170+
point.send_keys(Keys.TAB)
171+
break

0 commit comments

Comments
 (0)