Tabs.tsx171 lines · main
1'use client'
2
3import { Tabs as TabsPrimitive } from 'radix-ui'
4import {
5 Children,
6 isValidElement,
7 useMemo,
8 useState,
9 type KeyboardEvent,
10 type PropsWithChildren,
11 type RefObject,
12} from 'react'
13
14import styleHandler from '../../lib/theme/styleHandler'
15
16export interface TabsProps {
17 type?: 'pills' | 'underlined' | 'cards' | 'rounded-pills'
18 defaultActiveId?: string
19 activeId?: string
20 size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
21 block?: boolean
22 tabBarGutter?: number
23 tabBarStyle?: React.CSSProperties
24 onChange?: any
25 onClick?: any
26 scrollable?: boolean
27 wrappable?: boolean
28 addOnBefore?: React.ReactNode
29 addOnAfter?: React.ReactNode
30 listClassNames?: string
31 baseClassNames?: string
32 refs?: {
33 base: RefObject<HTMLDivElement> | ((elem: HTMLDivElement | null) => void)
34 list: RefObject<HTMLDivElement> | ((elem: HTMLDivElement | null) => void)
35 }
36}
37
38interface TabsSubComponents {
39 Panel: React.FC<PropsWithChildren<PanelProps>>
40}
41
42/**
43 * @deprecated Use `import { Tabs_shadcn_ } from "ui"` instead
44 */
45const Tabs: React.FC<PropsWithChildren<TabsProps>> & TabsSubComponents = ({
46 defaultActiveId,
47 activeId,
48 type = 'pills',
49 size = 'tiny',
50 block,
51 onChange,
52 onClick,
53 scrollable,
54 wrappable,
55 addOnBefore,
56 addOnAfter,
57 listClassNames,
58 baseClassNames,
59 refs,
60 children: _children,
61}) => {
62 // Children.toArray clones elements (to assign keys) and accesses element.ref
63 // internally, which triggers a React 19 warning. Children.forEach iterates
64 // without cloning, so it never touches .ref
65 // todo: remove this when we upgrade the radix packages to support react 19
66 const childrenArr: PanelPropsProps[] = []
67 Children.forEach(_children, (child) => {
68 if (isValidElement(child)) childrenArr.push(child as unknown as PanelPropsProps)
69 })
70 const children = childrenArr
71
72 const [activeTab, setActiveTab] = useState(
73 activeId ??
74 defaultActiveId ??
75 // if no defaultActiveId is set use the first panel
76 children?.[0]?.props?.id
77 )
78
79 useMemo(() => {
80 if (activeId && activeId !== activeTab) setActiveTab(activeId)
81 }, [activeId])
82
83 let __styles = styleHandler('tabs')
84
85 function onTabClick(id: string) {
86 onClick?.(id)
87 if (id !== activeTab) {
88 onChange?.(id)
89 setActiveTab(id)
90 }
91 }
92
93 const listClasses = [__styles[type].list]
94 if (scrollable) listClasses.push(__styles.scrollable)
95 if (wrappable) listClasses.push(__styles.wrappable)
96 if (listClassNames) listClasses.push(listClassNames)
97
98 return (
99 <TabsPrimitive.Root
100 value={activeTab}
101 className={[__styles.base, baseClassNames].join(' ')}
102 ref={refs?.base}
103 >
104 <TabsPrimitive.List className={listClasses.join(' ')} ref={refs?.list}>
105 {addOnBefore}
106 {children.map((tab) => {
107 const isActive = activeTab === tab.props.id
108 const triggerClasses = [__styles[type].base, __styles.size[size]]
109 if (isActive) {
110 triggerClasses.push(__styles[type].active)
111 } else {
112 triggerClasses.push(__styles[type].inactive)
113 }
114 if (block) {
115 triggerClasses.push(__styles.block)
116 }
117
118 return (
119 <TabsPrimitive.Trigger
120 onKeyDown={(e: KeyboardEvent<HTMLButtonElement>) => {
121 if (e.key === 'Enter') {
122 e.preventDefault()
123 onTabClick(tab.props.id)
124 }
125 }}
126 onClick={() => onTabClick(tab.props.id)}
127 key={`${tab.props.id}-tab-button`}
128 value={tab.props.id}
129 className={triggerClasses.join(' ')}
130 >
131 {tab.props.icon}
132 <span>{tab.props.label}</span>
133 {tab.props.iconRight}
134 </TabsPrimitive.Trigger>
135 )
136 })}
137 {addOnAfter}
138 </TabsPrimitive.List>
139 {children as any}
140 </TabsPrimitive.Root>
141 )
142}
143
144// bit of a hack because we map over the JSX in the parent component
145interface PanelPropsProps {
146 props: PanelProps
147}
148
149interface PanelProps {
150 id: string
151 label?: string
152 icon?: React.ReactNode
153 iconRight?: React.ReactNode
154 className?: string
155}
156
157/**
158 * @deprecated Use ./TabsContent_Shadcn_ instead
159 */
160export const Panel: React.FC<PropsWithChildren<PanelProps>> = ({ children, id, className }) => {
161 let __styles = styleHandler('tabs')
162
163 return (
164 <TabsPrimitive.Content value={id} className={[__styles.content, className].join(' ')}>
165 {children}
166 </TabsPrimitive.Content>
167 )
168}
169
170Tabs.Panel = Panel
171export default Tabs