import React, { useState, useEffect, useContext } from "react"; import { Tabs as TabsPrimitive, Select as SelectPrimitive } from "radix-ui"; // TODO: remove and use separation import { HighlightedCode } from "codehike/code"; import { CodeCopy } from "../CodeCopy"; import * as cn from "./CodeTabs.styles"; // TODO: style by highlighted? import { SyntaxHighlightedCode } from "../CodeTheme/CodeTheme"; import { useCodeSampleAnalytics } from "../CodeSample/CodeSampleAnalytics"; import { useUXEvents } from "../../uxsdk"; import { CodeContext } from "../CodeSample/CodeSample"; import { Icon, IconProvider } from "../../writer/Icon/Icon"; import { CODE_LANG_ICONS, CODE_LANG_LABELS, resolveCodeLangName, } from "../langIconSet"; export interface CodeTabsProps { description: string; highlighted: SyntaxHighlightedCode[]; className?: string; controlByMeta?: boolean; // TODO: BETTER IN THE FUTURE } export function withCodeTabs(PreComponent) { return function CodeTabs(props: CodeTabsProps) { const isSingle = props?.highlighted?.length === 1 && !props.description; const defaultValue = props.highlighted[0]?.meta || props.highlighted[0]?.lang; const {markdownFormat, activeLang, onLangChange} = useContext(CodeContext); // CONTROLLED mode: Atlas feeds a page-shared language + setter so every code // sample stays on the same language (switching one switches all). Falls back // to this sample's default when it doesn't have that language. Otherwise the // tab state is local (uncontrolled — the standalone-component behaviour). const controlled = activeLang != null && !!onLangChange; const [localTab, setLocalTab] = useState(defaultValue); const hasLang = (v?: string) => !!v && props.highlighted.some((h) => (h.meta || h.lang) === v); const activeTab = controlled ? (hasLang(activeLang) ? (activeLang as string) : defaultValue) : localTab; const codeSampleAnalytics = useCodeSampleAnalytics(); const ux = useUXEvents(); // Reset the local tab when highlighted changes (uncontrolled only). useEffect(() => { if (!controlled) setLocalTab(defaultValue); }, [defaultValue, controlled]); useEffect(() => { codeSampleAnalytics.setActiveTab(activeTab); }, []); function changeTab(value: string) { if (controlled) onLangChange?.(value); else setLocalTab(value); codeSampleAnalytics.setActiveTab(value); ux.docs.code.tab_change({ tab: value }); } if (props?.highlighted?.length === 0) { return null; } let ContentRoot = TabsPrimitive.Content if (markdownFormat) { // TODO: BETTER IN THE FUTURE ContentRoot = React.Fragment } return ( <$LanguageTabSwitcher description={props.description} highlighted={props.highlighted} value={activeTab} onValueChange={changeTab} /> {props.highlighted?.map((codeblock, i) => ( ))} ); }; } interface LanguageTabSwitcherProps { description: string; highlighted: HighlightedCode[]; value?: string; onValueChange?: (value: string) => void; } function $LanguageTabSwitcher(props: LanguageTabSwitcherProps) { const { renderLanguage, languageSwitcher, languageIcons, codeActions } = useContext(CodeContext); const isSingle = props?.highlighted?.length === 1 && !props.description; const highlighted = props.highlighted.filter( (item, index, self) => index === self.findIndex((t) => (t.meta || t.lang) === (item.meta || item.lang)) ); // The label for a language tab/option. Precedence: a consumer `renderLanguage` // override → xyd's built-in language icon (when `languageIcons` is on and the // language is known) → the raw language name. `withName` adds the pretty name // beside the icon (used in the dropdown; tabs stay icon-only). const label = ( lang: string, meta?: string, withName = false, ): React.ReactNode => { if (renderLanguage) return renderLanguage(lang, meta); if (languageIcons) { const canonical = resolveCodeLangName(lang) ?? resolveCodeLangName(meta); if (canonical) return ( <$CodeLangIcon name={canonical} title={meta || lang} withName={withName} /> ); } return meta || lang; }; // A dropdown to pick the language (opt-in) instead of the tab row — only // when there's more than one language to choose from. const asDropdown = languageSwitcher === "dropdown" && !isSingle && highlighted.length > 1; return ( {props.description && (
{props.description}
)} {asDropdown ? ( <$LanguageDropdown highlighted={highlighted} value={props.value} onValueChange={props.onValueChange} label={(lang, meta) => label(lang, meta, true)} /> ) : ( {highlighted?.map(({ meta, lang }, i) => { if (isSingle) { return null; } return ( {label(lang, meta)} ); })} )}
{highlighted?.map((codeblock, i) => ( ))} {/* Extra toolbar action(s) after copy — e.g. Atlas's run-request play. */} {codeActions}
); } interface LanguageDropdownProps { highlighted: HighlightedCode[]; value?: string; onValueChange?: (value: string) => void; label: (lang: string, meta?: string) => React.ReactNode; } // The language switcher rendered as a dropdown. Drives the same active value as // the tab row (the enclosing Tabs.Root is controlled), so the code panes swap // exactly as they do for tabs — analytics included (via `onValueChange`). function $LanguageDropdown(props: LanguageDropdownProps) { const current = props.highlighted.find((h) => (h.meta || h.lang) === props.value) || props.highlighted[0]; return ( {props.label(current.lang, current.meta)} <$ChevronsUpDown /> {props.highlighted.map(({ meta, lang }, i) => ( {props.label(lang, meta)} ))} ); } function $ChevronsUpDown() { return ( ); } // A programming-language icon from xyd's built-in code-language set, drawn via // xyd's own component — the icons live entirely inside the xyd ecosystem // (no external icon dependency). Brand-coloured; `withName` adds the pretty // display name beside the icon (the raw id stays as a tooltip). function $CodeLangIcon({ name, title, withName, }: { name: string; title?: string; withName?: boolean; }) { const entry = CODE_LANG_ICONS[name]; return ( {withName && ( {CODE_LANG_LABELS[name] ?? title ?? name} )} ); }