Skip to content

Update Tab Styles (Take 2) #29189

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
169 changes: 154 additions & 15 deletions assets/scripts/components/codetabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,102 @@ const initCodeTabs = () => {
const { allowedRegions } = regionConfig;
const tabQueryParameter = getQueryParameterByName('tab') || getQueryParameterByName('tabs')
const codeTabParameters = allowedRegions.reduce((k,v) => ({...k, [v]: {}}), {});
let resizeTimeout;
let currentActiveTab = null; // Store the current active tab

const cleanupExistingTabs = () => {
// Store current active tab before cleanup
const activeTab = document.querySelector('.code-tabs .nav-tabs li.active a');
if (activeTab) {
currentActiveTab = activeTab.getAttribute('data-lang');
}

// Remove all existing tab navigation elements
document.querySelectorAll('.nav-tabs').forEach(navTabs => {
// Only remove if it's a child of a code-tabs container
if (navTabs.closest('.code-tabs')) {
navTabs.innerHTML = '';
}
});
}

const detectTabWrapping = () => {
const tabContainers = document.querySelectorAll('.code-tabs');

tabContainers.forEach(container => {
const tabsNav = container.querySelector('.nav-tabs');
if (!tabsNav) return;

const tabs = tabsNav.querySelectorAll('li');
if (tabs.length < 2) return; // Need at least two tabs to wrap

const firstTab = tabs[0];
const lastTab = tabs[tabs.length - 1];

// Store original state
const originalHasClass = container.classList.contains('tabs-wrap-layout');

// Ensure measurement happens in the unwrapped state
container.classList.remove('tabs-wrap-layout');

// Force layout recalculation
void tabsNav.offsetHeight;

const firstTop = firstTab.offsetTop;
const lastTop = lastTab.offsetTop;
const heightDifference = lastTop - firstTop;

// Determine if it *should* be wrapped based on measurement in unwrapped state
// Use a small buffer (e.g., 2px) to account for rendering variations
const shouldBeWrapped = heightDifference > 2;

// Apply the correct class if the state needs to change
if (shouldBeWrapped !== originalHasClass) {
if (shouldBeWrapped) {
container.classList.add('tabs-wrap-layout');
} else {
container.classList.remove('tabs-wrap-layout');
}
} else {
// If no change needed, ensure the class is restored if it was removed for measurement
if (originalHasClass) {
container.classList.add('tabs-wrap-layout');
}
}
});
};

const debouncedDetectTabWrapping = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(detectTabWrapping, 150); // Increased debounce time
};

const init = () => {
// Clean up existing tabs first
cleanupExistingTabs();

renderCodeTabElements()
addEventListeners()
activateTabsOnLoad()
getContentTabHeight()
addObserversToCodeTabs()

// Initial detection with font loading check
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
// Force reflow after fonts are loaded
document.body.offsetHeight;
detectTabWrapping();
});
} else {
// Fallback for browsers without font loading API
detectTabWrapping();
}

// Remove any existing resize listeners
window.removeEventListener('resize', debouncedDetectTabWrapping);
// Add new resize listener
window.addEventListener('resize', debouncedDetectTabWrapping);
}

/**
Expand All @@ -25,16 +114,26 @@ const initCodeTabs = () => {
const navTabsElement = codeTabsElement.querySelector('.nav-tabs')
const tabContent = codeTabsElement.querySelector('.tab-content')
const tabPaneNodeList = tabContent.querySelectorAll('.tab-pane')

// Create a map to track unique tab titles
const uniqueTabs = new Map();

tabPaneNodeList.forEach(tabPane => {
const title = tabPane.getAttribute('title')
const lang = tabPane.getAttribute('data-lang')
const li = document.createElement('li')
const anchor = document.createElement('a')
anchor.dataset.lang = lang
anchor.href = '#'
anchor.innerText = title
li.appendChild(anchor)
navTabsElement.appendChild(li)

// Only add the tab if we haven't seen this title before
if (!uniqueTabs.has(title)) {
uniqueTabs.set(title, true);

const li = document.createElement('li')
const anchor = document.createElement('a')
anchor.dataset.lang = lang
anchor.href = '#'
anchor.innerText = title
li.appendChild(anchor)
navTabsElement.appendChild(li)
}
})
})
}
Expand All @@ -56,12 +155,17 @@ const initCodeTabs = () => {

if (activeLangTab && activePane) {
// Hide all tab content and remove 'active' class from all tab elements.
tabsList.forEach(tab => tab.classList.remove('active'))
tabPanesList.forEach(pane => pane.classList.remove('active', 'show'))
// Also, remove any inline display style to let CSS classes control visibility.
tabsList.forEach(tab => tab.classList.remove('active'));
tabPanesList.forEach(pane => {
pane.classList.remove('active', 'show');
pane.style.removeProperty('display');
});

// Show the active content and highlight active tab.
activeLangTab.closest('li').classList.add('active')
activePane.classList.add('active', 'show')
activeLangTab.closest('li').classList.add('active');
activePane.classList.add('active', 'show');
activePane.style.removeProperty('display');
}

const currentActiveTab = codeTabsElement.querySelector('.nav-tabs li.active')
Expand All @@ -75,14 +179,17 @@ const initCodeTabs = () => {
firstTabPane.classList.add('active', 'show')
}
})

// Run tab wrapping detection after tab activation with slight delay
setTimeout(detectTabWrapping, 10);
}

updateUrl(activeLang)
}

const scrollToAnchor = (tab, anchorname) => {
const anchor = document.querySelectorAll(`[data-lang='${tab}'] ${anchorname}`)[0];

if (anchor) {
anchor.scrollIntoView();
} else {
Expand All @@ -91,6 +198,16 @@ const initCodeTabs = () => {
}

const activateTabsOnLoad = () => {
// If we have a stored active tab from before reinitialization, use that
if (currentActiveTab) {
const selectedLanguageTab = document.querySelector(`a[data-lang="${currentActiveTab}"]`);
if (selectedLanguageTab) {
activateCodeTab(selectedLanguageTab);
return;
}
}

// Otherwise use URL parameter or fall back to first tab
const firstTab = document.querySelectorAll('.code-tabs .nav-tabs a').item(0)
if (tabQueryParameter) {
const selectedLanguageTab = document.querySelector(`a[data-lang="${tabQueryParameter}"]`);
Expand All @@ -102,7 +219,7 @@ const initCodeTabs = () => {
scrollToAnchor(tabQueryParameter, window.location.hash);
}, 300);
}
}else{
} else {
activateCodeTab(firstTab)
}
} else {
Expand All @@ -124,7 +241,7 @@ const initCodeTabs = () => {

activateCodeTab(link);
getContentTabHeight();

// ensures page doesnt jump when navigating tabs.
// takes into account page shifting that occurs due to navigating tabbed content w/ height changes.
// implementation of synced tabs from https://github.com/withastro/starlight/blob/main/packages/starlight/user-components/Tabs.astro
Expand Down Expand Up @@ -223,7 +340,29 @@ const initCodeTabs = () => {
})
}

/**
* If Cdocs is running on this page,
* tell it to refresh the tabs when content changes
*/
if (window.clientFiltersManager) {
// Update the tabs after the page is initially rendered
clientFiltersManager.registerHook('afterReveal', () => {
// Reset stored tab on initial reveal
currentActiveTab = null;
init();
});

// Update the tabs after the page is re-rendered
clientFiltersManager.registerHook('afterRerender', init);
}

init()

// Add window load event to handle final detection after everything is loaded
window.addEventListener('load', () => {
// Final check after page is fully loaded (all resources)
setTimeout(detectTabWrapping, 50);
});
}

export default initCodeTabs
export default initCodeTabs
Loading
Loading