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

Keyboard handlers to open and close mobile sidebars #1585

Merged
merged 8 commits into from
Jan 23, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,89 @@ if (hasVersionsJSON && (hasSwitcherMenu || wantsWarningBanner)) {
}
}

/*******************************************************************************
* Add keyboard functionality to mobile sidebars.
*
* Wire up the hamburger-style buttons using the click event which (on buttons)
* handles both mouse clicks and the space and enter keys.
*/
function setupMobileSidebarKeyboardHandlers() {
// These are hidden checkboxes at the top of the page whose :checked property
// allows the mobile sidebars to be hidden or revealed via CSS.
const primaryToggle = document.getElementById("pst-primary-sidebar-checkbox");
const secondaryToggle = document.getElementById(
"pst-secondary-sidebar-checkbox"
);
const primarySidebar = document.querySelector(".bd-sidebar-primary");
const secondarySidebar = document.querySelector(".bd-sidebar-secondary");

// Toggle buttons -
//
// These are the hamburger-style buttons in the header nav bar. When the user
// clicks, the button transmits the click to the hidden checkboxes used by the
// CSS to control whether the sidebar is open or closed.
const primaryClickTransmitter = document.querySelector(".primary-toggle");
const secondaryClickTransmitter = document.querySelector(".secondary-toggle");
[
[primaryClickTransmitter, primaryToggle, primarySidebar],
[secondaryClickTransmitter, secondaryToggle, secondarySidebar],
].forEach(([clickTransmitter, toggle, sidebar]) => {
if (!clickTransmitter) {
return;
}
clickTransmitter.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
toggle.checked = !toggle.checked;

// If we are opening the sidebar, move focus to the first focusable item
// in the sidebar
if (toggle.checked) {
// Note: this selector is not exhaustive, and we may need to update it
// in the future
const tabStop = sidebar.querySelector("a, button");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to focus on the first tab stop in the sidebar after it opens because this behavior mimics the <dialog> element.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes absolute sense, this ensures focus remains visible

// use setTimeout because you cannot move focus synchronously during a
// click in the handler for the click event
setTimeout(() => tabStop.focus(), 100);
}
});
});

// Escape key -
//
// When sidebar is open, user should be able to press escape key to close the
// sidebar.
[
[primarySidebar, primaryToggle, primaryClickTransmitter],
[secondarySidebar, secondaryToggle, secondaryClickTransmitter],
].forEach(([sidebar, toggle, transmitter]) => {
if (!sidebar) {
return;
}
sidebar.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
toggle.checked = false;
transmitter.focus();
}
});
});

// When the <label> overlay is clicked to close the sidebar, return focus to
// the opener button in the nav bar.
Comment on lines +634 to +635
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is <label> correct here or should this be button following the changes elsewhere?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah seems this refers to the label overlay in src/pydata_sphinx_theme/assets/styles/sections/_sidebar-toggle.scss and src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html so nvm

[
[primaryToggle, primaryClickTransmitter],
[secondaryToggle, secondaryClickTransmitter],
].forEach(([toggle, transmitter]) => {
toggle.addEventListener("change", (event) => {
if (!event.currentTarget.checked) {
transmitter.focus();
}
});
});
}

/*******************************************************************************
* Call functions after document loading.
*/
Expand All @@ -571,3 +654,4 @@ documentReady(scrollToActive);
documentReady(addTOCInteractivity);
documentReady(setupSearchButtons);
documentReady(initRTDObserver);
documentReady(setupMobileSidebarKeyboardHandlers);
6 changes: 4 additions & 2 deletions src/pydata_sphinx_theme/assets/styles/sections/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@

// Hide the header items on mobile
.bd-header {
// Toggle labels
label {
// Toggle buttons
button {
&.sidebar-toggle {
display: flex;
cursor: pointer;
Expand All @@ -186,6 +186,8 @@
color: var(--pst-color-muted);
margin-bottom: 0;
padding-bottom: 0.25rem;
background-color: inherit;
border: none;
}

&.primary-toggle {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,22 @@ label.overlay {

input {
// Show the correct overlay when its input is checked
&#__primary:checked + label.overlay.overlay-primary,
&#__secondary:checked + label.overlay.overlay-secondary {
&#pst-primary-sidebar-checkbox:checked + label.overlay.overlay-primary,
&#pst-secondary-sidebar-checkbox:checked + label.overlay.overlay-secondary {
Comment on lines +31 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the descriptive names here that clearly indicate component + function

height: 100vh;
width: 100vw;
}

// Primary sidebar slides in from the left
&#__primary:checked ~ .bd-container .bd-sidebar-primary {
&#pst-primary-sidebar-checkbox:checked ~ .bd-container .bd-sidebar-primary {
visibility: visible;
margin-left: 0;
}

// Secondary sidebar slides in from the right
&#__secondary:checked ~ .bd-container .bd-sidebar-secondary {
&#pst-secondary-sidebar-checkbox:checked
~ .bd-container
.bd-sidebar-secondary {
visibility: visible;
margin-right: 0;
}
Expand Down Expand Up @@ -82,11 +84,11 @@ input {

// Primary sidebar hides/shows at earlier widths
@include media-breakpoint-up($breakpoint-sidebar-primary) {
label.sidebar-toggle.primary-toggle {
.sidebar-toggle.primary-toggle {
display: none;
}

input#__primary {
input#pst-primary-sidebar-checkbox {
&:checked + label.overlay.overlay-primary {
height: 0;
width: 0;
Expand Down
10 changes: 4 additions & 6 deletions src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@
{# checkbox to toggle primary sidebar #}
<input type="checkbox"
class="sidebar-toggle"
name="__primary"
id="__primary"/>
<label class="overlay overlay-primary" for="__primary"></label>
id="pst-primary-sidebar-checkbox"/>
<label class="overlay overlay-primary" for="pst-primary-sidebar-checkbox"></label>
{# Checkboxes to toggle the secondary sidebar #}
<input type="checkbox"
class="sidebar-toggle"
name="__secondary"
id="__secondary"/>
<label class="overlay overlay-secondary" for="__secondary"></label>
id="pst-secondary-sidebar-checkbox"/>
<label class="overlay overlay-secondary" for="pst-secondary-sidebar-checkbox"></label>
{# A search field pop-up that will only show when the search button is clicked #}
<div class="search-button__wrapper">
<div class="search-button__overlay"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{% if theme_navbar_start or theme_navbar_center or theme_navbar_end or theme_navbar_persistent %}
<div class="bd-header__inner bd-page-width">
<label class="sidebar-toggle primary-toggle" for="__primary">
<button class="sidebar-toggle primary-toggle" aria-label="{{ _('Site navigation') }}">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose this ARIA label and the one below because these strings are already in the translation system.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay! we have real buttons now

<span class="fa-solid fa-bars"></span>
</label>
</button>
{% set navbar_start, navbar_class, navbar_align = navbar_align_class() %}
{% if theme_navbar_start %}
<div class="{{ navbar_start }} navbar-header-items__start">
Expand Down Expand Up @@ -40,9 +40,9 @@
{% endfor %}

{% if not remove_sidebar_secondary %}
<label class="sidebar-toggle secondary-toggle" for="__secondary" tabindex="0">
<button class="sidebar-toggle secondary-toggle" aria-label="{{ _('On this page') }}">
<span class="fa-solid fa-outdent"></span>
</label>
</button>
{% endif %}
</div>
{% endif %}
Loading