Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #34042 -- Improved accessibility of admin's navigation sidebar. #16093

Merged
merged 1 commit into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions django/contrib/admin/static/admin/css/nav_sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@
content: '\00AB';
}

.main > #nav-sidebar {
visibility: hidden;
}

.main.shifted > #nav-sidebar {
margin-left: 0;
visibility: visible;
}

[dir="rtl"] .main.shifted > #nav-sidebar {
Expand Down
29 changes: 3 additions & 26 deletions django/contrib/admin/static/admin/js/nav_sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,24 @@
{
const toggleNavSidebar = document.getElementById('toggle-nav-sidebar');
if (toggleNavSidebar !== null) {
const navLinks = document.querySelectorAll('#nav-sidebar a');
function disableNavLinkTabbing() {
for (const navLink of navLinks) {
navLink.tabIndex = -1;
}
}
function enableNavLinkTabbing() {
for (const navLink of navLinks) {
navLink.tabIndex = 0;
}
}
function disableNavFilterTabbing() {
document.getElementById('nav-filter').tabIndex = -1;
}
function enableNavFilterTabbing() {
document.getElementById('nav-filter').tabIndex = 0;
}

const navSidebar = document.getElementById('nav-sidebar');
const main = document.getElementById('main');
let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen');
if (navSidebarIsOpen === null) {
navSidebarIsOpen = 'true';
}
if (navSidebarIsOpen === 'false') {
disableNavLinkTabbing();
disableNavFilterTabbing();
}
main.classList.toggle('shifted', navSidebarIsOpen === 'true');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);

toggleNavSidebar.addEventListener('click', function() {
if (navSidebarIsOpen === 'true') {
navSidebarIsOpen = 'false';
disableNavLinkTabbing();
disableNavFilterTabbing();
} else {
navSidebarIsOpen = 'true';
enableNavLinkTabbing();
enableNavFilterTabbing();
}
localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen);
main.classList.toggle('shifted');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
});
}

Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/nav_sidebar.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% load i18n %}
<button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{% translate 'Toggle navigation' %}"></button>
<nav class="sticky" id="nav-sidebar">
<nav class="sticky" id="nav-sidebar" aria-label="{% translate 'Sidebar' %}">
<input type="search" id="nav-filter"
placeholder="{% translate 'Start typing to filter…' %}"
aria-label="{% translate 'Filter navigation items' %}">
Expand Down
53 changes: 30 additions & 23 deletions tests/admin_views/test_nav_sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,29 @@ def setUp(self):
def test_sidebar_not_on_index(self):
response = self.client.get(reverse("test_with_sidebar:index"))
self.assertContains(response, '<div class="main" id="main">')
self.assertNotContains(response, '<nav class="sticky" id="nav-sidebar">')
self.assertNotContains(
response, '<nav class="sticky" id="nav-sidebar" aria-label="Sidebar">'
)

def test_sidebar_disabled(self):
response = self.client.get(reverse("test_without_sidebar:index"))
self.assertNotContains(response, '<nav class="sticky" id="nav-sidebar">')
self.assertNotContains(
response, '<nav class="sticky" id="nav-sidebar" aria-label="Sidebar">'
)

def test_sidebar_unauthenticated(self):
self.client.logout()
response = self.client.get(reverse("test_with_sidebar:login"))
self.assertNotContains(response, '<nav class="sticky" id="nav-sidebar">')
self.assertNotContains(
response, '<nav class="sticky" id="nav-sidebar" aria-label="Sidebar">'
)

def test_sidebar_aria_current_page(self):
url = reverse("test_with_sidebar:auth_user_changelist")
response = self.client.get(url)
self.assertContains(response, '<nav class="sticky" id="nav-sidebar">')
self.assertContains(
response, '<nav class="sticky" id="nav-sidebar" aria-label="Sidebar">'
)
self.assertContains(
response, '<a href="%s" aria-current="page">Users</a>' % url
)
Expand All @@ -80,7 +88,9 @@ def test_sidebar_aria_current_page(self):
def test_sidebar_aria_current_page_missing_without_request_context_processor(self):
url = reverse("test_with_sidebar:auth_user_changelist")
response = self.client.get(url)
self.assertContains(response, '<nav class="sticky" id="nav-sidebar">')
self.assertContains(
response, '<nav class="sticky" id="nav-sidebar" aria-label="Sidebar">'
)
# Does not include aria-current attribute.
self.assertContains(response, '<a href="%s">Users</a>' % url)
self.assertNotContains(response, "aria-current")
Expand Down Expand Up @@ -146,16 +156,15 @@ def test_sidebar_can_be_closed(self):
)
self.assertEqual(toggle_button.tag_name, "button")
self.assertEqual(toggle_button.get_attribute("aria-label"), "Toggle navigation")
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"):
self.assertEqual(link.get_attribute("tabIndex"), "0")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter")
self.assertEqual(filter_input.get_attribute("tabIndex"), "0")
nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "true")
self.assertTrue(nav_sidebar.is_displayed())
toggle_button.click()
# Hidden sidebar is not reachable via keyboard navigation.
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"):
self.assertEqual(link.get_attribute("tabIndex"), "-1")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter")
self.assertEqual(filter_input.get_attribute("tabIndex"), "-1")

# Hidden sidebar is not visible.
nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "false")
self.assertFalse(nav_sidebar.is_displayed())
main_element = self.selenium.find_element(By.CSS_SELECTOR, "#main")
self.assertNotIn("shifted", main_element.get_attribute("class").split())

Expand Down Expand Up @@ -189,16 +198,14 @@ def test_sidebar_state_persists(self):
toggle_button = self.selenium.find_element(
By.CSS_SELECTOR, "#toggle-nav-sidebar"
)
# Hidden sidebar is not reachable via keyboard navigation.
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"):
self.assertEqual(link.get_attribute("tabIndex"), "-1")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter")
self.assertEqual(filter_input.get_attribute("tabIndex"), "-1")
# Hidden sidebar is not visible.
nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "false")
self.assertFalse(nav_sidebar.is_displayed())
toggle_button.click()
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"):
self.assertEqual(link.get_attribute("tabIndex"), "0")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter")
self.assertEqual(filter_input.get_attribute("tabIndex"), "0")
nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "true")
self.assertTrue(nav_sidebar.is_displayed())
self.assertEqual(
self.selenium.execute_script(
"return localStorage.getItem('django.admin.navSidebarIsOpen')"
Expand Down