import React, { useEffect, useRef, useState } from 'react'; import { useInRouterContext } from 'react-router-dom'; import styled from 'styled-components'; import type { JSX } from 'react'; import { useActiveTab } from '@redocly/theme/core/hooks'; import { TabList } from '@redocly/theme/markdoc/components/Tabs/TabList'; import { getTabId } from '@redocly/theme/core/utils'; export enum TabsSize { SMALL = 'small', MEDIUM = 'medium', } export type TabItemProps = { label: string; disable?: boolean; onClick?: () => void; children: React.ReactNode; icon?: React.ReactNode | string; }; type TabsProps = { id?: string; children: React.ReactElement[]; className?: string; size: TabsSize; initialTab?: string; activeTab?: string; }; type UseActiveTabFn = ( initialTab: string, tabsId: string | undefined, childrenArray: React.ReactElement[], ) => { activeTab: string; setActiveTab: (tab: string) => void; }; type TabsViewProps = Omit & { childrenArray: React.ReactElement[]; initialTab: string; labelsHash: string; useActiveTab: UseActiveTabFn; containerRef: React.RefObject; externalActiveTab?: string; }; export function Tabs({ id, children, className, size, initialTab: propInitialTab, activeTab: controlledActiveTab, }: TabsProps): JSX.Element { const childrenArray = React.Children.toArray(children) as React.ReactElement[]; const containerRef = useRef(null); const initialTab = propInitialTab ?? childrenArray[0]?.props.label ?? ''; const labelsHash = childrenArray.map((c) => c.props.label).join('|'); const inRouter = useInRouterContext(); return ( ); } function TabsView({ id, className, size, childrenArray, useActiveTab, initialTab, labelsHash, containerRef, externalActiveTab, }: TabsViewProps): JSX.Element { const { activeTab, setActiveTab } = useActiveTab(initialTab, id, childrenArray); useEffect(() => { if (externalActiveTab && externalActiveTab !== activeTab) { setActiveTab(externalActiveTab); } }, [externalActiveTab, activeTab, setActiveTab]); return ( {childrenArray.map((child, index) => { const { label } = child.props; const tabId = getTabId(label, index); return label === activeTab ? ( {child.props.children} ) : null; })} ); } /** * Validates that the active tab exists in children and resets to initial tab if not found. * Ensures tab state remains consistent when tab structure changes. */ function useValidateActiveTab( activeTab: string, setActiveTab: (tab: string) => void, childrenArray: React.ReactElement[], initialTab: string, ) { useEffect(() => { const availableLabels = childrenArray.map((child) => child.props.label); if (activeTab && !availableLabels.includes(activeTab) && availableLabels.length > 0) { setActiveTab(initialTab); } }, [childrenArray, activeTab, initialTab, setActiveTab]); } const useActiveTabWithRouter = ( initialTab: string, tabsId: string | undefined, childrenArray: React.ReactElement[], ) => { const { activeTab, setActiveTab } = useActiveTab({ initialTab, tabsId }); useValidateActiveTab(activeTab, setActiveTab, childrenArray, initialTab); return { activeTab, setActiveTab, }; }; const useActiveTabWithoutRouter = ( initialTab: string, _tabsId: string | undefined, childrenArray: React.ReactElement[], ) => { const [activeTab, setActiveTab] = useState(initialTab); useValidateActiveTab(activeTab, setActiveTab, childrenArray, initialTab); return { activeTab, setActiveTab, }; }; const TabsContainer = styled.div` position: relative; color: var(--md-tabs-container-text-color); font-size: var(--md-tabs-container-font-size); font-family: var(--md-tabs-container-font-family); font-style: var(--md-tabs-container-font-style); font-weight: var(--md-tabs-container-font-weight); background-color: var(--md-tabs-container-bg-color); margin: var(--md-tabs-container-margin); padding: var(--md-tabs-container-padding); border: var(--md-tabs-container-border); ol[class^='Tabs__TabList'] { margin: 0; padding: 0; } `; export const TabContent = styled.div` color: var(--md-tabs-content-text-color); font-size: var(--md-tabs-content-font-size); font-family: var(--md-tabs-content-font-family); font-style: var(--md-tabs-content-font-style); font-weight: var(--md-tabs-content-font-weight); background-color: var(--md-tabs-content-bg-color); margin: var(--md-tabs-content-margin); padding: var(--md-tabs-content-padding); border: var(--md-tabs-content-border); &:focus-visible { outline: none; position: relative; &::after { content: ''; position: absolute; top: -2px; right: -4px; bottom: -2px; left: -4px; border: 1px solid var(--button-border-color-focused); border-radius: 6px; pointer-events: none; } } `;