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

This commit is contained in:
Rasmus Magnell 2022-09-24 12:21:11 +02:00 committed by Mariusz Felisiak
parent 7c9d0c31d5
commit c4aac2ac1e
4 changed files with 39 additions and 50 deletions

View File

@ -59,8 +59,13 @@
content: '\00AB'; content: '\00AB';
} }
.main > #nav-sidebar {
visibility: hidden;
}
.main.shifted > #nav-sidebar { .main.shifted > #nav-sidebar {
margin-left: 0; margin-left: 0;
visibility: visible;
} }
[dir="rtl"] .main.shifted > #nav-sidebar { [dir="rtl"] .main.shifted > #nav-sidebar {

View File

@ -2,47 +2,24 @@
{ {
const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); const toggleNavSidebar = document.getElementById('toggle-nav-sidebar');
if (toggleNavSidebar !== null) { if (toggleNavSidebar !== null) {
const navLinks = document.querySelectorAll('#nav-sidebar a'); const navSidebar = document.getElementById('nav-sidebar');
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 main = document.getElementById('main'); const main = document.getElementById('main');
let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen');
if (navSidebarIsOpen === null) { if (navSidebarIsOpen === null) {
navSidebarIsOpen = 'true'; navSidebarIsOpen = 'true';
} }
if (navSidebarIsOpen === 'false') {
disableNavLinkTabbing();
disableNavFilterTabbing();
}
main.classList.toggle('shifted', navSidebarIsOpen === 'true'); main.classList.toggle('shifted', navSidebarIsOpen === 'true');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
toggleNavSidebar.addEventListener('click', function() { toggleNavSidebar.addEventListener('click', function() {
if (navSidebarIsOpen === 'true') { if (navSidebarIsOpen === 'true') {
navSidebarIsOpen = 'false'; navSidebarIsOpen = 'false';
disableNavLinkTabbing();
disableNavFilterTabbing();
} else { } else {
navSidebarIsOpen = 'true'; navSidebarIsOpen = 'true';
enableNavLinkTabbing();
enableNavFilterTabbing();
} }
localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen);
main.classList.toggle('shifted'); main.classList.toggle('shifted');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
}); });
} }

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{% translate 'Toggle navigation' %}"></button> <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" <input type="search" id="nav-filter"
placeholder="{% translate 'Start typing to filter…' %}" placeholder="{% translate 'Start typing to filter…' %}"
aria-label="{% translate 'Filter navigation items' %}"> aria-label="{% translate 'Filter navigation items' %}">

View File

@ -43,21 +43,29 @@ class AdminSidebarTests(TestCase):
def test_sidebar_not_on_index(self): def test_sidebar_not_on_index(self):
response = self.client.get(reverse("test_with_sidebar:index")) response = self.client.get(reverse("test_with_sidebar:index"))
self.assertContains(response, '<div class="main" id="main">') 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): def test_sidebar_disabled(self):
response = self.client.get(reverse("test_without_sidebar:index")) 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): def test_sidebar_unauthenticated(self):
self.client.logout() self.client.logout()
response = self.client.get(reverse("test_with_sidebar:login")) 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): def test_sidebar_aria_current_page(self):
url = reverse("test_with_sidebar:auth_user_changelist") url = reverse("test_with_sidebar:auth_user_changelist")
response = self.client.get(url) 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( self.assertContains(
response, '<a href="%s" aria-current="page">Users</a>' % url response, '<a href="%s" aria-current="page">Users</a>' % url
) )
@ -80,7 +88,9 @@ class AdminSidebarTests(TestCase):
def test_sidebar_aria_current_page_missing_without_request_context_processor(self): def test_sidebar_aria_current_page_missing_without_request_context_processor(self):
url = reverse("test_with_sidebar:auth_user_changelist") url = reverse("test_with_sidebar:auth_user_changelist")
response = self.client.get(url) 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. # Does not include aria-current attribute.
self.assertContains(response, '<a href="%s">Users</a>' % url) self.assertContains(response, '<a href="%s">Users</a>' % url)
self.assertNotContains(response, "aria-current") self.assertNotContains(response, "aria-current")
@ -146,16 +156,15 @@ class SeleniumTests(AdminSeleniumTestCase):
) )
self.assertEqual(toggle_button.tag_name, "button") self.assertEqual(toggle_button.tag_name, "button")
self.assertEqual(toggle_button.get_attribute("aria-label"), "Toggle navigation") self.assertEqual(toggle_button.get_attribute("aria-label"), "Toggle navigation")
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"): nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(link.get_attribute("tabIndex"), "0") self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "true")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter") self.assertTrue(nav_sidebar.is_displayed())
self.assertEqual(filter_input.get_attribute("tabIndex"), "0")
toggle_button.click() toggle_button.click()
# Hidden sidebar is not reachable via keyboard navigation.
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"): # Hidden sidebar is not visible.
self.assertEqual(link.get_attribute("tabIndex"), "-1") nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter") self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "false")
self.assertEqual(filter_input.get_attribute("tabIndex"), "-1") self.assertFalse(nav_sidebar.is_displayed())
main_element = self.selenium.find_element(By.CSS_SELECTOR, "#main") main_element = self.selenium.find_element(By.CSS_SELECTOR, "#main")
self.assertNotIn("shifted", main_element.get_attribute("class").split()) self.assertNotIn("shifted", main_element.get_attribute("class").split())
@ -189,16 +198,14 @@ class SeleniumTests(AdminSeleniumTestCase):
toggle_button = self.selenium.find_element( toggle_button = self.selenium.find_element(
By.CSS_SELECTOR, "#toggle-nav-sidebar" By.CSS_SELECTOR, "#toggle-nav-sidebar"
) )
# Hidden sidebar is not reachable via keyboard navigation. # Hidden sidebar is not visible.
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"): nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(link.get_attribute("tabIndex"), "-1") self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "false")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter") self.assertFalse(nav_sidebar.is_displayed())
self.assertEqual(filter_input.get_attribute("tabIndex"), "-1")
toggle_button.click() toggle_button.click()
for link in self.selenium.find_elements(By.CSS_SELECTOR, "#nav-sidebar a"): nav_sidebar = self.selenium.find_element(By.ID, "nav-sidebar")
self.assertEqual(link.get_attribute("tabIndex"), "0") self.assertEqual(nav_sidebar.get_attribute("aria-expanded"), "true")
filter_input = self.selenium.find_element(By.CSS_SELECTOR, "#nav-filter") self.assertTrue(nav_sidebar.is_displayed())
self.assertEqual(filter_input.get_attribute("tabIndex"), "0")
self.assertEqual( self.assertEqual(
self.selenium.execute_script( self.selenium.execute_script(
"return localStorage.getItem('django.admin.navSidebarIsOpen')" "return localStorage.getItem('django.admin.navSidebarIsOpen')"