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

Accessibility: Keyboard Navigation #660

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
76 changes: 76 additions & 0 deletions src/furo/assets/scripts/furo.js
Expand Up @@ -154,10 +154,86 @@ function setupTheme() {
});
}

////////////////////////////////////////////////////////////////////////////////
// Keyboard Accessibility
////////////////////////////////////////////////////////////////////////////////

// Determines which of the two table-of-contents menu labels is visible.
function determineVisibleTocOpenMenu() {
const mediaQuery = window.matchMedia("(max-width: 67em)");
return mediaQuery.matches ? "toc-menu-open-sm" : "toc-menu-open-md";
}

// A mapping of current to next focus id's. For example, We want a corresponsing
// menu's close button to be highlighted after a menu is opened with a keyboard.
const NEXT_FOCUS_ID_MAP = {
"nav-menu-open": "nav-menu-close",
"nav-menu-close": "nav-menu-open",
"toc-menu-open-sm": "toc-menu-close",
"toc-menu-open-md": "toc-menu-close",
"toc-menu-close": determineVisibleTocOpenMenu(),
};

// Toggles the visibility of a sidebar menu to prevent keyboard focus on hidden elements.
function toggleSidebarMenuVisibility(elementQuery, inputQuery) {
const sidebarElement = document.querySelector(elementQuery);
const sidebarInput = document.querySelector(inputQuery);
sidebarInput.addEventListener("change", () => {
setTimeout(
() => {
sidebarElement.classList.toggle("hide-sidebar", !sidebarInput.checked);
},
sidebarInput.checked ? 0 : 250,
);
});
window.matchMedia("(max-width: 67em)").addEventListener("change", (event) => {
NEXT_FOCUS_ID_MAP["toc-menu-close"] = determineVisibleTocOpenMenu();
if (!event.matches) {
document
.querySelector(".sidebar-drawer")
.classList.remove("hide-sidebar");
}
});
window.matchMedia("(max-width: 82em)").addEventListener("change", (event) => {
if (!event.matches) {
document.querySelector(".toc-drawer").classList.remove("hide-sidebar");
}
});
}

// Activates labels when a user focuses on them and clicks "Enter".
// Also highlights the next appropriate input label.
function activateLabelOnEnter() {
const labels = document.querySelectorAll("label");
labels.forEach((element) => {
element.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
const targetId = element.getAttribute("for");
document.getElementById(targetId).click();
const nextFocusId = NEXT_FOCUS_ID_MAP[element.id];
if (nextFocusId) {
// Timeout is needed to let the label become visible.
setTimeout(() => {
document.getElementById(nextFocusId).focus();
}, 250);
}
}
});
});
}

// Improves accessibility for keyboard-only users.
function setupKeyboardFriendlyNavigation() {
activateLabelOnEnter();
toggleSidebarMenuVisibility(".toc-drawer", "#__toc");
toggleSidebarMenuVisibility(".sidebar-drawer", "#__navigation");
}

function setup() {
setupTheme();
setupScrollHandler();
setupScrollSpy();
setupKeyboardFriendlyNavigation();
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
29 changes: 28 additions & 1 deletion src/furo/assets/styles/_scaffold.sass
Expand Up @@ -81,6 +81,18 @@ article
display: flex
flex: 1

.justify-content-left
justify-content: left

.justify-content-right
justify-content: right

.show-div-sm
display: none

.show-div-md
display: none

// Sidebar (left) also covers the entire left portion of screen.
.sidebar-drawer
box-sizing: border-box
Expand Down Expand Up @@ -123,6 +135,13 @@ article
overflow: auto
scroll-behavior: smooth

.sidebar-div
margin: var(--sidebar-caption-space-above) 0 0 0
padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)

.hide-sidebar
visibility: hidden

// Central items.
.content
padding: 0 $content-padding
Expand Down Expand Up @@ -207,11 +226,15 @@ article
height: 1rem
width: 1rem

.toc-header-icon, .nav-overlay-icon
.toc-header-icon, .nav-overlay-icon, nav-close-icon
// for when we set display: flex
justify-content: center
align-items: center

.nav-close-icon
display: flex
cursor: pointer

.toc-content-icon
height: 1.5rem
width: 1.5rem
Expand Down Expand Up @@ -339,6 +362,8 @@ article
.toc-tree
border-left: none
font-size: var(--toc-font-size--mobile)
.show-div-md
display: flex

// Accomodate for a changed content width.
.sidebar-drawer
Expand All @@ -355,6 +380,8 @@ article

top: 0
left: -$sidebar-width
.show-div-sm
display: flex

// Swap which icon is visible.
.toc-header-icon
Expand Down
1 change: 1 addition & 0 deletions src/furo/navigation.py
Expand Up @@ -52,6 +52,7 @@ def get_navigation_tree(toctree_html: str) -> str:
"label",
attrs={
"for": checkbox_name,
"tabindex": "0",
},
)
screen_reader_label = soup.new_tag(
Expand Down
12 changes: 9 additions & 3 deletions src/furo/theme/furo/page.html
Expand Up @@ -24,7 +24,7 @@
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<label class="nav-overlay-icon" for="__navigation" id="nav-menu-open" tabindex="0">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
Expand All @@ -41,7 +41,7 @@
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc">
<label class="toc-overlay-icon toc-header-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc" id="toc-menu-open-sm" tabindex="0">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
Expand Down Expand Up @@ -82,7 +82,7 @@
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc">
<label class="toc-overlay-icon toc-content-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc" id="toc-menu-open-md" tabindex="0">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
Expand Down Expand Up @@ -190,6 +190,12 @@
{% block right_sidebar %}
{% if not furo_hide_toc %}
<div class="toc-sticky toc-scroll">
<div class="sidebar-div show-div-md justify-content-left">
<label class="nav-close-icon" id="toc-menu-close" for="__toc" tabindex="0">
<div class="visually-hidden">Toggle site table of content right sidebar</div>
<i class="icon"><svg><use href="#svg-close"></use></svg></i>
</label>
</div>
<div class="toc-title-container">
<span class="toc-title">
{{ _("On this page") }}
Expand Down
8 changes: 8 additions & 0 deletions src/furo/theme/furo/partials/icons.html
Expand Up @@ -58,4 +58,12 @@
<path d="M13 6h1" />
</svg>
</symbol>
<symbol id="svg-close" viewBox="0 0 24 24">
<title>Close Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="20" x2="20" y2="4"/>
<line x1="4" y1="4" x2="20" y2="20"/>
</svg>
</symbol>
</svg>
6 changes: 6 additions & 0 deletions src/furo/theme/furo/sidebar/close-icon.html
@@ -0,0 +1,6 @@
<div class="sidebar-div show-div-sm justify-content-right">
<label class="nav-close-icon" id="nav-menu-close" for="__navigation" tabindex="0">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-close"></use></svg></i>
</label>
</div>
1 change: 1 addition & 0 deletions src/furo/theme/furo/theme.conf
Expand Up @@ -4,6 +4,7 @@ stylesheet = styles/furo.css
pygments_style = tango
# sidebar-start
sidebars =
sidebar/close-icon.html,
sidebar/brand.html,
sidebar/search.html,
sidebar/scroll-start.html,
Expand Down