Skip to content

Commit

Permalink
Make overlay sidebars behave like modals (#1942)
Browse files Browse the repository at this point in the history
This is very similar to #1932.

Closes external issue
Quansight-Labs/czi-scientific-python-mgmt#84

---------

Co-authored-by: M Bussonnier <[email protected]>
  • Loading branch information
gabalafou and Carreau committed Aug 29, 2024
1 parent feb5fc2 commit 15494ec
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 187 deletions.
163 changes: 79 additions & 84 deletions src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,29 @@ var changeSearchShortcutKey = () => {
}
};

const closeDialogOnBackdropClick = ({
currentTarget: dialog,
clientX,
clientY,
}) => {
if (!dialog.open) {
return;
}

// Dialog.getBoundingClientRect() does not include ::backdrop. (This is the
// trick that allows us to determine if click was inside or outside of the
// dialog: click handler includes backdrop, getBoundingClientRect does not.)
const { left, right, top, bottom } = dialog.getBoundingClientRect();

// 0, 0 means top left
const clickWasOutsideDialog =
clientX < left || right < clientX || clientY < top || bottom < clientY;

if (clickWasOutsideDialog) {
dialog.close();
}
};

/**
* Activate callbacks for search button popup
*/
Expand All @@ -306,27 +329,7 @@ var setupSearchButtons = () => {
// If user clicks outside the search modal dialog, then close it.
const searchDialog = document.getElementById("pst-search-dialog");
// Dialog click handler includes clicks on dialog ::backdrop.
searchDialog.addEventListener("click", (event) => {
if (!searchDialog.open) {
return;
}

// Dialog.getBoundingClientRect() does not include ::backdrop. (This is the
// trick that allows us to determine if click was inside or outside of the
// dialog: click handler includes backdrop, getBoundingClientRect does not.)
const { left, right, top, bottom } = searchDialog.getBoundingClientRect();

// 0, 0 means top left
const clickWasOutsideDialog =
event.clientX < left ||
right < event.clientX ||
event.clientY < top ||
bottom < event.clientY;

if (clickWasOutsideDialog) {
searchDialog.close();
}
});
searchDialog.addEventListener("click", closeDialogOnBackdropClick);
};

/*******************************************************************************
Expand Down Expand Up @@ -535,7 +538,7 @@ function showVersionWarningBanner(data) {
const versionsAreComparable = validate(version) && validate(preferredVersion);
if (versionsAreComparable && compare(version, preferredVersion, "=")) {
console.log(
"This is the prefered version of the docs, not showing the warning banner.",
"[PST]: This is the preferred version of the docs, not showing the warning banner.",
);
return;
}
Expand Down Expand Up @@ -665,84 +668,76 @@ async function fetchAndUseVersions() {
}

/*******************************************************************************
* 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.
* Sidebar modals (for mobile / narrow screens)
*/
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",
// These are the left and right sidebars for wider screens. We cut and paste
// the content from these widescreen sidebars into the mobile dialogs, when
// the user clicks the hamburger icon button
const primarySidebar = document.getElementById("pst-primary-sidebar");
const secondarySidebar = document.getElementById("pst-secondary-sidebar");

// These are the corresponding left/right <dialog> elements, which are empty
// until the user clicks the hamburger icon
const primaryDialog = document.getElementById("pst-primary-sidebar-modal");
const secondaryDialog = document.getElementById(
"pst-secondary-sidebar-modal",
);
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");

// These are the hamburger-style buttons in the header nav bar. They only
// appear at narrow screen width.
const primaryToggle = document.querySelector(".primary-toggle");
const secondaryToggle = document.querySelector(".secondary-toggle");

// Cut nodes and classes from `from`, paste into/onto `to`
const cutAndPasteNodesAndClasses = (from, to) => {
Array.from(from.childNodes).forEach((node) => to.appendChild(node));
Array.from(from.classList).forEach((cls) => {
from.classList.remove(cls);
to.classList.add(cls);
});
};

// Hook up the ways to open and close the dialog
[
[primaryClickTransmitter, primaryToggle, primarySidebar],
[secondaryClickTransmitter, secondaryToggle, secondarySidebar],
].forEach(([clickTransmitter, toggle, sidebar]) => {
if (!clickTransmitter) {
[primaryToggle, primaryDialog, primarySidebar],
[secondaryToggle, secondaryDialog, secondarySidebar],
].forEach(([toggleButton, dialog, sidebar]) => {
if (!toggleButton || !dialog || !sidebar) {
return;
}
clickTransmitter.addEventListener("click", (event) => {

// Clicking the button can only open the sidebar, not close it.
// Clicking the button is also the *only* way to open the sidebar.
toggleButton.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);
}

// When we open the dialog, we cut and paste the nodes and classes from
// the widescreen sidebar into the dialog
cutAndPasteNodesAndClasses(sidebar, dialog);

dialog.showModal();
});
});

// 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) => {
// Listen for clicks on the backdrop in order to close the dialog
dialog.addEventListener("click", closeDialogOnBackdropClick);

// We have to manually attach the escape key because there's some code in
// Sphinx's Sphinx_highlight.js that prevents the default behavior of the
// escape key
dialog.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
toggle.checked = false;
transmitter.focus();
dialog.close();
}
});
});

// When the <label> overlay is clicked to close the sidebar, return focus to
// the opener button in the nav bar.
[
[primaryToggle, primaryClickTransmitter],
[secondaryToggle, secondaryClickTransmitter],
].forEach(([toggle, transmitter]) => {
toggle.addEventListener("change", (event) => {
if (!event.currentTarget.checked) {
transmitter.focus();
}
// When the dialog is closed, move the nodes (and classes) back to their
// original place
dialog.addEventListener("close", () => {
cutAndPasteNodesAndClasses(dialog, sidebar);
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,34 +72,6 @@ $sidebar-padding-right: 1rem;
margin-bottom: 0.5rem;
}

// The dropdown toggle for extra links just shows them all instead.
.nav-item.dropdown {
// On mobile, the dropdown behaves like any other link, no hiding
button {
display: none;
}

.dropdown-menu {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
border: none;
background-color: inherit;
font-size: inherit;

.dropdown-item {
&:hover,
&:focus {
// In the mobile sidebar, the dropdown menu is inlined with the
// other links, which do not have background-color changes on hover
// and focus
background-color: unset;
}
}
}
}

.bd-navbar-elements {
.nav-link {
&:focus-visible {
Expand Down
76 changes: 14 additions & 62 deletions src/pydata_sphinx_theme/assets/styles/sections/_sidebar-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,11 @@
* It is broken up into major sections below.
*/

/*******************************************************************************
* Buttons and overlays
*/
input.sidebar-toggle {
display: none;
}

// Background overlays
label.overlay {
background-color: black;
opacity: 0.5;
height: 0;
width: 0;
position: fixed;
top: 0;
left: 0;
transition: opacity $animation-time ease-out;
z-index: $zindex-modal-backdrop;
}

input {
// Show the correct overlay when its input is checked
&#pst-primary-sidebar-checkbox:checked + label.overlay.overlay-primary,
&#pst-secondary-sidebar-checkbox:checked + label.overlay.overlay-secondary {
height: 100vh;
width: 100vw;
}

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

// Secondary sidebar slides in from the right
&#pst-secondary-sidebar-checkbox:checked
~ .bd-container
.bd-sidebar-secondary {
visibility: visible;
margin-right: 0;
}
}

/*******************************************************************************
* Sidebar drawer behavior
*/

/**
* Behavior for sliding drawer elements that will be toggled with an input
*
* NOTE: We use this mixin to define the toggle behavior on narrow screens,
* And the wide-screen behavior of the sections is defined in their own section
* .scss files.
Expand All @@ -73,6 +28,7 @@ input {
visibility $animation-time ease-out,
margin $animation-time ease-out;
visibility: hidden;
border: 0;

@if $side == "right" {
margin-right: -75%;
Expand All @@ -83,33 +39,29 @@ input {
}
}

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

input#pst-primary-sidebar-checkbox {
&:checked + label.overlay.overlay-primary {
height: 0;
width: 0;
}
}

.bd-sidebar-primary {
margin-left: 0;
visibility: visible;
}
.bd-sidebar::backdrop {
background-color: black;
opacity: 0.5;
}

.bd-sidebar-primary {
@include media-breakpoint-down($breakpoint-sidebar-primary) {
@include sliding-drawer("left");
}

&[open] {
margin-left: 0;
visibility: visible;
}
}

.bd-sidebar-secondary {
@include media-breakpoint-down($breakpoint-sidebar-secondary) {
@include sliding-drawer("right");
}

&[open] {
margin-right: 0;
visibility: visible;
}
}
16 changes: 4 additions & 12 deletions src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,6 @@
</button>
{%- endif %}

{# checkbox to toggle primary sidebar #}
<input type="checkbox"
class="sidebar-toggle"
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"
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 #}
<dialog id="pst-search-dialog">
{% include "../components/search-field.html" %}
Expand All @@ -91,7 +81,8 @@
{% if suppress_sidebar_toctree(includehidden=theme_sidebar_includehidden | tobool) %}
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
{% endif %}
<div class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">
<dialog id="pst-primary-sidebar-modal"></dialog>
<div id="pst-primary-sidebar" class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">
{% include "sections/sidebar-primary.html" %}
</div>
{# Using an ID here so that the skip-link works #}
Expand Down Expand Up @@ -126,7 +117,8 @@
{# Secondary sidebar #}
{% block docs_toc %}
{% if not remove_sidebar_secondary %}
<div class="bd-sidebar-secondary bd-toc">{% include "sections/sidebar-secondary.html" %}</div>
<dialog id="pst-secondary-sidebar-modal"></dialog>
<div id="pst-secondary-sidebar" class="bd-sidebar-secondary bd-toc">{% include "sections/sidebar-secondary.html" %}</div>
{% endif %}
{% endblock docs_toc %}
</div>
Expand Down
2 changes: 1 addition & 1 deletion tests/test_build/sidebar_subpage.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="bd-sidebar-primary bd-sidebar">
<div class="bd-sidebar-primary bd-sidebar" id="pst-primary-sidebar">
<div class="sidebar-header-items sidebar-primary__section">
<div class="sidebar-header-items__center">
<div class="navbar-item">
Expand Down

0 comments on commit 15494ec

Please sign in to comment.