import classNames from 'classnames'; import type { ReactNode } from 'react'; import { memo, useEffect, useRef } from 'react'; import { ErrorBoundary } from '../../ErrorBoundary'; import type { ButtonProps } from '../Button'; import { Button } from '../Button'; import { Icon } from '../Icon'; import type { RadioDropdownProps } from '../RadioDropdown'; import { RadioDropdown } from '../RadioDropdown'; export type TabItem = | { value: string; label: string; hidden?: boolean; rightSlot?: ReactNode; } | { value: string; options: Omit; rightSlot?: ReactNode; }; interface Props { label: string; value?: string; onChangeValue: (value: string) => void; tabs: TabItem[]; tabListClassName?: string; className?: string; children: ReactNode; addBorders?: boolean; layout?: 'horizontal' | 'vertical'; } export function Tabs({ value, onChangeValue, label, children, tabs, className, tabListClassName, addBorders, layout = 'vertical', }: Props) { const ref = useRef(null); value = value ?? tabs[0]?.value; // Update tabs when value changes useEffect(() => { const tabs = ref.current?.querySelectorAll(`[data-tab]`); for (const tab of tabs ?? []) { const v = tab.getAttribute('data-tab'); const parent = tab.closest('.tabs-container'); if (parent !== ref.current) { // Tab is part of a nested tab container, so ignore it } else if (v === value) { tab.setAttribute('data-state', 'active'); tab.setAttribute('aria-hidden', 'false'); tab.style.display = 'block'; } else { tab.setAttribute('data-state', 'inactive'); tab.setAttribute('aria-hidden', 'true'); tab.style.display = 'none'; } } }, [value]); return (
{tabs.map((t) => { if ('hidden' in t && t.hidden) { return null; } const isActive = t.value === value; const btnProps: Partial = { size: 'sm', color: 'custom', justify: layout === 'horizontal' ? 'start' : 'center', onClick: isActive ? undefined : () => onChangeValue(t.value), className: classNames( 'flex items-center rounded whitespace-nowrap', '!px-2 ml-[1px]', 'outline-none', 'ring-none', 'focus-visible-or-class:outline-2', addBorders && 'border focus-visible:bg-surface-highlight', isActive ? 'text-text' : 'text-text-subtle', isActive && addBorders ? 'border-surface-active bg-surface-active' : layout === 'vertical' ? 'border-border-subtle' : 'border-transparent', layout === 'horizontal' && 'min-w-[10rem]', ), }; if ('options' in t) { const option = t.options.items.find( (i) => 'value' in i && i.value === t.options?.value, ); return (
} {...btnProps} > {option && 'shortLabel' in option && option.shortLabel ? option.shortLabel : (option?.label ?? 'Unknown')} ); } else { return ( ); } })}
{children} ); } interface TabContentProps { value: string; children: ReactNode; className?: string; } export const TabContent = memo(function TabContent({ value, children, className, }: TabContentProps) { return (
{children}
); });