diff --git a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js index 993aff244..dc6847aa2 100644 --- a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js +++ b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js @@ -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"); + // 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