feat: implement tab synchronization for named code groups

- Add syncTabsInOtherGroups function to sync tabs across groups
- When clicking a tab in a named group, all groups with same name sync
- Add E2E test for synchronization behavior
- Update documentation to explain sync purpose and usage
- Add second test fixture for testing sync between groups
pull/5012/head
juji 1 month ago
parent 2c33bedac2
commit 6e26da905e

@ -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

@ -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

@ -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')

@ -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

@ -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')
}
})
}

Loading…
Cancel
Save