Fixed #34042 -- Improved accessibility of admin's navigation sidebar.
This commit is contained in:
parent
7c9d0c31d5
commit
c4aac2ac1e
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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' %}">
|
||||||
|
|
|
@ -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')"
|
||||||
|
|
Loading…
Reference in New Issue