From 7c9cb67941eb0c2dcac03fa3aac2fbb0330f9b8d Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 20 Aug 2024 05:39:21 -0700 Subject: [PATCH 1/8] Bump minimum sphinx version. (#1943) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 64bb12b55..b8b595337 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dynamic = ["version"] readme = "README.md" requires-python = ">=3.9" dependencies = [ - "sphinx>=5", + "sphinx>=6.1", "beautifulsoup4", "docutils!=0.17.0", "Babel", From feb5fc21034319b61f7dcb59638945398e1f96ba Mon Sep 17 00:00:00 2001 From: gabalafou Date: Wed, 28 Aug 2024 05:22:37 -0400 Subject: [PATCH 2/8] Increase horizontal whitespace between navbar icon links (#1964) Fixes external issue: https://github.com/Quansight-Labs/czi-scientific-python-mgmt/issues/81 This PR increases the horizontal gap between the navbar icons slightly (1rem to 1.12rem) to align better with accessibility guidelines. The idea is to make sure that the circle targets (pink) do not overlap. It would be nice if we could rework the navbar icon links to have a known hit area at build time (hit area being the union of width, height, and padding). That way we could calculate the [needed horizontal and vertical margins](https://github.com/Quansight-Labs/czi-scientific-python-mgmt/issues/81#issuecomment-2251325783) with SCSS, like so: ```scss @mixin enclosing-circle-margin($hit-area-width, $hit-area-height) { // the diagonal of the hit area rectangle is also the diameter of the minimum enclosing circle $hit-area-diagonal: math.hypot($hit-area-width, $hit-area-height); $mx: ($hit-area-diagonal - $hit-area-width) / 2; $my: ($hit-area-diagonal - $hit-area-height) / 2; margin: $my $mx; } ``` But that will have to be for another time. --- .../assets/styles/components/_icon-links.scss | 2 +- src/pydata_sphinx_theme/assets/styles/sections/_header.scss | 2 +- src/pydata_sphinx_theme/assets/styles/variables/_layout.scss | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss b/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss index 8edba3fdb..bb4334c09 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss @@ -27,7 +27,7 @@ ul.navbar-icon-links { display: flex; flex-flow: row wrap; - column-gap: 1rem; + column-gap: $nav-icon-column-gap; justify-content: space-evenly; align-items: center; padding-left: 0; diff --git a/src/pydata_sphinx_theme/assets/styles/sections/_header.scss b/src/pydata_sphinx_theme/assets/styles/sections/_header.scss index b2c969ab1..e57d59b56 100644 --- a/src/pydata_sphinx_theme/assets/styles/sections/_header.scss +++ b/src/pydata_sphinx_theme/assets/styles/sections/_header.scss @@ -65,7 +65,7 @@ .navbar-header-items__end, .navbar-header-items__center { - column-gap: 1rem; + column-gap: $nav-icon-column-gap; } // A little smaller because this is displayed by default on mobile diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss b/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss index eaae298a2..571403595 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss @@ -37,3 +37,8 @@ $admonition-border-radius: 0.25rem; $focus-ring-radius: 0.125rem; // 2px at 100% zoom and 16px base font. $navbar-link-padding-y: 0.25rem; + +// Ensure that there is no overlap of bumper disks (smallest circle that +// contains the bounding box of the interactive element). +// - https://github.com/Quansight-Labs/czi-scientific-python-mgmt/issues/81#issuecomment-2251325783 +$nav-icon-column-gap: 1.12rem; From 15494ecd48147e03d57fd462c1eaf8fb5aeb67d2 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 29 Aug 2024 04:49:37 -0400 Subject: [PATCH 3/8] Make overlay sidebars behave like modals (#1942) This is very similar to #1932. Closes external issue https://github.com/Quansight-Labs/czi-scientific-python-mgmt/issues/84 --------- Co-authored-by: M Bussonnier --- .../assets/scripts/pydata-sphinx-theme.js | 163 +++++++++--------- .../styles/sections/_sidebar-primary.scss | 28 --- .../styles/sections/_sidebar-toggle.scss | 76 ++------ .../theme/pydata_sphinx_theme/layout.html | 16 +- tests/test_build/sidebar_subpage.html | 2 +- 5 files changed, 98 insertions(+), 187 deletions(-) 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 48a1e4257..0c876e622 100644 --- a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js +++ b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js @@ -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 */ @@ -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); }; /******************************************************************************* @@ -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; } @@ -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 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