Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions contribute/style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,53 @@ which may need versioned documentation, we use the following custom
<ClientVersionDropdown versions={}/>
```

### How to use it
### How to use it

The `ClientVersionDropdown` component supports two APIs:

#### API 1: Inline Content (Recommended)

This approach keeps all content in the main file, which allows Docusaurus to properly generate the table of contents (TOC) for all versions.

```js
import ClientVersionDropdown from '@theme/ClientVersionDropdown/ClientVersionDropdown'
import Version from '@theme/ClientVersionDropdown/Version'

<ClientVersionDropdown versions={[
{
'version': 'v0.8+'
},
{
'version': 'v0.7.x'
}
]}>

<Version>
## Environment requirements

Your v0.8+ content here...
</Version>

<Version>
## Environment requirements {#v07-environment-requirements}

Your v0.7.x content here...
</Version>

</ClientVersionDropdown>
```

**Important Notes:**
- All content is placed directly in the main `.mdx` file
- Each `<Version>` block contains the content for one version
- The order of `<Version>` blocks must match the order in the `versions` array
- **Make header IDs unique** across versions using explicit anchor IDs (e.g., `{#v07-environment-requirements}`)
- The TOC will show only headers from the currently selected version
- The component will display the first version as 'selected' by default

#### API 2: External Snippets (Legacy)

This approach uses separate snippet files for each version. Note that this method has limitations with TOC generation.

Versioned folders are structured as follows:

Expand All @@ -352,9 +398,9 @@ Versioned folders are structured as follows:
```

* The content for each version is placed in a snippet. For example `_v0_7.mdx`
* Snippets begin with `_`
* Snippets begin with `_`
* Snippets do not contain front-matter
* These snippets import any components they may need (See `_v0_7.mdx` for example)
* These snippets import any components they may need
* They should be .mdx files
* There is a single page for all versions. For example `client.mdx`
* This page contains frontmatter
Expand All @@ -370,8 +416,8 @@ import ClientVersionDropdown from '@theme/ClientVersionDropdown/ClientVersionDro
Also import the two snippets:

```js
import v07 from './_v0_7.mdx'
import v08 from './_v0_8.mdx'
import v07 from './_snippets/_v0_7.mdx'
import v08 from './_snippets/_v0_8.mdx'
```

Pass it an array of objects representing versions and their respective snippets:
Expand Down
95 changes: 92 additions & 3 deletions src/theme/ClientVersionDropdown/ClientVersionDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ const ClientVersionDropdown = (props) => {
const buttonRef = useRef(null);
const dropdownRef = useRef(null);

// Store header IDs for each version
// Structure: { 0: ['header-1', 'header-2'], 1: ['header-3', 'header-4'] }
const [versionHeaders, setVersionHeaders] = useState({});

// Support both old API (snippet) and new API (children)
const usingChildren = props.children && React.Children.count(props.children) > 0;
const childrenArray = usingChildren ? React.Children.toArray(props.children) : [];

// Callback for Version components to report their headers
const handleHeadersCollected = (versionIndex, headerIds) => {
setVersionHeaders(prev => ({
...prev,
[versionIndex]: headerIds
}));
};

// Find version from URL parameter on initial load
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
Expand Down Expand Up @@ -135,8 +151,81 @@ const ClientVersionDropdown = (props) => {

const selectedVersion = props.versions[selectedVersionIndex];

// Get the MDXContent component
const MDXContent = selectedVersion.snippet;
// Effect to filter TOC based on selected version
useEffect(() => {
if (!usingChildren) return;

const filterTOC = () => {
// Get the header IDs for the currently selected version
const activeVersionHeaderIds = versionHeaders[selectedVersionIndex] || [];

if (activeVersionHeaderIds.length === 0) {
// Headers not collected yet, try again later
return;
}

// Find TOC - try multiple selectors for Docusaurus compatibility
const tocLinks = document.querySelectorAll(
'.table-of-contents a, .theme-doc-toc-desktop a, [class*="tocDesktop"] a, .toc a'
);

tocLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('#')) {
const headerId = href.substring(1);
const linkParent = link.parentElement; // <li> element

// Show TOC link only if its header ID is in the active version's headers
if (activeVersionHeaderIds.includes(headerId)) {
linkParent.style.display = '';
} else {
linkParent.style.display = 'none';
}
}
});
};

// Run immediately
filterTOC();

// Re-run after a delay to catch dynamically rendered TOC items
const timer = setTimeout(filterTOC, 200);

return () => clearTimeout(timer);
}, [selectedVersionIndex, usingChildren, versionHeaders]);

// Render content based on API being used
const renderContent = () => {
if (usingChildren) {
// New API: render children with show/hide based on selection
// Clone children and inject version props
return childrenArray.map((child, index) => {
const isVisible = index === selectedVersionIndex;

// Clone the child to add versionIndex and callback props
const clonedChild = React.cloneElement(child, {
versionIndex: index,
isVisible: isVisible,
onHeadersCollected: handleHeadersCollected
});

return (
<div
key={index}
style={{ display: isVisible ? 'block' : 'none' }}
data-version-index={index}
data-version-visible={isVisible}
>
{clonedChild}
</div>
);
});
} else {
// Old API: render snippet
const MDXContent = selectedVersion.snippet;
return MDXContent && typeof MDXContent === 'function' && <MDXContent />;
}
};

return (
<>
Expand All @@ -154,7 +243,7 @@ const ClientVersionDropdown = (props) => {
{renderDropdown()}

<div className={styles.snippetContainer}>
{MDXContent && typeof MDXContent === 'function' && <MDXContent />}
{renderContent()}
</div>
</>
);
Expand Down
47 changes: 47 additions & 0 deletions src/theme/ClientVersionDropdown/Version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useEffect, useRef } from 'react';

/**
* Version wrapper component for use with ClientVersionDropdown.
* Collects all heading IDs within this version for TOC filtering.
* The parent ClientVersionDropdown component handles showing/hiding based on selection.
*/
const Version = ({ children, versionIndex, isVisible, onHeadersCollected }) => {
const containerRef = useRef(null);

useEffect(() => {
// Collect heading IDs from this version
const collectHeaders = () => {
if (containerRef.current && typeof versionIndex !== 'undefined') {
const headings = containerRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6');
const headerIds = [];

headings.forEach(heading => {
if (heading.id) {
headerIds.push(heading.id);
// Mark heading with version index for reference
heading.setAttribute('data-version-index', versionIndex);
}
});

// Notify parent component of the headers in this version
if (onHeadersCollected && headerIds.length > 0) {
onHeadersCollected(versionIndex, headerIds);
}
}
};

// Run immediately and after a short delay to catch late-rendered content
collectHeaders();
const timer = setTimeout(collectHeaders, 100);

return () => clearTimeout(timer);
}, [versionIndex, children, onHeadersCollected]);

return (
<div ref={containerRef} data-version-index={versionIndex}>
{children}
</div>
);
};

export default Version;
Loading