From 3704ed668d3927deef78fd4eb88347fd124ec0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A1udia?= Date: Thu, 16 Jul 2026 13:36:58 +0100 Subject: [PATCH 1/9] fix(theme): stabilize palette selector without instant navigation --- docs/assets/javascripts/docs-theme.js | 22 +++++++++++----------- docs/assets/javascripts/version-select.js | 6 +++--- theme/partials/header.html | 1 - theme/stylesheets/header.css | 11 +++++------ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/assets/javascripts/docs-theme.js b/docs/assets/javascripts/docs-theme.js index bca89dd5f2..3dfaad9273 100644 --- a/docs/assets/javascripts/docs-theme.js +++ b/docs/assets/javascripts/docs-theme.js @@ -1,4 +1,5 @@ var searchReturnTarget; +var searchFocusFrame; var tocScrollCleanup; var footerWidgetObserver; @@ -51,7 +52,8 @@ function openSearch(opener) { trigger.setAttribute("aria-expanded", "true"); } - window.setTimeout(function () { + window.cancelAnimationFrame(searchFocusFrame); + searchFocusFrame = window.requestAnimationFrame(function () { var input = document.querySelector(".md-search__input"); if (input) { // preventScroll: focusing the input inside the fixed search overlay @@ -64,6 +66,7 @@ function openSearch(opener) { function closeSearch() { var searchToggle = document.querySelector("#__search"); + window.cancelAnimationFrame(searchFocusFrame); if (searchToggle && searchToggle.checked) { searchToggle.click(); } @@ -99,9 +102,8 @@ function initializeDocsTheme() { if (shortcut && !/Mac|iPhone|iPad|iPod/i.test(platform)) { shortcut.textContent = "Ctrl K"; } - // The header search trigger persists across navigation.instant page loads, - // so bind each trigger once; otherwise every navigation stacks another - // handler on the same element. + // This is a full-page navigation site. Bind a search handler once to each + // trigger in case this file is included more than once by an integration. searchTriggers.forEach(function (searchTrigger) { if (searchTrigger.dataset.docsSearchTriggerInitialized) { return; @@ -125,10 +127,8 @@ function initializeDocsTheme() { if (drawer) { drawer.id = "docs-navigation"; } - // With navigation.instant, Material keeps the header and drawer mounted - // while emitting document$ for each page. Only bind these persistent - // controls once; otherwise every navigation adds another click handler - // and a hamburger tap toggles the checkbox an even number of times. + // Bind drawer controls once, even if an integration includes this script + // more than once on the same document. if (drawerToggle && drawerTrigger && drawer && !drawerTrigger.dataset.docsDrawerInitialized) { drawerTrigger.dataset.docsDrawerInitialized = "true"; let suppressDrawerKeyboardClick = false; @@ -309,10 +309,10 @@ function initializeDocsTheme() { observeFooterForWidget(); } -if (typeof document$ !== "undefined" && document$.subscribe) { - document$.subscribe(initializeDocsTheme); +if (document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", initializeDocsTheme, { once: true }); } else { - window.addEventListener("DOMContentLoaded", initializeDocsTheme); + initializeDocsTheme(); } window.addEventListener("keydown", function (event) { diff --git a/docs/assets/javascripts/version-select.js b/docs/assets/javascripts/version-select.js index 42c04c0dba..f18242f222 100644 --- a/docs/assets/javascripts/version-select.js +++ b/docs/assets/javascripts/version-select.js @@ -235,8 +235,8 @@ function initializeVersionSelector() { fetchVersions(generateVersionSwitcher); } -if (typeof document$ !== "undefined" && document$.subscribe) { - document$.subscribe(initializeVersionSelector); +if (document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", initializeVersionSelector, { once: true }); } else { - window.addEventListener("DOMContentLoaded", initializeVersionSelector); + initializeVersionSelector(); } diff --git a/theme/partials/header.html b/theme/partials/header.html index df8bdef565..c7c8830b8a 100644 --- a/theme/partials/header.html +++ b/theme/partials/header.html @@ -28,7 +28,6 @@ {% if config.theme.palette %} {% if not config.theme.palette is mapping %}{% include "partials/palette.html" %}{% endif %} {% endif %} - {% if not config.theme.palette is mapping %}{% include "partials/javascripts/palette.html" %}{% endif %} {% if config.extra.alternate %}{% include "partials/alternate.html" %}{% endif %} {% if "material/search" in config.plugins %}{% include "partials/search.html" %}{% endif %} diff --git a/theme/stylesheets/header.css b/theme/stylesheets/header.css index a08ea87e9e..6565dd8305 100644 --- a/theme/stylesheets/header.css +++ b/theme/stylesheets/header.css @@ -23,16 +23,15 @@ .docs-version-select__icon { position: absolute; top: 50%; right: .65rem; width: 1rem; height: 1rem; transform: translateY(-50%); color: var(--docs-text-secondary); pointer-events: none; } .version-select-container .select-css:disabled { opacity: 1; color: var(--docs-text-secondary); cursor: wait; } .version-select-container .select-css:hover, .version-select-container .select-css:focus { border-color: var(--docs-border-strong); } -.md-header [data-md-component="palette"] { grid-column: 7; } -.md-header [data-md-component="palette"] .md-header__button:not([hidden]) { display: inline-flex; width: 2.25rem; height: 2.25rem; align-items: center; justify-content: center; box-sizing: border-box; padding: 0; border: 1px solid var(--docs-border); border-radius: var(--docs-radius); background: var(--docs-bg); color: var(--docs-text-tertiary); } +.md-header [data-md-component="palette"] { display: flex; grid-column: 7; flex: 0 0 2.25rem; width: 2.25rem; min-width: 2.25rem; } +.md-header [data-md-component="palette"] .md-header__button:not([hidden]) { display: inline-flex; flex: 0 0 2.25rem; width: 2.25rem; min-width: 2.25rem; height: 2.25rem; align-items: center; justify-content: center; box-sizing: border-box; padding: 0; border: 1px solid var(--docs-border); border-radius: var(--docs-radius); background: var(--docs-bg); color: var(--docs-text-tertiary); } .md-header [data-md-component="palette"] .md-header__button:hover { border-color: var(--docs-border-strong); color: var(--docs-text-secondary); } .md-header [data-md-component="palette"] .md-header__button:focus-visible { outline: 2px solid var(--docs-link); outline-offset: 3px; } .md-header .md-search { display: block; position: fixed; inset: 0; z-index: 100; width: auto; visibility: hidden; opacity: 0; pointer-events: none; background: color-mix(in srgb, var(--docs-bg) 55%, transparent); backdrop-filter: blur(3px); } -/* While search is open, Material locks the page by setting the body to - position: fixed, which would drag the sticky header (and its theme toggle) - out of view and behind the search overlay. Pin the header to the viewport - above the overlay so the whole navbar stays visible and interactive. */ +/* Material locks the page when search opens. Keep the header fixed above the + page, while its controls are painted above the backdrop in their own layer. */ .md-toggle[data-md-toggle="search"]:checked ~ .md-header { position: fixed; top: 0; right: 0; left: 0; z-index: 101; } +.md-toggle[data-md-toggle="search"]:checked ~ .md-header .md-header__inner > :not(.md-search) { position: relative; z-index: 101; } .md-toggle[data-md-toggle="search"]:not(:checked) ~ .md-header .md-search__inner { width: 0; height: 0; margin: 0; overflow: hidden; } .md-toggle[data-md-toggle="search"]:checked ~ .md-header .md-search { visibility: visible; opacity: 1; pointer-events: auto; } .md-search__overlay { display: block; position: absolute; inset: 0; background: transparent; } From 142271dca6fb447ed6d295db8832356a072969fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A1udia?= Date: Thu, 16 Jul 2026 13:46:00 +0100 Subject: [PATCH 2/9] fix(theme): render palette control before client mount --- theme/partials/palette.html | 2 +- theme/stylesheets/header.css | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/theme/partials/palette.html b/theme/partials/palette.html index 5633dda066..8e907013d5 100644 --- a/theme/partials/palette.html +++ b/theme/partials/palette.html @@ -6,7 +6,7 @@ {% set accent = option.accent | d("indigo", true) %} {% if option.toggle %} -