mirror of https://github.com/django/django.git
Fixed #33726 -- Added skip-link to admin for keyboard navigation.
This commit is contained in:
parent
88dba2e3fd
commit
564437f767
|
@ -814,6 +814,20 @@ a.deletelink:focus, a.deletelink:hover {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skip-to-content-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -999px;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-to-content-link:focus {
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
padding: 20px 40px;
|
padding: 20px 40px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
|
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
|
||||||
data-admin-utc-offset="{% now "Z" %}">
|
data-admin-utc-offset="{% now "Z" %}">
|
||||||
|
<a href="#content-start" class="skip-to-content-link">{% translate 'Skip to main content' %}</a>
|
||||||
<!-- Container -->
|
<!-- Container -->
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
{% include "admin/nav_sidebar.html" %}
|
{% include "admin/nav_sidebar.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="content">
|
<div id="content-start" class="content" tabindex="-1">
|
||||||
{% block messages %}
|
{% block messages %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class="messagelist">{% for message in messages %}
|
<ul class="messagelist">{% for message in messages %}
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
from django.contrib.admin.tests import AdminSeleniumTestCase
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF="admin_views.urls")
|
||||||
|
class SeleniumTests(AdminSeleniumTestCase):
|
||||||
|
available_apps = ["admin_views"] + AdminSeleniumTestCase.available_apps
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.superuser = User.objects.create_superuser(
|
||||||
|
username="super",
|
||||||
|
password="secret",
|
||||||
|
email="super@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_use_skip_link_to_content(self):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
self.admin_login(
|
||||||
|
username="super",
|
||||||
|
password="secret",
|
||||||
|
login_url=reverse("admin:index"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# `Skip link` is not present.
|
||||||
|
skip_link = self.selenium.find_element(By.CLASS_NAME, "skip-to-content-link")
|
||||||
|
self.assertFalse(skip_link.is_displayed())
|
||||||
|
|
||||||
|
# 1st TAB is pressed, `skip link` is shown.
|
||||||
|
body = self.selenium.find_element(By.TAG_NAME, "body")
|
||||||
|
body.send_keys(Keys.TAB)
|
||||||
|
self.assertTrue(skip_link.is_displayed())
|
||||||
|
|
||||||
|
# Press RETURN to skip the navbar links (view site / documentation /
|
||||||
|
# change password / log out) and focus first model in the admin_views list.
|
||||||
|
skip_link.send_keys(Keys.RETURN)
|
||||||
|
self.assertFalse(skip_link.is_displayed()) # `skip link` disappear.
|
||||||
|
keys = [Keys.TAB, Keys.TAB] # The 1st TAB is the section title.
|
||||||
|
if self.browser == "firefox":
|
||||||
|
# For some reason Firefox doesn't focus the section title ('ADMIN_VIEWS').
|
||||||
|
keys.remove(Keys.TAB)
|
||||||
|
body.send_keys(keys)
|
||||||
|
actors_a_tag = self.selenium.find_element(By.LINK_TEXT, "Actors")
|
||||||
|
self.assertEqual(self.selenium.switch_to.active_element, actors_a_tag)
|
||||||
|
|
||||||
|
# Go to Actors changelist, skip sidebar and focus "Add actor +".
|
||||||
|
with self.wait_page_loaded():
|
||||||
|
actors_a_tag.send_keys(Keys.RETURN)
|
||||||
|
body = self.selenium.find_element(By.TAG_NAME, "body")
|
||||||
|
body.send_keys(Keys.TAB)
|
||||||
|
skip_link = self.selenium.find_element(By.CLASS_NAME, "skip-to-content-link")
|
||||||
|
self.assertTrue(skip_link.is_displayed())
|
||||||
|
ActionChains(self.selenium).send_keys(Keys.RETURN, Keys.TAB).perform()
|
||||||
|
actors_add_url = reverse("admin:admin_views_actor_add")
|
||||||
|
actors_a_tag = self.selenium.find_element(
|
||||||
|
By.CSS_SELECTOR, f"#content [href='{actors_add_url}']"
|
||||||
|
)
|
||||||
|
self.assertEqual(self.selenium.switch_to.active_element, actors_a_tag)
|
||||||
|
|
||||||
|
# Go to the Actor form and the first input will be focused automatically.
|
||||||
|
with self.wait_page_loaded():
|
||||||
|
actors_a_tag.send_keys(Keys.RETURN)
|
||||||
|
first_input = self.selenium.find_element(By.ID, "id_name")
|
||||||
|
self.assertEqual(self.selenium.switch_to.active_element, first_input)
|
||||||
|
|
||||||
|
def test_dont_use_skip_link_to_content(self):
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
self.admin_login(
|
||||||
|
username="super",
|
||||||
|
password="secret",
|
||||||
|
login_url=reverse("admin:index"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# `Skip link` is not present.
|
||||||
|
skip_link = self.selenium.find_element(By.CLASS_NAME, "skip-to-content-link")
|
||||||
|
self.assertFalse(skip_link.is_displayed())
|
||||||
|
|
||||||
|
# 1st TAB is pressed, `skip link` is shown.
|
||||||
|
body = self.selenium.find_element(By.TAG_NAME, "body")
|
||||||
|
body.send_keys(Keys.TAB)
|
||||||
|
self.assertTrue(skip_link.is_displayed())
|
||||||
|
|
||||||
|
# The 2nd TAB will focus the page title.
|
||||||
|
body.send_keys(Keys.TAB)
|
||||||
|
django_administration_title = self.selenium.find_element(
|
||||||
|
By.LINK_TEXT, "Django administration"
|
||||||
|
)
|
||||||
|
self.assertFalse(skip_link.is_displayed()) # `skip link` disappear.
|
||||||
|
self.assertEqual(
|
||||||
|
self.selenium.switch_to.active_element, django_administration_title
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_skip_link_is_skipped_when_there_is_searchbar(self):
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
|
self.admin_login(
|
||||||
|
username="super",
|
||||||
|
password="secret",
|
||||||
|
login_url=reverse("admin:index"),
|
||||||
|
)
|
||||||
|
|
||||||
|
group_a_tag = self.selenium.find_element(By.LINK_TEXT, "Groups")
|
||||||
|
with self.wait_page_loaded():
|
||||||
|
group_a_tag.click()
|
||||||
|
|
||||||
|
# `Skip link` is not present.
|
||||||
|
skip_link = self.selenium.find_element(By.CLASS_NAME, "skip-to-content-link")
|
||||||
|
self.assertFalse(skip_link.is_displayed())
|
||||||
|
|
||||||
|
# `Searchbar` has autofocus.
|
||||||
|
searchbar = self.selenium.find_element(By.ID, "searchbar")
|
||||||
|
self.assertEqual(self.selenium.switch_to.active_element, searchbar)
|
||||||
|
|
||||||
|
def test_skip_link_with_RTL_language_doesnt_create_horizontal_scrolling(self):
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
with override_settings(LANGUAGE_CODE="ar"):
|
||||||
|
self.admin_login(
|
||||||
|
username="super",
|
||||||
|
password="secret",
|
||||||
|
login_url=reverse("admin:index"),
|
||||||
|
)
|
||||||
|
|
||||||
|
skip_link = self.selenium.find_element(
|
||||||
|
By.CLASS_NAME, "skip-to-content-link"
|
||||||
|
)
|
||||||
|
body = self.selenium.find_element(By.TAG_NAME, "body")
|
||||||
|
body.send_keys(Keys.TAB)
|
||||||
|
self.assertTrue(skip_link.is_displayed())
|
||||||
|
|
||||||
|
is_vertical_scrolleable = self.selenium.execute_script(
|
||||||
|
"return arguments[0].scrollHeight > arguments[0].offsetHeight;", body
|
||||||
|
)
|
||||||
|
is_horizontal_scrolleable = self.selenium.execute_script(
|
||||||
|
"return arguments[0].scrollWeight > arguments[0].offsetWeight;", body
|
||||||
|
)
|
||||||
|
self.assertTrue(is_vertical_scrolleable)
|
||||||
|
self.assertFalse(is_horizontal_scrolleable)
|
Loading…
Reference in New Issue