diff --git a/DOC-GROUP-CHANGES.md b/DOC-GROUP-CHANGES.md index bbf05e45..b15a47a7 100644 --- a/DOC-GROUP-CHANGES.md +++ b/DOC-GROUP-CHANGES.md @@ -2,10 +2,14 @@ ## Overview -Add support for named code groups via the `group-name` parameter in the code-group container syntax. This allows developers to semantically identify and potentially sync code groups across a documentation site. +Add support for named code groups via the `group-name` parameter in the code-group container syntax. This allows **synchronization of tab selections across multiple code groups** with the same name on a page. ## Feature Specification +### Purpose + +When multiple code groups share the same `group-name`, selecting a tab in one group automatically selects the corresponding tab in all other groups with the same name. This is useful for documentation that shows the same tool (e.g., package managers) in multiple contexts. + ### Markdown Syntax **Current:** @@ -117,16 +121,49 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs { - No TypeScript interface changes needed (HTML data attributes are dynamic) - Document the new attribute in JSDoc comments if applicable -### 3. Client-Side Changes (Optional for Future Enhancement) +### 3. Client-Side Changes #### File: `/src/client/app/composables/codeGroups.ts` -**Current scope:** No immediate changes required for basic implementation +**Changes Required:** +- Add tab synchronization logic when a tab is clicked +- Find all code groups with the same `data-group-name` attribute +- Update their selected tabs to match the clicked tab +- Ensure proper DOM updates for both radio inputs and active blocks -**Future enhancement possibilities:** -- Sync tab selection across code groups with the same `group-name` -- Store selection preference in localStorage per group name -- Emit events for cross-component synchronization +**Implementation:** +```typescript +function syncTabsInOtherGroups( + groupName: string, + tabIndex: number, + currentGroup: HTMLElement +) { + // Find all code groups with the same group-name + const groups = document.querySelectorAll( + `.vp-code-group[data-group-name="${groupName}"]` + ) + + groups.forEach((g) => { + // Skip the current group that was clicked + if (g === currentGroup) return + + const inputs = g.querySelectorAll('input') + const blocks = g.querySelector('.blocks') + if (!blocks || !inputs[tabIndex]) return + + // Update radio input + inputs[tabIndex].checked = true + + // Update active block + const currentActive = blocks.querySelector('.active') + const newActive = blocks.children[tabIndex] + if (currentActive && newActive && currentActive !== newActive) { + currentActive.classList.remove('active') + newActive.classList.add('active') + } + }) +} +``` ### 4. Styling Changes diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 7d19b49d..af2843bd 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -187,6 +187,20 @@ pnpm add vitepress ::: +### Group Name Second Instance (Same Name for Sync Test) + +::: code-group group-name=installs + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +::: + ### Group Name with Hyphens and Underscores ::: code-group group-name=install_methods-v2 diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 3cbc5435..312eef77 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -92,6 +92,7 @@ describe('Table of Contents', () => { "Basic Code Group", "With Other Features", "Group Name Basic", + "Group Name Second Instance (Same Name for Sync Test)", "Group Name with Hyphens and Underscores", "Group Name with Spaces (Should be Rejected)", "Group Name with Invalid Characters (Should be Rejected)", @@ -299,6 +300,47 @@ describe('Code Groups', () => { expect(await getClassList(blocks.nth(1))).toContain('active') }) + test('group-name synchronization across groups', async () => { + const div1 = page.locator('#group-name-basic + div') + const div2 = page.locator( + '#group-name-second-instance-same-name-for-sync-test + div' + ) + + // Both groups should have the same group-name + expect(await div1.getAttribute('data-group-name')).toBe('installs') + expect(await div2.getAttribute('data-group-name')).toBe('installs') + + // Initially, both should have first tab active + expect(await getClassList(div1.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + + // Click second tab in first group + await div1.locator('.tabs > label').nth(1).click() + + // Both groups should now have second tab active (synced) + expect(await getClassList(div1.locator('.blocks > div').nth(1))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(1))).toContain( + 'active' + ) + + // Click first tab in second group + await div2.locator('.tabs > label').nth(0).click() + + // Both groups should now have first tab active again (synced back) + expect(await getClassList(div1.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + }) + test('group-name with hyphens and underscores', async () => { const div = page.locator('#group-name-with-hyphens-and-underscores + div') const groupName = await div.getAttribute('data-group-name') diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index 16c0b538..ffbedf19 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -778,7 +778,7 @@ You can also [import snippets](#import-code-snippets) in code groups: ### Named Code Groups -You can optionally name code groups using the `group-name` parameter. This can be useful for semantic identification and potential future features like syncing tab selections across groups. +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name on a page, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. **Input** @@ -797,9 +797,29 @@ pnpm add vitepress yarn add vitepress ``` +::: + +Later in the same page: + +::: code-group group-name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + ::: ```` +When you click on a tab (e.g., "pnpm") in one group, all other groups with `group-name=package-managers` will automatically switch to the same tab. + **Output** ::: code-group group-name=package-managers @@ -827,7 +847,7 @@ Valid examples: - `group-name=installMethods` ::: tip -Named code groups add a `data-group-name` attribute to the generated HTML, which can be useful for custom styling or scripting. +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. ::: ## Markdown File Inclusion diff --git a/src/client/app/composables/codeGroups.ts b/src/client/app/composables/codeGroups.ts index d8a38ba7..e01a2584 100644 --- a/src/client/app/composables/codeGroups.ts +++ b/src/client/app/composables/codeGroups.ts @@ -24,6 +24,7 @@ export function useCodeGroups() { const i = Array.from(group.querySelectorAll('input')).indexOf(el) if (i < 0) return + // Update current group const blocks = group.querySelector('.blocks') if (!blocks) return @@ -40,7 +41,44 @@ export function useCodeGroups() { const label = group?.querySelector(`label[for="${el.id}"]`) label?.scrollIntoView({ block: 'nearest' }) + + // Sync other groups with same group-name + const groupName = group.getAttribute('data-group-name') + if (groupName) { + syncTabsInOtherGroups(groupName, i, group as HTMLElement) + } } }) } } + +function syncTabsInOtherGroups( + groupName: string, + tabIndex: number, + currentGroup: HTMLElement +) { + // Find all code groups with the same group-name + const groups = document.querySelectorAll( + `.vp-code-group[data-group-name="${groupName}"]` + ) + + groups.forEach((g) => { + // Skip the current group that was clicked + if (g === currentGroup) return + + const inputs = g.querySelectorAll('input') + const blocks = g.querySelector('.blocks') + if (!blocks || !inputs[tabIndex]) return + + // Update radio input + inputs[tabIndex].checked = true + + // Update active block + const currentActive = blocks.querySelector('.active') + const newActive = blocks.children[tabIndex] + if (currentActive && newActive && currentActive !== newActive) { + currentActive.classList.remove('active') + newActive.classList.add('active') + } + }) +}