'use client'; import { type ComponentProps, createContext, useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import * as Primitive from '@radix-ui/react-tabs'; import { mergeRefs } from '../lib/merge-refs'; import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event'; type ChangeListener = (v: string) => void; const listeners = new Map(); function addChangeListener(id: string, listener: ChangeListener): void { const list = listeners.get(id) ?? []; list.push(listener); listeners.set(id, list); } function removeChangeListener(id: string, listener: ChangeListener): void { const list = listeners.get(id) ?? []; listeners.set( id, list.filter((item) => item !== listener), ); } export interface TabsProps extends ComponentProps { /** * Identifier for Sharing value of tabs */ groupId?: string; /** * Enable persistent */ persist?: boolean; /** * If true, updates the URL hash based on the tab's id */ updateAnchor?: boolean; } const TabsContext = createContext<{ valueToIdMap: Map; } | null>(null); function useTabContext() { const ctx = useContext(TabsContext); if (!ctx) throw new Error('You must wrap your component in '); return ctx; } export const TabsList = Primitive.TabsList; export const TabsTrigger = Primitive.TabsTrigger; /** * @internal You better not use it */ export function Tabs({ ref, groupId, persist = false, updateAnchor = false, defaultValue, value: _value, onValueChange: _onValueChange, ...props }: TabsProps) { const tabsRef = useRef(null); const [value, setValue] = _value === undefined ? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled useState(defaultValue) : [_value, _onValueChange ?? (() => undefined)]; const onChange = useEffectEvent((v: string) => setValue(v)); const valueToIdMap = useMemo(() => new Map(), []); useLayoutEffect(() => { if (!groupId) return; const previous = persist ? localStorage.getItem(groupId) : sessionStorage.getItem(groupId); if (previous) onChange(previous); addChangeListener(groupId, onChange); return () => { removeChangeListener(groupId, onChange); }; }, [groupId, persist]); useLayoutEffect(() => { const hash = window.location.hash.slice(1); if (!hash) return; for (const [value, id] of valueToIdMap.entries()) { if (id === hash) { onChange(value); tabsRef.current?.scrollIntoView(); break; } } }, [valueToIdMap]); return ( { if (updateAnchor) { const id = valueToIdMap.get(v); if (id) { window.history.replaceState(null, '', `#${id}`); } } if (groupId) { listeners.get(groupId)?.forEach((item) => { item(v); }); if (persist) localStorage.setItem(groupId, v); else sessionStorage.setItem(groupId, v); } else { setValue(v); } }} {...props} > ({ valueToIdMap }), [valueToIdMap])} > {props.children} ); } export function TabsContent({ value, ...props }: ComponentProps) { const { valueToIdMap } = useTabContext(); if (props.id) { valueToIdMap.set(value, props.id); } return ( {props.children} ); }