import * as React from "react"; import { ReactNode } from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; import { cn } from "../libs/utils"; import { SelectBox } from "./selectBox"; export interface Tab { name: string; current?: boolean; href?: string; label: ReactNode; content: ReactNode; disabled?: boolean; is_allowed?: boolean; } const TabsContext = React.createContext<{ size?: number; tabs?: Tab[]; current?: string; setTab?: (name: string) => void; responsive?: boolean; variant?: "tabs" | "pills"; updateHash?: boolean; }>({ size: undefined, tabs: undefined, current: undefined, setTab: undefined, responsive: false, variant: "tabs", updateHash: true }); interface TabsProps { current?: string | (() => string); tabs: Tab[]; defaultValue?: string; className?: string; fullWidth?: boolean; fullHeight?: boolean; children?: React.ReactNode; onTabChange?: (tabName: string) => void; responsive?: boolean; variant?: "tabs" | "pills"; updateHash?: boolean; } const Tabs = ({ tabs, defaultValue, current, className, fullWidth, fullHeight, children, onTabChange, responsive = false, variant = "tabs", updateHash = true }: TabsProps) => { // Filter tabs based on is_allowed (undefined or true means visible) const visibleTabs = React.useMemo(() => tabs.filter(tab => tab.is_allowed === undefined || tab.is_allowed === true), [tabs] ); // Initialize value const [value, setValue] = React.useState(() => { // First check if current is provided const currentValue = typeof current === 'function' ? current() : current; if (currentValue) { return currentValue; } // Then check hash const hash = window.location.hash; const currentTab = hash ? hash.substring(1) : undefined; // Check if the tab from hash exists in visible tabs if (currentTab && visibleTabs.some(tab => tab.name === currentTab)) { return currentTab; } // Fall back to default or first visible tab return defaultValue || visibleTabs[0]?.name; }); // Update when current prop changes (but don't create a loop) React.useEffect(() => { const currentValue = typeof current === 'function' ? current() : current; if (currentValue && currentValue !== value) { setValue(currentValue); } }, [current]); // Listen to hash changes only when there's no current prop being controlled externally React.useEffect(() => { if (current) return; // Skip hash handling if controlled by parent const handleHashChange = () => { const hash = window.location.hash; const currentTab = hash ? hash.substring(1) : undefined; // Only update if the tab exists in visible tabs if (currentTab && visibleTabs.some(tab => tab.name === currentTab)) { setValue(currentTab); } else if (!hash && defaultValue) { // If no hash, fall back to default setValue(defaultValue); } }; // Check initial hash handleHashChange(); window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); }, [current, visibleTabs, defaultValue]); const handleValueChange = (newValue: string) => { setValue(newValue); // Update the URL hash when tab changes (only if updateHash is true and not controlled by parent) if (updateHash && !current) { // Preserve existing history state when changing hash const currentState = window.history.state; const newUrl = window.location.pathname + window.location.search + '#' + newValue; window.history.pushState(currentState, '', newUrl); } if (onTabChange) { onTabChange(newValue); } }; const setTab = React.useCallback((tabName: string) => { handleValueChange(tabName); }, [handleValueChange]); return ( {children} ); }; interface TabsBarProps { className?: string; sticky?: boolean; direction?: "vertical" | "horizontal"; } const TabsBar = ({ className, sticky, direction }: TabsBarProps) => { const { tabs, size, current, setTab, responsive, variant, updateHash } = React.useContext(TabsContext); const fullWidth = size !== 0; const handleTabChange = React.useCallback((tabName: string) => { if (!tabs || !setTab) return; const tab = tabs.find(t => t.name === tabName); if (tab?.href && updateHash) { // Preserve existing history state when changing tabs const currentState = window.history.state; window.history.pushState(currentState, '', tab.href); } setTab(tabName); }, [tabs, setTab, updateHash]); if (!tabs || !setTab) { console.warn("TabsBar: No tabs provided or setTab not available"); return null; } return ( <> {responsive && (
typeof tab.label === 'string' ? tab.label : String(tab.label)} value={tabs.find(tab => tab.name === current)} onChange={(tab: Tab) => { handleTabChange(tab.name); }} />
)} {tabs.map((tab) => ( handleTabChange(tab.name)} > {tab.label} ))} ); }; const TabsPanel = ({ className }: { className?: string }) => { const { tabs } = React.useContext(TabsContext); if (!tabs) return null; return ( <> {tabs.map((tab) => ( {tab.content} ))} ); }; type TabsListProps = React.ComponentPropsWithoutRef & { size?: number; variant?: "tabs" | "pills" }; const TabsList: React.ForwardRefExoticComponent>> = React.forwardRef< React.ElementRef, TabsListProps >(({ className, size, variant = "tabs", ...props }, ref) => ( )); TabsList.displayName = TabsPrimitive.List.displayName; type TabsTriggerProps = React.ComponentPropsWithoutRef & { href?: string; variant?: "tabs" | "pills"; }; const TabsTrigger: React.ForwardRefExoticComponent>> = React.forwardRef< React.ElementRef, TabsTriggerProps >(({ className, href, variant = "tabs", ...props }, ref) => { const { size } = React.useContext(TabsContext); const handleClick = React.useCallback((event: React.MouseEvent) => { if (href) { event.preventDefault(); // Preserve existing history state when changing tabs const currentState = window.history.state; window.history.pushState(currentState, '', href); } if (props.onClick) { (props.onClick as React.MouseEventHandler)(event); } }, [href, props.onClick]); return ( ); }); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; type TabsContentProps = React.ComponentPropsWithoutRef; const TabsContent: React.ForwardRefExoticComponent>> = React.forwardRef< React.ElementRef, TabsContentProps >(({ className, ...props }, ref) => ( )); TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsBar, TabsPanel, TabsList, TabsTrigger, TabsContent };