'use client'; import * as React from 'react'; import { type ComponentProps, createContext, type ReactNode, useContext, useEffect, useId, useMemo, useState, } from 'react'; import { cn } from '../lib/cn'; import * as Unstyled from './tabs.unstyled'; type CollectionKey = string | symbol; export interface TabsProps extends Omit< ComponentProps, 'value' | 'onValueChange' > { /** * Use simple mode instead of advanced usage as documented in https://radix-ui.com/primitives/docs/components/tabs. */ items?: string[]; /** * Shortcut for `defaultValue` when `items` is provided. * * @defaultValue 0 */ defaultIndex?: number; /** * Additional label in tabs list when `items` is provided. */ label?: ReactNode; } const TabsContext = createContext<{ items?: string[]; collection: CollectionKey[]; } | null>(null); function useTabContext() { const ctx = useContext(TabsContext); if (!ctx) throw new Error('You must wrap your component in '); return ctx; } export const TabsList = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >((props, ref) => ( )); TabsList.displayName = 'TabsList'; export const TabsTrigger = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >((props, ref) => ( )); TabsTrigger.displayName = 'TabsTrigger'; export function Tabs({ ref, className, items, label, defaultIndex = 0, defaultValue = items ? escapeValue(items[defaultIndex]) : undefined, ...props }: TabsProps) { const [value, setValue] = useState(defaultValue); const collection = useMemo(() => [], []); return ( { if (items && !items.some((item) => escapeValue(item) === v)) return; setValue(v); }} {...props} > {items && ( {label && ( {label} )} {items.map((item) => ( {item} ))} )} ({ items, collection }), [collection, items])} > {props.children} ); } export interface TabProps extends Omit, 'value'> { /** * Value of tab, detect from index if unspecified. */ value?: string; } export function Tab({ value, ...props }: TabProps) { const { items } = useTabContext(); const resolved = value ?? // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change items?.at(useCollectionIndex()); if (!resolved) throw new Error( 'Failed to resolve tab `value`, please pass a `value` prop to the Tab component.', ); return ( {props.children} ); } export function TabsContent({ value, className, ...props }: ComponentProps) { return ( figure:only-child]:-m-4 [&>figure:only-child]:border-none', className, )} {...props} > {props.children} ); } /** * Inspired by Headless UI. * * Return the index of children, this is made possible by registering the order of render from children using React context. * This is supposed by work with pre-rendering & pure client-side rendering. */ function useCollectionIndex() { const key = useId(); const { collection } = useTabContext(); useEffect(() => { return () => { const idx = collection.indexOf(key); if (idx !== -1) collection.splice(idx, 1); }; }, [key, collection]); if (!collection.includes(key)) collection.push(key); return collection.indexOf(key); } /** * only escape whitespaces in values in simple mode */ function escapeValue(v: string): string { return v.toLowerCase().replace(/\s/, '-'); }