diff --git a/code-group-language-persist.js b/code-group-language-persist.js index 30b7ec22ec..3a0591e2e1 100644 --- a/code-group-language-persist.js +++ b/code-group-language-persist.js @@ -22,10 +22,18 @@ * LANGUAGES — only Python/TypeScript). The whitelist stops both descriptive * tabs ("Web App", "Summary metrics") and incidental code tabs ("Bash", * "cURL") from clobbering the saved language. - * - Restore: on load, after hydration, and on SPA route changes (a - * MutationObserver), any tab whose label matches the saved language (compared - * case-insensitively) is selected. Tabs already active are skipped, so this - * coexists with Mintlify's own restore without fighting it. + * - Restore: on load, after hydration, and on SPA route changes (detected by + * a MutationObserver watching location.pathname), any tab whose label + * matches the saved language (compared case-insensitively) is selected. + * Tabs already active are skipped, so this coexists with Mintlify's own + * restore without fighting it. + * - Yield: when the reader manually picks a non-language tab ("HTTP API", + * "Command Line", …), its group is marked overridden and restore leaves it + * alone for the rest of that page view. Restore also only re-runs when the + * pathname actually changes, never on arbitrary DOM mutations — otherwise + * the mutation caused by the reader's own tab switch would re-trigger a + * restore that instantly snaps mixed groups (e.g. Python / TypeScript / + * HTTP API) back to the saved language, making other tabs unselectable. * * Selection uses a real pointer sequence, not element.click(): Mintlify's tabs * activate on pointer/mouse-down, and a synthetic click() does not switch them. @@ -60,6 +68,14 @@ python: 1, typescript: 1 }; + /** + * Tab groups the reader has manually steered to a non-language tab, which + * restore() must then leave alone. Keyed by the group's DOM node, so entries + * evaporate naturally when a client-side route change replaces the page + * content (and with it, the nodes). + */ + var overridden = typeof WeakSet === 'function' ? new WeakSet() : null; + /** Read the saved language, tolerant of disabled/throwing localStorage. */ function read() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } @@ -99,6 +115,16 @@ tab.getAttribute('aria-selected') === 'true'; } + /** + * The tab group a tab belongs to — its ARIA tablist, or failing that its + * parent element. Used to scope manual-override tracking per group. + * @param {Element} tab + * @returns {Element|null} + */ + function groupOf(tab) { + return tab.closest('[role="tablist"]') || tab.parentElement; + } + /** * Selects a tab the way a real click does. Mintlify's tabs activate on * pointer/mouse-down, not on a synthetic element.click(), so dispatch a full @@ -116,19 +142,32 @@ /** * Capture-phase selection handler. Records the chosen tab's label when it - * names a language, so the choice survives navigation. Bound to pointerdown - * (when tabs actually select) and click (covers keyboard activation). + * names a language, so the choice survives navigation; when it names + * anything else ("HTTP API", "Command Line", …), marks the group as + * reader-overridden so restore() stops re-asserting the saved language in + * it. Bound to pointerdown (when tabs actually select) and click (covers + * keyboard activation). Only trusted events count — the synthetic clicks + * restore() dispatches must not register as reader choices. * @param {Event} e */ function onSelect(e) { + if (!e.isTrusted) return; var tab = e.target && e.target.closest ? e.target.closest('[role="tab"]') : null; if (!tab) return; var label = labelOf(tab); - if (label && isLanguage(label)) write(label); + if (!label) return; + var group = overridden && groupOf(tab); + if (isLanguage(label)) { + write(label); + if (group) overridden.delete(group); + } else if (group) { + overridden.add(group); + } } /** - * Selects the saved language in every group that isn't already on it. + * Selects the saved language in every group that isn't already on it, + * except groups the reader has manually overridden this page view. * Self-terminating: tabs already active are skipped, so the synthetic clicks * we trigger here do not cause an observe/click feedback loop, and we never * fight Mintlify's own already-applied selection. @@ -140,9 +179,12 @@ var tabs = document.querySelectorAll('[role="tab"]'); for (var i = 0; i < tabs.length; i++) { var tab = tabs[i]; - if (labelOf(tab).toLowerCase() === want && !isActive(tab)) { - activate(tab); + if (labelOf(tab).toLowerCase() !== want || isActive(tab)) continue; + if (overridden) { + var group = groupOf(tab); + if (group && overridden.has(group)) continue; } + activate(tab); } } @@ -154,18 +196,38 @@ setTimeout(function () { pending = false; restore(); }, 80); } + /** Restore now-ish, then again while late-hydrating content settles. */ + function restoreSoon() { + schedule(); + [300, 800, 1500].forEach(function (ms) { setTimeout(restore, ms); }); + } + + /** + * Mutation handler: re-applies the saved language only when the mutation + * burst accompanies an actual client-side navigation (the pathname + * changed). Restoring on every mutation would treat the reader's own tab + * switch — itself a DOM mutation — as a cue to snap the group back to the + * saved language. + */ + var lastPathname = location.pathname; + function onMutate() { + if (location.pathname === lastPathname) return; + lastPathname = location.pathname; + restoreSoon(); + } + function start() { // Capture phase so we observe the selection around Mintlify's own handling. document.addEventListener('pointerdown', onSelect, true); document.addEventListener('click', onSelect, true); + // Initial load: restore immediately, then catch tabs that hydrate later. restore(); - // Catch tabs that hydrate after first paint. - [300, 800, 1500].forEach(function (ms) { setTimeout(restore, ms); }); + restoreSoon(); - // Re-apply whenever new content renders (client-side route changes). + // Re-apply on client-side route changes. if (document.body) { - new MutationObserver(schedule).observe(document.body, { + new MutationObserver(onMutate).observe(document.body, { childList: true, subtree: true });