Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Collapsible sidebar #2159

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 5 additions & 3 deletions docs/user_guide/layout.rst
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,15 @@ By default, it has the following configuration:
.. code-block:: python

html_sidebars = {
"**": ["sidebar-nav-bs", "sidebar-ethical-ads"]
"**": ["sidebar-collapse", "sidebar-nav-bs"]
}

- ``sidebar-collapse.html`` - a button that allows users to expand and collapse the sidebar.

- ``sidebar-nav-bs.html`` - a bootstrap-friendly navigation section.

When there are no pages to show, it will disappear and potentially add extra space for your page's content.

- ``sidebar-ethical-ads.html`` - a placement for ReadTheDocs's Ethical Ads (will only show up on ReadTheDocs).

Primary sidebar end sections
----------------------------

Expand All @@ -382,6 +382,8 @@ By default, it has the following templates:
# ...
}

`sidebar-ethical-ads.html`` is a placement for ReadTheDocs's Ethical Ads (will only show up on ReadTheDocs).

Remove the primary sidebar from pages
-------------------------------------

Expand Down
122 changes: 122 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 @@ -1011,6 +1011,127 @@ async function fetchRevealBannersTogether() {
}, 320);
}

/*******************************************************************************
* Set up expand/collapse button for primary sidebar
*/
function setupCollapseSidebarButton() {
const button = document.getElementById("pst-collapse-sidebar-button");
const sidebar = document.getElementById("pst-primary-sidebar");

// If this page rendered without the button or sidebar, then there's nothing to do.
if (!button || !sidebar) {
return;
}

const sidebarSections = Array.from(sidebar.children);

const expandTooltip = new bootstrap.Tooltip(button, {
title: button.querySelector(".pst-expand-sidebar-label").textContent,

// In manual testing, relying on Bootstrap to handle "hover" and "focus" was buggy.
trigger: "manual",

placement: "left",
fallbackPlacements: ["right"],

// Offsetting the tooltip a bit more than the default [0, 0] solves an issue
// where the appearance of the tooltip triggers a mouseleave event which in
// turn triggers the call to hide the tooltip. So in certain areas around
// the button, it would appear to the user that tooltip flashes in and then
// back out.
offset: [0, 12],
});

const showTooltip = () => {
// Only show the "expand sidebar" tooltip when the sidebar is not expanded
if (button.getAttribute("aria-expanded") === "false") {
expandTooltip.show();
}
};
const hideTooltip = () => {
expandTooltip.hide();
};

function squeezeSidebar(prefersReducedMotion, done) {
// Before squeezing the sidebar, freeze the widths of its subsections.
// Otherwise, the subsections will also narrow and cause the text in the
// sidebar to reflow and wrap, which we don't want. This is necessary
// because we do not remove the sidebar contents from the layout (with
// `display: none`). Rather, we hide the contents from both sighted users
// and screen readers (with `visibility: hidden`). This provides better
// stability to the overall layout.
sidebarSections.forEach(
(el) => (el.style.width = el.getBoundingClientRect().width + "px"),
);

const afterSqueeze = () => {
// After squeezing the sidebar, set aria-expanded to false
button.setAttribute("aria-expanded", "false"); // "false" is in quotes because HTML attributes are strings

button.dataset.busy = false;
};

if (prefersReducedMotion) {
sidebar.classList.add("pst-squeeze");
afterSqueeze();
} else {
sidebar.addEventListener("transitionend", function onTransitionEnd() {
afterSqueeze();
sidebar.removeEventListener("transitionend", onTransitionEnd);
});
sidebar.classList.add("pst-squeeze");
}
}

function expandSidebar(prefersReducedMotion, done) {
hideTooltip();

const afterExpand = () => {
// After expanding the sidebar (which may be delayed by a CSS transition),
// unfreeze the widths of the subsections that were frozen when the sidebar
// was squeezed.
sidebarSections.forEach((el) => (el.style.width = null));

// After expanding the sidebar, set aria-expanded to "true" - in quotes
// because HTML attributes are strings.
button.setAttribute("aria-expanded", "true");

button.dataset.busy = false;
};

if (prefersReducedMotion) {
sidebar.classList.remove("pst-squeeze");
afterExpand();
} else {
sidebar.addEventListener("transitionend", function onTransitionEnd() {
afterExpand();
sidebar.removeEventListener("transitionend", onTransitionEnd);
});
sidebar.classList.remove("pst-squeeze");
}
}

button.addEventListener("click", () => {
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion)", // must be in parentheses
).matches;
if (button.dataset.busy === "true") {
return;
}
button.dataset.busy = "true";
if (button.getAttribute("aria-expanded") === "true") {
squeezeSidebar(prefersReducedMotion);
} else {
expandSidebar(prefersReducedMotion);
}
});

button.addEventListener("focus", showTooltip);
button.addEventListener("mouseenter", showTooltip);
button.addEventListener("mouseleave", hideTooltip);
button.addEventListener("blur", hideTooltip);
}

/*******************************************************************************
* Call functions after document loading.
*/
Expand All @@ -1025,6 +1146,7 @@ documentReady(addTOCInteractivity);
documentReady(setupSearchButtons);
documentReady(setupSearchAsYouType);
documentReady(setupMobileSidebarKeyboardHandlers);
documentReady(setupCollapseSidebarButton);

// Determining whether an element has scrollable content depends on stylesheets,
// so we're checking for the "load" event rather than "DOMContentLoaded"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* The collapse/expand primary sidebar button
*/

.bd-sidebar-primary {
.sidebar-primary-item.pst-sidebar-collapse {
padding-top: 0;
}

#pst-collapse-sidebar-button {
// Only show this button when there's enough width for both sidebar and main
// content. Do not show the button in the mobile menu where it would not
// make any sense.
@include media-breakpoint-down($breakpoint-sidebar-primary) {
display: none;
}

border: 0; // reset
padding: 0; // reset
text-align: start; // reset;
background-color: transparent;
outline-offset: $focus-ring-offset;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;

// min width and height of the button must be 24px to meet WCAG Success Criterion 2.5.8
min-width: 24px;
min-height: 24px;
position: relative;
bottom: 0.2em;

.pst-icon {
color: var(--pst-color-link);

// The padding value was chosen by trial and error. For reference, the
// svg-inline--fa class normally applies a -.125em adjustment but this
// adjustment doesn't work when the icon is within a flex box. Important:
// the padding top value must match the padding bottom value because the
// icon undergoes a 180deg rotation when the sidebar is collapsed.
padding: 0.4em 0;
height: 1.4em;
}

.pst-collapse-sidebar-label {
// // inline-flex so we can set dimensions (width, height)
// display: inline-flex;
width: 100%;
height: 100%;

@include link-style-default;
}

.pst-expand-sidebar-label {
// inline-flex so we can set dimensions (width, height)
// display: inline-flex;

// When the sidebar is squeezed, there is no space to show the "expand
// sidebar" label. However, the label text is copied to a Bootstrap
// tooltip. It's also exposed to screen readers with this
// `visually-hidden` mixin from Bootstrap.
@include visually-hidden;

// Turn off for screen readers initially because the sidebar starts off in the expanded state.
// When the
visibility: hidden;
}

&:hover {
.pst-icon {
color: var(--pst-color-link-hover);
}

.pst-collapse-sidebar-label {
@include link-style-hover;
}
}
}

// Define transitions (if the environment permits animation)
@media (prefers-reduced-motion: no-preference) {
$duration: 400ms;

transition: width $duration ease;

#pst-collapse-sidebar-button {
.pst-icon {
transition:
transform $duration ease,
padding $duration ease;
}
}

@each $selector,
$delay
in (
// When the sidebar is collapsing, we need to delay the transition of
// properties that make the elements invisible so the user can see the
// opacity transition from 1 to 0 first.
".pst-squeeze": $duration,
// When the sidebar is expanding, it's the opposite: we need to transition
// the properties that make the elements visible immediately so the user
// can watch the opacity transition from 0 to 1.
":not(.pst-squeeze)": "0s"
)
{
&#{$selector} {
.pst-collapse-sidebar-label {
transition:
opacity $duration linear,
visibility 0s linear $delay,
width 0s linear $delay,
height 0s linear $delay;
}

.sidebar-primary-item:not(.pst-sidebar-collapse) {
transition:
opacity $duration linear,
visibility 0s linear $delay;
}

// There is no need to transition any other properties on the expand
// label (width, height, opacity) because it is always visually hidden
// (i.e., width 0, height 0, etc), but toggles its availability to
// screen readers as the sidebar collapses or expands via the
// `visibility` property.
.pst-expand-sidebar-label {
transition: visibility 0s linear $delay;
}
}
}
}

// Why "squeeze" and not "collapse"? Bootstrap uses the class name `collapse`
// so it seemed best to avoid possible confusion. (Later the class name was
// prefixed with `pst-`.)
&.pst-squeeze {
width: 4rem;
overflow: hidden;

#pst-collapse-sidebar-button {
.pst-icon {
transform: translateX(0.25em) rotate(180deg);
}

.pst-collapse-sidebar-label {
opacity: 0;
visibility: hidden;
width: 0;
height: 0;
}

.pst-expand-sidebar-label {
visibility: visible;
}
}

.sidebar-primary-item:not(.pst-sidebar-collapse) {
opacity: 0;
visibility: hidden;
}
}

&:not(.pst-squeeze) {
// When the sidebar is expanded, hide the "Expand Sidebar" label.
.pst-expand-sidebar-label {
visibility: hidden;
}

// This block is shorter than its counterpart above because there's no need,
// for example, to explicitly set `visibility: visible` or `opacity: 1` on
// the collapse label and sidebar subsections because those are the default
// values.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
@import "./components/navbar-links";
@import "./components/page-toc";
@import "./components/prev-next";
@import "./components/sidebar-collapse";
@import "./components/search";
@import "./components/searchbox";
@import "./components/switcher-theme";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ $sidebar-padding-right: 1rem;
@include make-col(3);

// Borders padding and whitespace
padding: 2rem $sidebar-padding-right 1rem 1rem;
padding: $sidebar-padding-right;
border-right: 1px solid var(--pst-color-border);
background-color: var(--pst-color-background);
overflow-y: auto;
overflow: hidden auto;
font-size: var(--pst-sidebar-font-size-mobile);

@include media-breakpoint-up($breakpoint-sidebar-primary) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<button id="pst-collapse-sidebar-button" aria-expanded="true" aria-controls="pst-primary-sidebar">
{#- Even though this SVG is not a Font Awesome SVG, we use the `svg-inline--fa` class to apply some useful styles -#}
<svg class="pst-icon svg-inline--fa" role="img" aria-hidden="true" focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M3 15.5C2.36232 15.5 1.74874 15.2564 1.28478 14.8189C0.820828 14.3815 0.541576 13.7832 0.504167 13.1467L0.5 13L0.5 3C0.499965 2.36232 0.743605 1.74874 1.18107 1.28478C1.61854 0.820828 2.21676 0.541576 2.85333 0.504167L3 0.5L13 0.5C13.6377 0.499965 14.2513 0.743605 14.7152 1.18107C15.1792 1.61854 15.4584 2.21676 15.4958 2.85333L15.5 3L15.5 13C15.5 13.6377 15.2564 14.2513 14.8189 14.7152C14.3815 15.1792 13.7832 15.4584 13.1467 15.4958L13 15.5L3 15.5ZM3 13.8333L10.5 13.8333L10.5 2.16667L3 2.16667C2.79589 2.16669 2.59889 2.24163 2.44636 2.37726C2.29383 2.5129 2.19638 2.69979 2.1725 2.9025L2.16667 3L2.16667 13C2.16669 13.2041 2.24163 13.4011 2.37726 13.5536C2.5129 13.7062 2.69979 13.8036 2.9025 13.8275L3 13.8333ZM6.65583 10.325L6.5775 10.2558L4.91083 8.58917C4.76735 8.44567 4.68116 8.25476 4.66843 8.05223C4.65569 7.84971 4.71729 7.6495 4.84167 7.48917L4.91083 7.41083L6.5775 5.74417C6.72747 5.59471 6.9287 5.50794 7.14032 5.50148C7.35194 5.49502 7.55809 5.56935 7.7169 5.70937C7.8757 5.8494 7.97525 6.04463 7.99533 6.25539C8.01541 6.46616 7.95451 6.67667 7.825 6.84417L7.75583 6.9225L6.67917 8L7.75583 9.0775C7.89931 9.22099 7.98551 9.41191 7.99824 9.61443C8.01097 9.81695 7.94938 10.0172 7.825 10.1775L7.75583 10.2558C7.61234 10.3993 7.42142 10.4855 7.2189 10.4982C7.01638 10.511 6.81617 10.4494 6.65583 10.325Z"/>
</svg>
<span class="pst-collapse-sidebar-label">{{ _("Collapse Sidebar") }}</span>
<span class="pst-expand-sidebar-label">
{#- Careful: the contents of this span element are read by JavaScript and passed to the Bootstrap tooltip.
If you modify the markup within this tag, be sure that the tooltip on the expand sidebar button still works
and doesn't have any extraneous whitespace. -#}
{{- _("Expand Sidebar") -}}
</span>
</button>
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
{# Primary sidebar #}
{# If we have no sidebar TOC, pop the TOC component from the sidebars list #}
{% if suppress_sidebar_toctree(includehidden=theme_sidebar_includehidden | tobool) %}
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
{% set sidebars = sidebars | reject("in", ["sidebar-collapse.html", "sidebar-nav-bs.html"]) | list %}
{% 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 %}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{% if sidebars %}
<div class="sidebar-primary-items__start sidebar-primary__section">
{%- for sidebartemplate in sidebars %}
<div class="sidebar-primary-item">{%- include sidebartemplate %}</div>
<div class="sidebar-primary-item{% if sidebartemplate == 'sidebar-collapse.html' %} pst-sidebar-collapse{% endif %}">{%- include sidebartemplate %}</div>
{%- endfor %}
</div>
{% endif %}
Expand Down
Loading
Loading