Skip to content

Commit 15494ec

Browse files
gabalafouCarreau
andauthored
Make overlay sidebars behave like modals (#1942)
This is very similar to #1932. Closes external issue Quansight-Labs/czi-scientific-python-mgmt#84 --------- Co-authored-by: M Bussonnier <[email protected]>
1 parent feb5fc2 commit 15494ec

File tree

5 files changed

+98
-187
lines changed

5 files changed

+98
-187
lines changed

src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js

+79-84
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,29 @@ var changeSearchShortcutKey = () => {
291291
}
292292
};
293293

294+
const closeDialogOnBackdropClick = ({
295+
currentTarget: dialog,
296+
clientX,
297+
clientY,
298+
}) => {
299+
if (!dialog.open) {
300+
return;
301+
}
302+
303+
// Dialog.getBoundingClientRect() does not include ::backdrop. (This is the
304+
// trick that allows us to determine if click was inside or outside of the
305+
// dialog: click handler includes backdrop, getBoundingClientRect does not.)
306+
const { left, right, top, bottom } = dialog.getBoundingClientRect();
307+
308+
// 0, 0 means top left
309+
const clickWasOutsideDialog =
310+
clientX < left || right < clientX || clientY < top || bottom < clientY;
311+
312+
if (clickWasOutsideDialog) {
313+
dialog.close();
314+
}
315+
};
316+
294317
/**
295318
* Activate callbacks for search button popup
296319
*/
@@ -306,27 +329,7 @@ var setupSearchButtons = () => {
306329
// If user clicks outside the search modal dialog, then close it.
307330
const searchDialog = document.getElementById("pst-search-dialog");
308331
// Dialog click handler includes clicks on dialog ::backdrop.
309-
searchDialog.addEventListener("click", (event) => {
310-
if (!searchDialog.open) {
311-
return;
312-
}
313-
314-
// Dialog.getBoundingClientRect() does not include ::backdrop. (This is the
315-
// trick that allows us to determine if click was inside or outside of the
316-
// dialog: click handler includes backdrop, getBoundingClientRect does not.)
317-
const { left, right, top, bottom } = searchDialog.getBoundingClientRect();
318-
319-
// 0, 0 means top left
320-
const clickWasOutsideDialog =
321-
event.clientX < left ||
322-
right < event.clientX ||
323-
event.clientY < top ||
324-
bottom < event.clientY;
325-
326-
if (clickWasOutsideDialog) {
327-
searchDialog.close();
328-
}
329-
});
332+
searchDialog.addEventListener("click", closeDialogOnBackdropClick);
330333
};
331334

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

667670
/*******************************************************************************
668-
* Add keyboard functionality to mobile sidebars.
669-
*
670-
* Wire up the hamburger-style buttons using the click event which (on buttons)
671-
* handles both mouse clicks and the space and enter keys.
671+
* Sidebar modals (for mobile / narrow screens)
672672
*/
673673
function setupMobileSidebarKeyboardHandlers() {
674-
// These are hidden checkboxes at the top of the page whose :checked property
675-
// allows the mobile sidebars to be hidden or revealed via CSS.
676-
const primaryToggle = document.getElementById("pst-primary-sidebar-checkbox");
677-
const secondaryToggle = document.getElementById(
678-
"pst-secondary-sidebar-checkbox",
674+
// These are the left and right sidebars for wider screens. We cut and paste
675+
// the content from these widescreen sidebars into the mobile dialogs, when
676+
// the user clicks the hamburger icon button
677+
const primarySidebar = document.getElementById("pst-primary-sidebar");
678+
const secondarySidebar = document.getElementById("pst-secondary-sidebar");
679+
680+
// These are the corresponding left/right <dialog> elements, which are empty
681+
// until the user clicks the hamburger icon
682+
const primaryDialog = document.getElementById("pst-primary-sidebar-modal");
683+
const secondaryDialog = document.getElementById(
684+
"pst-secondary-sidebar-modal",
679685
);
680-
const primarySidebar = document.querySelector(".bd-sidebar-primary");
681-
const secondarySidebar = document.querySelector(".bd-sidebar-secondary");
682-
683-
// Toggle buttons -
684-
//
685-
// These are the hamburger-style buttons in the header nav bar. When the user
686-
// clicks, the button transmits the click to the hidden checkboxes used by the
687-
// CSS to control whether the sidebar is open or closed.
688-
const primaryClickTransmitter = document.querySelector(".primary-toggle");
689-
const secondaryClickTransmitter = document.querySelector(".secondary-toggle");
686+
687+
// These are the hamburger-style buttons in the header nav bar. They only
688+
// appear at narrow screen width.
689+
const primaryToggle = document.querySelector(".primary-toggle");
690+
const secondaryToggle = document.querySelector(".secondary-toggle");
691+
692+
// Cut nodes and classes from `from`, paste into/onto `to`
693+
const cutAndPasteNodesAndClasses = (from, to) => {
694+
Array.from(from.childNodes).forEach((node) => to.appendChild(node));
695+
Array.from(from.classList).forEach((cls) => {
696+
from.classList.remove(cls);
697+
to.classList.add(cls);
698+
});
699+
};
700+
701+
// Hook up the ways to open and close the dialog
690702
[
691-
[primaryClickTransmitter, primaryToggle, primarySidebar],
692-
[secondaryClickTransmitter, secondaryToggle, secondarySidebar],
693-
].forEach(([clickTransmitter, toggle, sidebar]) => {
694-
if (!clickTransmitter) {
703+
[primaryToggle, primaryDialog, primarySidebar],
704+
[secondaryToggle, secondaryDialog, secondarySidebar],
705+
].forEach(([toggleButton, dialog, sidebar]) => {
706+
if (!toggleButton || !dialog || !sidebar) {
695707
return;
696708
}
697-
clickTransmitter.addEventListener("click", (event) => {
709+
710+
// Clicking the button can only open the sidebar, not close it.
711+
// Clicking the button is also the *only* way to open the sidebar.
712+
toggleButton.addEventListener("click", (event) => {
698713
event.preventDefault();
699714
event.stopPropagation();
700-
toggle.checked = !toggle.checked;
701-
702-
// If we are opening the sidebar, move focus to the first focusable item
703-
// in the sidebar
704-
if (toggle.checked) {
705-
// Note: this selector is not exhaustive, and we may need to update it
706-
// in the future
707-
const tabStop = sidebar.querySelector("a, button");
708-
// use setTimeout because you cannot move focus synchronously during a
709-
// click in the handler for the click event
710-
setTimeout(() => tabStop.focus(), 100);
711-
}
715+
716+
// When we open the dialog, we cut and paste the nodes and classes from
717+
// the widescreen sidebar into the dialog
718+
cutAndPasteNodesAndClasses(sidebar, dialog);
719+
720+
dialog.showModal();
712721
});
713-
});
714722

715-
// Escape key -
716-
//
717-
// When sidebar is open, user should be able to press escape key to close the
718-
// sidebar.
719-
[
720-
[primarySidebar, primaryToggle, primaryClickTransmitter],
721-
[secondarySidebar, secondaryToggle, secondaryClickTransmitter],
722-
].forEach(([sidebar, toggle, transmitter]) => {
723-
if (!sidebar) {
724-
return;
725-
}
726-
sidebar.addEventListener("keydown", (event) => {
723+
// Listen for clicks on the backdrop in order to close the dialog
724+
dialog.addEventListener("click", closeDialogOnBackdropClick);
725+
726+
// We have to manually attach the escape key because there's some code in
727+
// Sphinx's Sphinx_highlight.js that prevents the default behavior of the
728+
// escape key
729+
dialog.addEventListener("keydown", (event) => {
727730
if (event.key === "Escape") {
728731
event.preventDefault();
729732
event.stopPropagation();
730-
toggle.checked = false;
731-
transmitter.focus();
733+
dialog.close();
732734
}
733735
});
734-
});
735736

736-
// When the <label> overlay is clicked to close the sidebar, return focus to
737-
// the opener button in the nav bar.
738-
[
739-
[primaryToggle, primaryClickTransmitter],
740-
[secondaryToggle, secondaryClickTransmitter],
741-
].forEach(([toggle, transmitter]) => {
742-
toggle.addEventListener("change", (event) => {
743-
if (!event.currentTarget.checked) {
744-
transmitter.focus();
745-
}
737+
// When the dialog is closed, move the nodes (and classes) back to their
738+
// original place
739+
dialog.addEventListener("close", () => {
740+
cutAndPasteNodesAndClasses(dialog, sidebar);
746741
});
747742
});
748743
}

src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss

-28
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,6 @@ $sidebar-padding-right: 1rem;
7272
margin-bottom: 0.5rem;
7373
}
7474

75-
// The dropdown toggle for extra links just shows them all instead.
76-
.nav-item.dropdown {
77-
// On mobile, the dropdown behaves like any other link, no hiding
78-
button {
79-
display: none;
80-
}
81-
82-
.dropdown-menu {
83-
display: flex;
84-
flex-direction: column;
85-
padding: 0;
86-
margin: 0;
87-
border: none;
88-
background-color: inherit;
89-
font-size: inherit;
90-
91-
.dropdown-item {
92-
&:hover,
93-
&:focus {
94-
// In the mobile sidebar, the dropdown menu is inlined with the
95-
// other links, which do not have background-color changes on hover
96-
// and focus
97-
background-color: unset;
98-
}
99-
}
100-
}
101-
}
102-
10375
.bd-navbar-elements {
10476
.nav-link {
10577
&:focus-visible {

src/pydata_sphinx_theme/assets/styles/sections/_sidebar-toggle.scss

+14-62
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,11 @@
66
* It is broken up into major sections below.
77
*/
88

9-
/*******************************************************************************
10-
* Buttons and overlays
11-
*/
12-
input.sidebar-toggle {
13-
display: none;
14-
}
15-
16-
// Background overlays
17-
label.overlay {
18-
background-color: black;
19-
opacity: 0.5;
20-
height: 0;
21-
width: 0;
22-
position: fixed;
23-
top: 0;
24-
left: 0;
25-
transition: opacity $animation-time ease-out;
26-
z-index: $zindex-modal-backdrop;
27-
}
28-
29-
input {
30-
// Show the correct overlay when its input is checked
31-
&#pst-primary-sidebar-checkbox:checked + label.overlay.overlay-primary,
32-
&#pst-secondary-sidebar-checkbox:checked + label.overlay.overlay-secondary {
33-
height: 100vh;
34-
width: 100vw;
35-
}
36-
37-
// Primary sidebar slides in from the left
38-
&#pst-primary-sidebar-checkbox:checked ~ .bd-container .bd-sidebar-primary {
39-
visibility: visible;
40-
margin-left: 0;
41-
}
42-
43-
// Secondary sidebar slides in from the right
44-
&#pst-secondary-sidebar-checkbox:checked
45-
~ .bd-container
46-
.bd-sidebar-secondary {
47-
visibility: visible;
48-
margin-right: 0;
49-
}
50-
}
51-
529
/*******************************************************************************
5310
* Sidebar drawer behavior
5411
*/
5512

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

7733
@if $side == "right" {
7834
margin-right: -75%;
@@ -83,33 +39,29 @@ input {
8339
}
8440
}
8541

86-
// Primary sidebar hides/shows at earlier widths
87-
@include media-breakpoint-up($breakpoint-sidebar-primary) {
88-
.sidebar-toggle.primary-toggle {
89-
display: none;
90-
}
91-
92-
input#pst-primary-sidebar-checkbox {
93-
&:checked + label.overlay.overlay-primary {
94-
height: 0;
95-
width: 0;
96-
}
97-
}
98-
99-
.bd-sidebar-primary {
100-
margin-left: 0;
101-
visibility: visible;
102-
}
42+
.bd-sidebar::backdrop {
43+
background-color: black;
44+
opacity: 0.5;
10345
}
10446

10547
.bd-sidebar-primary {
10648
@include media-breakpoint-down($breakpoint-sidebar-primary) {
10749
@include sliding-drawer("left");
10850
}
51+
52+
&[open] {
53+
margin-left: 0;
54+
visibility: visible;
55+
}
10956
}
11057

11158
.bd-sidebar-secondary {
11259
@include media-breakpoint-down($breakpoint-sidebar-secondary) {
11360
@include sliding-drawer("right");
11461
}
62+
63+
&[open] {
64+
margin-right: 0;
65+
visibility: visible;
66+
}
11567
}

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html

+4-12
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,6 @@
6161
</button>
6262
{%- endif %}
6363

64-
{# checkbox to toggle primary sidebar #}
65-
<input type="checkbox"
66-
class="sidebar-toggle"
67-
id="pst-primary-sidebar-checkbox"/>
68-
<label class="overlay overlay-primary" for="pst-primary-sidebar-checkbox"></label>
69-
{# Checkboxes to toggle the secondary sidebar #}
70-
<input type="checkbox"
71-
class="sidebar-toggle"
72-
id="pst-secondary-sidebar-checkbox"/>
73-
<label class="overlay overlay-secondary" for="pst-secondary-sidebar-checkbox"></label>
7464
{# A search field pop-up that will only show when the search button is clicked #}
7565
<dialog id="pst-search-dialog">
7666
{% include "../components/search-field.html" %}
@@ -91,7 +81,8 @@
9181
{% if suppress_sidebar_toctree(includehidden=theme_sidebar_includehidden | tobool) %}
9282
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
9383
{% endif %}
94-
<div class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">
84+
<dialog id="pst-primary-sidebar-modal"></dialog>
85+
<div id="pst-primary-sidebar" class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">
9586
{% include "sections/sidebar-primary.html" %}
9687
</div>
9788
{# Using an ID here so that the skip-link works #}
@@ -126,7 +117,8 @@
126117
{# Secondary sidebar #}
127118
{% block docs_toc %}
128119
{% if not remove_sidebar_secondary %}
129-
<div class="bd-sidebar-secondary bd-toc">{% include "sections/sidebar-secondary.html" %}</div>
120+
<dialog id="pst-secondary-sidebar-modal"></dialog>
121+
<div id="pst-secondary-sidebar" class="bd-sidebar-secondary bd-toc">{% include "sections/sidebar-secondary.html" %}</div>
130122
{% endif %}
131123
{% endblock docs_toc %}
132124
</div>

tests/test_build/sidebar_subpage.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="bd-sidebar-primary bd-sidebar">
1+
<div class="bd-sidebar-primary bd-sidebar" id="pst-primary-sidebar">
22
<div class="sidebar-header-items sidebar-primary__section">
33
<div class="sidebar-header-items__center">
44
<div class="navbar-item">

0 commit comments

Comments
 (0)