diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 312eef77..656b669e 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -301,6 +301,11 @@ describe('Code Groups', () => { }) test('group-name synchronization across groups', async () => { + // Clear localStorage to ensure clean test state + await page.evaluate(() => localStorage.clear()) + await page.reload() + await page.waitForSelector('#group-name-basic + div') + const div1 = page.locator('#group-name-basic + div') const div2 = page.locator( '#group-name-second-instance-same-name-for-sync-test + div' diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index 99ea3dbd..d11bb6e5 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -858,6 +858,10 @@ yarn docs Try clicking different tabs above! Notice how both code groups switch together because they share the same `group-name`. ::: +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each group-name will be automatically selected. +::: + The `group-name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. Valid examples: diff --git a/src/client/app/composables/codeGroups.ts b/src/client/app/composables/codeGroups.ts index e01a2584..5d57d6ad 100644 --- a/src/client/app/composables/codeGroups.ts +++ b/src/client/app/composables/codeGroups.ts @@ -1,5 +1,28 @@ import { inBrowser, onContentUpdated } from 'vitepress' +const STORAGE_KEY = 'vitepress:tabsCache' + +function getStoredTabIndex(groupName: string): number | null { + if (!inBrowser) return null + try { + const cache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + return cache[groupName] ?? null + } catch { + return null + } +} + +function setStoredTabIndex(groupName: string, index: number) { + if (!inBrowser) return + try { + const cache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + cache[groupName] = index + localStorage.setItem(STORAGE_KEY, JSON.stringify(cache)) + } catch { + // Silently ignore localStorage errors + } +} + export function useCodeGroups() { if (import.meta.env.DEV) { onContentUpdated(() => { @@ -13,6 +36,38 @@ export function useCodeGroups() { } if (inBrowser) { + // Restore tabs from localStorage on page load, but only on first content load + let hasRestoredTabs = false + onContentUpdated(() => { + if (hasRestoredTabs) return + hasRestoredTabs = true + + document + .querySelectorAll('.vp-code-group[data-group-name]') + .forEach((group) => { + const groupName = group.getAttribute('data-group-name') + if (!groupName) return + + const storedIndex = getStoredTabIndex(groupName) + if (storedIndex === null) return + + const inputs = group.querySelectorAll('input') + const blocks = group.querySelector('.blocks') + if (!blocks || !inputs[storedIndex]) return + + // Update radio input + inputs[storedIndex].checked = true + + // Update active block + const currentActive = blocks.querySelector('.active') + const newActive = blocks.children[storedIndex] + if (currentActive && newActive && currentActive !== newActive) { + currentActive.classList.remove('active') + newActive.classList.add('active') + } + }) + }) + window.addEventListener('click', (e) => { const el = e.target as HTMLInputElement @@ -42,9 +97,10 @@ export function useCodeGroups() { const label = group?.querySelector(`label[for="${el.id}"]`) label?.scrollIntoView({ block: 'nearest' }) - // Sync other groups with same group-name + // Sync other groups with same group-name and save to localStorage const groupName = group.getAttribute('data-group-name') if (groupName) { + setStoredTabIndex(groupName, i) syncTabsInOtherGroups(groupName, i, group as HTMLElement) } }