import * as React from 'react';
import {
CodeBlock as CodeBlockComponent,
type CodeBlockProps,
} from '@redocly/theme/components/CodeBlock/CodeBlock';
import { langToName } from '@redocly/theme/core/utils';
import { useActiveCodeSnippetId } from '@redocly/theme/core/contexts';
type SnippetData = {
name: string;
languageName: string;
lang: string;
props: CodeBlockProps;
id: string;
};
export function CodeGroup(props: React.PropsWithChildren<{ mode?: 'tabs' | 'dropdown' }>) {
const mode = props.mode || 'tabs';
const isTabsMode = mode === 'tabs';
const rawSnippets = React.useMemo(
() => parseSnippetsFromChildren(props.children),
[props.children],
);
const groupId = React.useMemo(() => generateGroupId(rawSnippets, mode), [rawSnippets, mode]);
const [activeSnippetId, setActiveSnippetId] = useActiveCodeSnippetId(groupId, rawSnippets);
const snippets = React.useMemo(() => {
const items = createItemsFromSnippets(rawSnippets, isTabsMode);
const itemsProps = {
items,
onChange: (id: string | string[]) => setActiveSnippetId(id as string),
value: activeSnippetId,
};
return Object.fromEntries(
rawSnippets.map((snippet: SnippetData) => {
const snippetProps = {
...snippet.props,
header: {
...snippet.props.header,
title: isTabsMode ? undefined : snippet.name,
},
...(isTabsMode ? { tabs: itemsProps } : { dropdown: itemsProps }),
};
return [snippet.id, snippetProps];
}),
);
}, [rawSnippets, activeSnippetId, isTabsMode, setActiveSnippetId]);
const activeSnippet = snippets[activeSnippetId];
if (!activeSnippet) {
return null;
}
return ;
}
function generateContentHash(content: string): number {
let hash = 0;
for (let i = 0; i < content.length; i++) {
hash = content.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash);
}
// Generate unique group ID for CodeGroup instance
// Examples: "dropdown-8901234", "tabs-1234567"
function generateGroupId(rawSnippets: SnippetData[], mode: string): string {
const content = rawSnippets.map((s) => s.id + (s.props.source || '')).join('|') + `|${mode}`;
const hash = generateContentHash(content);
return `${mode}-${hash}`;
}
function getTabName(props: CodeBlockProps, idx: number): string {
const fallbackName = `Tab ${idx + 1}`;
return String(props.header?.title || props.file || langToName(props.lang || '') || fallbackName);
}
function parseSnippetsFromChildren(children: React.ReactNode): SnippetData[] {
return React.Children.toArray(children).map((child, idx) => {
const childProps = child as React.ReactElement;
const props = childProps.props;
return {
name: getTabName(props, idx),
languageName: String(langToName(props.lang || 'Default') || ''),
lang: props.lang || '',
props,
id: `${props.lang || ''}-${idx}`,
};
});
}
function createItemsFromSnippets(snippets: SnippetData[], isTabsMode: boolean) {
return snippets.map((snippet) => ({
name: isTabsMode ? snippet.name : snippet.languageName || '',
lang: snippet.lang,
id: snippet.id,
}));
}