From e1904a017b083b4980b360c2a99020757f9ff5d6 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:37:52 +0000 Subject: [PATCH 1/5] Updates for file src/pydata_sphinx_theme/locale/en/LC_MESSAGES/sphinx.po in es (#2079) The following localization files have been updated: Parameter | Value ---- | ---- Source File | src/pydata_sphinx_theme/locale/en/LC_MESSAGES/sphinx.po Translation File | src/pydata_sphinx_theme/locale/es/LC_MESSAGES/sphinx.po Language Code | es Transifex Project | [pydata-sphinx-theme](https://app.transifex.com/12rambau/pydata-sphinx-theme/) Transifex Resource | [src..LC_MESSAGES/sphinx.po (main)](https://app.transifex.com/12rambau/pydata-sphinx-theme/79592a50a377d7a120926189491c3d76/) Transifex Event | translated --------- Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../locale/es/LC_MESSAGES/sphinx.po | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pydata_sphinx_theme/locale/es/LC_MESSAGES/sphinx.po b/src/pydata_sphinx_theme/locale/es/LC_MESSAGES/sphinx.po index a47eac62c..a8f632b5c 100644 --- a/src/pydata_sphinx_theme/locale/es/LC_MESSAGES/sphinx.po +++ b/src/pydata_sphinx_theme/locale/es/LC_MESSAGES/sphinx.po @@ -8,6 +8,7 @@ # Cristhian Rivera, 2024 # Felipe Moreno, 2024 # Tania Allard, 2024 +# msgid "" msgstr "" @@ -41,7 +42,8 @@ msgstr "Error" #: src/pydata_sphinx_theme/theme/pydata_sphinx_theme/search.html:9 msgid "Please activate JavaScript to enable the search functionality." -msgstr "Por favor, active JavaScript para habilitar la funcionalidad de búsqueda." +msgstr "" +"Por favor, active JavaScript para habilitar la funcionalidad de búsqueda." #: src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/breadcrumbs.html:6 msgid "Breadcrumb" @@ -152,8 +154,7 @@ msgid "" "Built with the PyData Sphinx Theme " "%(theme_version)s." msgstr "" -"Construido con el Tema PyData Sphinx " +"Construido con el Tema PyData Sphinx " "%(theme_version)s." #: src/pydata_sphinx_theme/theme/pydata_sphinx_theme/sections/announcement.html:4 @@ -191,3 +192,12 @@ msgstr "Navegación del sitio" #~ msgid "light/dark" #~ msgstr "claro/oscuro" + +#~ msgid "" +#~ "Built with the PyData Sphinx Theme " +#~ "%(theme_version)s." +#~ msgstr "" +#~ "Construido con el Tema PyData Sphinx " +#~ "%(theme_version)s." From c47786b993c85f0f442cc8d6e6b55e5d4e92b6b9 Mon Sep 17 00:00:00 2001 From: Tania Allard Date: Tue, 17 Dec 2024 10:38:01 +0000 Subject: [PATCH 2/5] Bump: 0.16.1rc0 -> 0.16.1 --- docs/_static/switcher.json | 12 ++++++------ src/pydata_sphinx_theme/__init__.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/_static/switcher.json b/docs/_static/switcher.json index 089e626b5..184e17ce0 100644 --- a/docs/_static/switcher.json +++ b/docs/_static/switcher.json @@ -4,15 +4,15 @@ "url": "https://pydata-sphinx-theme.readthedocs.io/en/latest/" }, { - "name": "0.16.1rc0", - "version": "v0.16.1rc0", - "url": "https://pydata-sphinx-theme.readthedocs.io/en/v0.16.1rc0/" + "name": "0.16.1 (stable)", + "version": "v0.16.1", + "url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/", + "preferred": true }, { - "name": "0.16.0 (stable)", + "name": "0.16.0", "version": "v0.16.0", - "url": "https://pydata-sphinx-theme.readthedocs.io/en/stable/", - "preferred": true + "url": "https://pydata-sphinx-theme.readthedocs.io/en/v0.16.0/" }, { "name": "0.15.4", diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 219af6aba..71a975ad3 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -17,7 +17,7 @@ from . import edit_this_page, logo, pygments, short_link, toctree, translator, utils -__version__ = "0.16.1rc0" +__version__ = "0.16.1" def update_config(app): From 9b92ec9e8b834c303b842700acac47c7ef07aad9 Mon Sep 17 00:00:00 2001 From: Tania Allard Date: Tue, 17 Dec 2024 16:57:55 +0000 Subject: [PATCH 3/5] BUG - Add `--pst-color-heading` fallback (#2082) Nick pointed in https://github.com/pydata/pydata-sphinx-theme/pull/2058#issuecomment-2548597450 that the `--pst-heading-color` variable is used by folks to customise the headings colour (this was the colour variable before https://github.com/pydata/pydata-sphinx-theme/pull/2058 which brought this in line with our naming convention `--pst-color-heading`). This change effectively renders the use of `--pst-heading-color` useless, so this PR adds a fallback mechanism for this variable. I also added a note about `--pst-color-heading` being our preferred variable. This is not a "big" breaking change, but it will likely affect folks who do not have their PST version pinned since I just made a release. So, it might be worth publishing this change in a follow-up. --- src/pydata_sphinx_theme/assets/styles/base/_base.scss | 11 +++++++---- .../assets/styles/variables/_color.scss | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pydata_sphinx_theme/assets/styles/base/_base.scss b/src/pydata_sphinx_theme/assets/styles/base/_base.scss index 5ebd8bd06..82235df44 100644 --- a/src/pydata_sphinx_theme/assets/styles/base/_base.scss +++ b/src/pydata_sphinx_theme/assets/styles/base/_base.scss @@ -72,33 +72,36 @@ a { line-height: 1.15; } +// From 0.16.1, the preferred variable for headings is --pst-color-heading +// if you have --pst-heading-color, this variable will be used, otherwise the default +// --pst-color-heading will be used. h1 { @extend %heading-style; margin-top: 0; font-size: var(--pst-font-size-h1); - color: var(--pst-color-heading); + color: var(--pst-heading-color, --pst-color-heading); } h2 { @extend %heading-style; font-size: var(--pst-font-size-h2); - color: var(--pst-color-heading); + color: var(--pst-heading-color, --pst-color-heading); } h3 { @extend %heading-style; font-size: var(--pst-font-size-h3); - color: var(--pst-color-heading); + color: var(--pst-heading-color, --pst-color-heading); } h4 { @extend %heading-style; font-size: var(--pst-font-size-h4); - color: var(--pst-color-heading); + color: var(--pst-heading-color, --pst-color-heading); } h5 { diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_color.scss b/src/pydata_sphinx_theme/assets/styles/variables/_color.scss index 2738d775c..6553f0b24 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_color.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_color.scss @@ -286,6 +286,9 @@ $pst-semantic-colors: ( // assign the "duplicate" colors (ones that just reference other variables) & { + // From 0.16.1, the preferred variable for headings is --pst-color-heading + // if you have --pst-heading-color, this variable will be used, otherwise the default + // --pst-color-heading will be used --pst-color-heading: var(--pst-color-text-base); --pst-color-link: var(--pst-color-primary); --pst-color-link-hover: var(--pst-color-secondary); From d76892d437e3c0dc8a059741a84afbd6cc458509 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:51:28 -0600 Subject: [PATCH 4/5] [pre-commit.ci] pre-commit autoupdate hooks (#2091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycontribs/mirrors-prettier: v3.3.3 → v3.4.2](https://github.com/pycontribs/mirrors-prettier/compare/v3.3.3...v3.4.2) - [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.6) - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) - [github.com/Riverside-Healthcare/djLint: v1.36.3 → v1.36.4](https://github.com/Riverside-Healthcare/djLint/compare/v1.36.3...v1.36.4) - [github.com/thibaudcolas/pre-commit-stylelint: v16.11.0 → v16.12.0](https://github.com/thibaudcolas/pre-commit-stylelint/compare/v16.11.0...v16.12.0) closes #2094 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy --- .pre-commit-config.yaml | 10 ++++---- package-lock.json | 9 +++---- .../styles/abstracts/_accessibility.scss | 6 +++-- .../assets/styles/variables/_color.scss | 24 +++++++------------ .../assets/styles/variables/_fonts.scss | 6 ++--- tests/test_build.py | 11 +++++---- 6 files changed, 31 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d7c67c84..209a6d3e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ default_language_version: repos: - repo: "https://github.com/pycontribs/mirrors-prettier" - rev: v3.3.3 + rev: v3.4.2 hooks: - id: prettier # Exclude the HTML, since it doesn't understand Jinja2 @@ -22,20 +22,20 @@ repos: exclude: .+\.html|webpack\.config\.js|tests/test_a11y/ - repo: "https://github.com/astral-sh/ruff-pre-commit" - rev: "v0.8.1" + rev: "v0.8.6" hooks: - id: ruff args: [--exit-non-zero-on-fix] - id: ruff-format - repo: "https://github.com/asottile/pyupgrade" - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: "https://github.com/Riverside-Healthcare/djLint" - rev: v1.36.3 + rev: v1.36.4 hooks: - id: djlint-jinja types_or: ["html"] @@ -56,7 +56,7 @@ repos: - id: remove-metadata - repo: "https://github.com/thibaudcolas/pre-commit-stylelint" - rev: v16.11.0 + rev: v16.12.0 hooks: - id: stylelint # automatically fix .scss files where possible diff --git a/package-lock.json b/package-lock.json index fef417f7c..5688e1834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -709,9 +709,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001609", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz", - "integrity": "sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -726,7 +726,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", diff --git a/src/pydata_sphinx_theme/assets/styles/abstracts/_accessibility.scss b/src/pydata_sphinx_theme/assets/styles/abstracts/_accessibility.scss index bc5cf74b7..53aa6aef0 100644 --- a/src/pydata_sphinx_theme/assets/styles/abstracts/_accessibility.scss +++ b/src/pydata_sphinx_theme/assets/styles/abstracts/_accessibility.scss @@ -70,8 +70,10 @@ $rgb-col: map.merge( $rgb-col, ( - $channel: - math.pow(math.div((math.div($value, 255) + 0.055), 1.055), 2.4), + $channel: math.pow( + math.div((math.div($value, 255) + 0.055), 1.055), + 2.4 + ), ) ); } diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_color.scss b/src/pydata_sphinx_theme/assets/styles/variables/_color.scss index 6553f0b24..5efe04989 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_color.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_color.scss @@ -26,8 +26,7 @@ /* Assign base colors for the PyData theme */ $color-palette: ( // Primary color - "teal": - ( + "teal": ( "50": #f4fbfc, "100": #e9f6f8, "200": #d0ecf1, @@ -40,8 +39,7 @@ $color-palette: ( "900": #021b1f, ), // Secondary color - "violet": - ( + "violet": ( "50": #f4eefb, "100": #e0c7ff, "200": #d5b4fd, @@ -54,8 +52,7 @@ $color-palette: ( "900": #1e0e39, ), // Neutrals - "gray": - ( + "gray": ( "50": #f9f9fa, "100": #f3f4f5, "200": #e5e7ea, @@ -68,8 +65,7 @@ $color-palette: ( "900": #14181e, ), // Accent color - "pink": - ( + "pink": ( "50": #fcf8fd, "100": #fcf0fa, "200": #f8dff5, @@ -161,8 +157,7 @@ $pst-semantic-colors: ( "bg-dark": #002f17, ), // This is based on the warning color - "attention": - ( + "attention": ( "light": var(--pst-color-warning), "bg-light": var(--pst-color-warning-bg), "dark": var(--pst-color-warning), @@ -229,15 +224,13 @@ $pst-semantic-colors: ( // DEPTH COLORS - you can see the elevation colours and shades // in the Figma file https://www.figma.com/file/rUrrHGhUBBIAAjQ82x6pz9/PyData-Design-system---proposal-for-implementation-(2)?node-id=1492%3A922&t=sQeQZehkOzposYEg-1 // background: color of the canvas / the furthest back layer - "background": - ( + "background": ( "light": #{map-deep-get($color-palette, "foundation", "white")}, "dark": #{map-deep-get($color-palette, "foundation", "black")}, ), // on-background: provides slight contrast against background // (by use of shadows in light theme) - "on-background": - ( + "on-background": ( "light": #{map-deep-get($color-palette, "foundation", "white")}, "dark": #{map-deep-get($color-palette, "gray", "800")}, ), @@ -246,8 +239,7 @@ $pst-semantic-colors: ( "dark": #{map-deep-get($color-palette, "gray", "700")}, ), // on_surface: object on top of surface object (without shadows) - "on-surface": - ( + "on-surface": ( "light": #{map-deep-get($color-palette, "gray", "800")}, "dark": $foundation-light-gray, ), diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss b/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss index 5b76b37ed..9a16122ea 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss @@ -33,9 +33,9 @@ html { // Font family // These are adapted from https://systemfontstack.com/ */ - --pst-font-family-base-system: -apple-system, "BlinkMacSystemFont", "Segoe UI", - "Helvetica Neue", "Arial", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol"; + --pst-font-family-base-system: -apple-system, "BlinkMacSystemFont", + "Segoe UI", "Helvetica Neue", "Arial", sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol"; --pst-font-family-monospace-system: "SFMono-Regular", "Menlo", "Consolas", "Monaco", "Liberation Mono", "Lucida Console", monospace; --pst-font-family-base: var(--pst-font-family-base-system); diff --git a/tests/test_build.py b/tests/test_build.py index 7a005fa4a..8444da330 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -891,8 +891,8 @@ def test_pygments_fallbacks(sphinx_build_factory, style_names, keyword_colors) - # see if our warnings worked if style_names[0].startswith("fake"): assert len(warnings) == 2 - re.match(r"Color theme fake_foo.*tango", warnings[0]) - re.match(r"Color theme fake_bar.*monokai", warnings[1]) + assert re.search(r"Highlighting style fake_foo.*tango", warnings[0]) + assert re.search(r"Highlighting style fake_bar.*monokai", warnings[1]) else: assert warnings == [""] # test that the rendered HTML has highlighting spans @@ -908,10 +908,11 @@ def test_pygments_fallbacks(sphinx_build_factory, style_names, keyword_colors) - assert lines[0].startswith('html[data-theme="light"]') for mode, color in dict(zip(["light", "dark"], keyword_colors)).items(): regexp = re.compile( - r'html\[data-theme="' + mode + r'"\].*\.k .*color: ' + color + r'html\[data-theme="' + mode + r'"\].*\.k .*color:\s?' + color, + re.IGNORECASE, ) - matches = [regexp.match(line) is not None for line in lines] - assert sum(matches) == 1 + matches = [regexp.search(line) is not None for line in lines] + assert sum(matches) == 1, f"expected {mode}: {color}\n" + "\n".join(lines) def test_deprecated_build_html(sphinx_build_factory, file_regression) -> None: From 7c54d4a635dee71855e517cfc21ea2a8b8dcf014 Mon Sep 17 00:00:00 2001 From: Kayce Basques Date: Wed, 22 Jan 2025 14:33:50 -0800 Subject: [PATCH 5/5] Add search-as-you-type (inline search results) feature (#2093) Fixes #1977 --------- Co-authored-by: Kayce Basques Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/conf.py | 1 + docs/user_guide/search.rst | 11 ++ src/pydata_sphinx_theme/__init__.py | 6 + .../assets/scripts/pydata-sphinx-theme.js | 166 ++++++++++++++++++ .../assets/styles/components/_search.scss | 25 ++- .../theme/pydata_sphinx_theme/layout.html | 9 + .../theme/pydata_sphinx_theme/theme.conf | 1 + tests/test_a11y.py | 31 ++++ 8 files changed, 248 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index afab8132c..a8682b93e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -217,6 +217,7 @@ "version_match": version_match, }, # "back_to_top_button": False, + "search_as_you_type": True, } html_sidebars = { diff --git a/docs/user_guide/search.rst b/docs/user_guide/search.rst index 3d1d87c4d..9ec837325 100644 --- a/docs/user_guide/search.rst +++ b/docs/user_guide/search.rst @@ -63,3 +63,14 @@ following configuration to your ``conf.py`` file: html_theme_options = { "search_bar_text": "Your text here..." } + +Configure the inline search results (search-as-you-type) feature +---------------------------------------------------------------- + +Set the ``search_as_you_type`` HTML theme option to ``True``. + +.. code:: python + + html_theme_options = { + "search_as_you_type": True + } diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 71a975ad3..92944f4c4 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -241,6 +241,12 @@ def update_and_remove_templates( """ app.add_js_file(None, body=js) + # Specify whether search-as-you-type should be used or not. + search_as_you_type = str(context["theme_search_as_you_type"]).lower() + app.add_js_file( + None, body=f"DOCUMENTATION_OPTIONS.search_as_you_type = {search_as_you_type};" + ) + # Update version number for the "made with version..." component context["theme_version"] = __version__ 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 a861cfdb0..ea01bb7b5 100644 --- a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js +++ b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js @@ -261,6 +261,7 @@ var addEventListenerForSearchKeyboard = () => { // also allow Escape key to hide (but not show) the dynamic search field else if (document.activeElement === input && /Escape/i.test(event.key)) { toggleSearchField(); + resetSearchAsYouTypeResults(); } }, true, @@ -332,6 +333,170 @@ var setupSearchButtons = () => { searchDialog.addEventListener("click", closeDialogOnBackdropClick); }; +/******************************************************************************* + * Inline search results (search-as-you-type) + * + * Immediately displays search results under the search query textbox. + * + * The search is conducted by Sphinx's built-in search tools (searchtools.js). + * Usually searchtools.js is only available on /search.html but + * pydata-sphinx-theme (PST) has been modified to load searchtools.js on every + * page. After the user types something into PST's search query textbox, + * searchtools.js executes the search and populates the results into + * the #search-results container. searchtools.js expects the results container + * to have that exact ID. + */ +var setupSearchAsYouType = () => { + if (!DOCUMENTATION_OPTIONS.search_as_you_type) { + return; + } + + // Don't interfere with the default search UX on /search.html. + if (window.location.pathname.endsWith("/search.html")) { + return; + } + + // Bail if the Search class is not available. Search-as-you-type is + // impossible without that class. layout.html should ensure that + // searchtools.js loads. + // + // Search class is defined in upstream Sphinx: + // https://github.com/sphinx-doc/sphinx/blob/6678e357048ea1767daaad68e7e0569786f3b458/sphinx/themes/basic/static/searchtools.js#L181 + if (!Search) { + return; + } + + // Destroy the previous search container and create a new one. + resetSearchAsYouTypeResults(); + let timeoutId = null; + let lastQuery = ""; + const searchInput = document.querySelector( + "#pst-search-dialog input[name=q]", + ); + + // Initiate searches whenever the user types stuff in the search modal textbox. + searchInput.addEventListener("keyup", () => { + const query = searchInput.value; + + // Don't search when there's nothing in the query textbox. + if (query === "") { + resetSearchAsYouTypeResults(); // Remove previous results. + return; + } + + // Don't search if there is no detectable change between + // the last query and the current query. E.g. the user presses + // Tab to start navigating the search results. + if (query === lastQuery) { + return; + } + + // The user has changed the search query. Delete the old results + // and start setting up the new container. + resetSearchAsYouTypeResults(); + + // Debounce so that the search only starts when the user stops typing. + const delay_ms = 300; + lastQuery = query; + if (timeoutId) { + window.clearTimeout(timeoutId); + } + timeoutId = window.setTimeout(() => { + Search.performSearch(query); + document.querySelector("#search-results").classList.remove("empty"); + timeoutId = null; + }, delay_ms); + }); +}; + +// Delete the old search results container (if it exists) and set up a new one. +// +// There is some complexity around ensuring that the search results links are +// correct because we're extending searchtools.js past its assumed usage. +// Sphinx assumes that searches are only executed from /search.html and +// therefore it assumes that all search results links should be relative to +// the root directory of the website. In our case the search can now execute +// from any page of the website so we must fix the relative URLs that +// searchtools.js generates. +var resetSearchAsYouTypeResults = () => { + if (!DOCUMENTATION_OPTIONS.search_as_you_type) { + return; + } + // If a search-as-you-type results container was previously added, + // remove it now. + let results = document.querySelector("#search-results"); + if (results) { + results.remove(); + } + + // Create a new search-as-you-type results container. + results = document.createElement("section"); + results.classList.add("empty"); + // Remove the container element from the tab order. Individual search + // results are still focusable. + results.tabIndex = -1; + // When focus is on a search result, make sure that pressing Escape closes + // the search modal. + results.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + toggleSearchField(); + resetSearchAsYouTypeResults(); + } + }); + // IMPORTANT: The search results container MUST have this exact ID. + // searchtools.js is hardcoded to populate into the node with this ID. + results.id = "search-results"; + let modal = document.querySelector("#pst-search-dialog"); + modal.appendChild(results); + + // Get the relative path back to the root of the website. + const root = + "URL_ROOT" in DOCUMENTATION_OPTIONS + ? DOCUMENTATION_OPTIONS.URL_ROOT // Sphinx v6 and earlier + : document.documentElement.dataset.content_root; // Sphinx v7 and later + + // As Sphinx populates the search results, this observer makes sure that + // each URL is correct (i.e. doesn't 404). + const linkObserver = new MutationObserver(() => { + const links = Array.from( + document.querySelectorAll("#search-results .search a"), + ); + // Check every link every time because the timing of when new results are + // added is unpredictable and it's not an expensive operation. + links.forEach((link) => { + link.tabIndex = 0; // Use natural tab order for search results. + // Don't use the link.href getter because the browser computes the href + // as a full URL. We need the relative URL that Sphinx generates. + const href = link.getAttribute("href"); + if (href.startsWith(root)) { + // No work needed. The root has already been prepended to the href. + return; + } + link.href = `${root}${href}`; + }); + }); + + // The node that linkObserver watches doesn't exist until the user types + // something into the search textbox. This second observer (resultsObserver) + // just waits for #search-results to exist and then registers + // linkObserver on it. + let isObserved = false; + const resultsObserver = new MutationObserver(() => { + if (isObserved) { + return; + } + const container = document.querySelector("#search-results .search"); + if (!container) { + return; + } + linkObserver.observe(container, { childList: true }); + isObserved = true; + }); + resultsObserver.observe(results, { childList: true }); +}; + /******************************************************************************* * Version Switcher * Note that this depends on two variables existing that are defined in @@ -857,6 +1022,7 @@ documentReady(addModeListener); documentReady(scrollToActive); documentReady(addTOCInteractivity); documentReady(setupSearchButtons); +documentReady(setupSearchAsYouType); documentReady(setupMobileSidebarKeyboardHandlers); // Determining whether an element has scrollable content depends on stylesheets, diff --git a/src/pydata_sphinx_theme/assets/styles/components/_search.scss b/src/pydata_sphinx_theme/assets/styles/components/_search.scss index 630c6cd1f..cf8d035cf 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_search.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_search.scss @@ -93,14 +93,17 @@ z-index: $zindex-modal; top: 30%; left: 50%; - transform: translate(-50%, -50%); + transform: translate(-50%, -30%); right: 1rem; + margin-bottom: 0; margin-top: 0.5rem; width: 90%; max-width: 800px; background-color: transparent; padding: $focus-ring-width; border: none; + flex-direction: column; + height: 80vh; &::backdrop { background-color: black; @@ -108,7 +111,7 @@ } form.bd-search { - flex-grow: 1; + flex-grow: 0; // Font and input text a bit bigger svg, @@ -116,6 +119,24 @@ font-size: var(--pst-font-size-icon); } } + + /* In pydata-sphinx-theme.js this container is appended below + * the query input node after the user types their search query. + * Search results are populated into this container using Sphinx's + * built-in, JS-powered local search tools. */ + #search-results { + overflow-y: scroll; + background-color: var(--pst-color-background); + padding: 1em; + + a { + color: var(--pst-color-link); + } + + &.empty { + display: none; + } + } } } diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html index e062c1806..2b286a6b2 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html @@ -37,6 +37,15 @@ {%- if last_updated %} {%- endif %} + {% if pagename == 'search' %} + {# Search tools are already loaded on search page. Don't load them twice. #} + {% else %} + {# Load Sphinx's built-in search tools so that our custom inline search + experience can work on any page. #} + + + + {% endif %} {%- endblock extrahead %} {% block body_tag %} {# set up with scrollspy to update the toc as we scroll #} diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf index 924ec116c..111691ea2 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf @@ -36,6 +36,7 @@ logo = logo_link = surface_warnings = True back_to_top_button = True +search_as_you_type = False # Template placement in theme layouts navbar_start = navbar-logo diff --git a/tests/test_a11y.py b/tests/test_a11y.py index 713926118..f0ff97450 100644 --- a/tests/test_a11y.py +++ b/tests/test_a11y.py @@ -291,3 +291,34 @@ def test_breadcrumb_expansion(page: Page, url_base: str) -> None: expect(page.get_by_label("Breadcrumb").get_by_role("list")).to_contain_text( "Update Sphinx configuration during the build" ) + + +@pytest.mark.a11y +def test_search_as_you_type(page: Page, url_base: str) -> None: + """Search-as-you-type feature should support keyboard navigation. + + When the search-as-you-type (inline search results) feature is enabled, + pressing Tab after entering a search query should focus the first inline + search result. + """ + page.set_viewport_size({"width": 1440, "height": 720}) + page.goto(urljoin(url_base, "/examples/kitchen-sink/blocks.html")) + # Click the search textbox. + searchbox = page.locator("css=.navbar-header-items .search-button__default-text") + searchbox.click() + # Type a search query. + query_input = page.locator("css=#pst-search-dialog input[type=search]") + expect(query_input).to_be_visible() + query_input.type("test") + page.wait_for_timeout(301) # Search execution is debounced for 300 ms. + search_results = page.locator("css=#search-results") + expect(search_results).to_be_visible() + # Navigate with the keyboard. + query_input.press("Tab") + # Make sure that the first inline search result is focused. + actual_focused_content = page.evaluate("document.activeElement.textContent") + first_result_selector = "#search-results .search li:first-child a" + expected_focused_content = page.evaluate( + f"document.querySelector('{first_result_selector}').textContent" + ) + assert actual_focused_content == expected_focused_content