1- import { ReactNode , createContext , useContext , useCallback , useState , useEffect } from 'react' ;
1+ import { FocusableRefValue } from '@react-types/shared' ;
2+ import {
3+ ReactNode ,
4+ createContext ,
5+ useContext ,
6+ useState ,
7+ useMemo ,
8+ useLayoutEffect ,
9+ useRef ,
10+ useEffect ,
11+ } from 'react' ;
212import { Action , tasty , CloseIcon , Styles } from '@cube-dev/ui-kit' ;
313
14+ import { useEvent } from '../../hooks' ;
15+
16+ interface TabData {
17+ content : ReactNode ;
18+ prerender : boolean ;
19+ keepMounted : boolean ;
20+ }
21+
422interface TabsContextValue {
523 type ?: 'default' | 'card' ;
624 size ?: 'normal' | 'large' ;
725 activeKey ?: string ;
826 extra ?: ReactNode ;
9- setContent : ( content ?: ReactNode ) => void ;
27+ setTabContent : ( id : string , content : TabData | null ) => void ;
28+ prerender ?: boolean ;
29+ keepMounted ?: boolean ;
1030 onChange : ( key : string ) => void ;
1131 onDelete ?: ( key : string ) => void ;
1232}
1333
34+ interface TabsProps extends Omit < TabsContextValue , 'setTabContent' > {
35+ label ?: string ;
36+ children ?: ReactNode ;
37+ styles ?: Styles ;
38+ size ?: TabsContextValue [ 'size' ] ;
39+ }
40+
41+ interface TabProps {
42+ id : string ;
43+ title : ReactNode ;
44+ children ?: ReactNode ;
45+ isDisabled ?: boolean ;
46+ qa ?: string ;
47+ qaVal ?: string ;
48+ styles ?: Styles ;
49+ size ?: TabsContextValue [ 'size' ] ;
50+ extra ?: ReactNode ;
51+ prerender ?: boolean ;
52+ keepMounted ?: boolean ;
53+ }
54+
1455const TabsContext = createContext < TabsContextValue | undefined > ( undefined ) ;
1556
1657const TabsElement = tasty ( {
@@ -23,11 +64,15 @@ const TabsElement = tasty({
2364 shadow : 'inset 0 -1bw 0 #border' ,
2465 width : '100%' ,
2566 padding : '0 2x' ,
67+ scrollbarWidth : 'none' ,
2668
2769 Container : {
2870 display : 'grid' ,
2971 gridAutoFlow : 'column' ,
30- gap : '0' ,
72+ gap : {
73+ '' : 0 ,
74+ card : '1bw' ,
75+ } ,
3176 placeContent : 'start' ,
3277 } ,
3378
@@ -49,6 +94,7 @@ const TabContainer = tasty({
4994
5095const TabElement = tasty ( Action , {
5196 styles : {
97+ position : 'relative' ,
5298 preset : {
5399 '' : 't3m' ,
54100 '[data-size="large"]' : 't2m' ,
@@ -74,6 +120,7 @@ const TabElement = tasty(Action, {
74120 '' : '#dark-02' ,
75121 hovered : '#purple' ,
76122 active : '#purple-text' ,
123+ 'disabled & !active' : '#dark-04' ,
77124 } ,
78125 borderBottom : {
79126 '' : 'none' ,
@@ -89,11 +136,27 @@ const TabElement = tasty(Action, {
89136 width : 'max 100%' ,
90137 transition : 'theme, borderBottom' ,
91138 whiteSpace : 'nowrap' ,
139+ outline : false ,
92140
93141 '@delete-padding' : {
94142 '' : '1.5x' ,
95143 deletable : '4.5x' ,
96144 } ,
145+
146+ '&::before' : {
147+ content : '""' ,
148+ display : 'block' ,
149+ position : 'absolute' ,
150+ inset : '0 0 -1ow 0' ,
151+ pointerEvents : 'none' ,
152+ radius : 'top' ,
153+ shadow : {
154+ '' : 'inset 0 0 0 #purple' ,
155+ focused : 'inset 0 0 0 1ow #purple-03' ,
156+ } ,
157+ transition : 'theme' ,
158+ zIndex : 1 ,
159+ } ,
97160 } ,
98161} ) ;
99162
@@ -122,80 +185,133 @@ const TabCloseButton = tasty(Action, {
122185 children : < CloseIcon /> ,
123186} ) ;
124187
125- interface TabsProps extends Omit < TabsContextValue , 'setContent' > {
126- label ?: string ;
127- children ?: ReactNode ;
128- styles ?: Styles ;
129- size ?: TabsContextValue [ 'size' ] ;
130- }
131-
132- interface TabProps {
133- id : string ;
134- title : ReactNode ;
135- children ?: ReactNode ;
136- isDisabled ?: boolean ;
137- qa ?: string ;
138- styles ?: Styles ;
139- size ?: TabsContextValue [ 'size' ] ;
140- extra ?: ReactNode ;
141- }
142-
143188export function Tabs ( props : TabsProps ) {
144- const [ content , setContent ] = useState < ReactNode > ( null ) ;
145- const { label, activeKey, size, type, onChange, onDelete, children, styles, extra } = props ;
189+ const [ contentMap , setContentMap ] = useState < Map < string , TabData > > ( new Map ( ) ) ;
190+ const {
191+ label,
192+ activeKey,
193+ size,
194+ type,
195+ onChange,
196+ onDelete,
197+ children,
198+ styles,
199+ extra,
200+ prerender,
201+ keepMounted,
202+ } = props ;
146203
147204 const isCardType = type === 'card' ;
148205
206+ // Update the content map whenever the activeKey changes
207+ const setTabContent = useEvent ( ( id : string , content : TabData | null ) => {
208+ setContentMap ( ( prev ) => {
209+ const newMap = new Map ( prev ) ;
210+ if ( content ) {
211+ newMap . set ( id , content ) ;
212+ } else {
213+ newMap . delete ( id ) ;
214+ }
215+
216+ return newMap ;
217+ } ) ;
218+ } ) ;
219+
220+ const mods = useMemo ( ( ) => ( { card : isCardType , deletable : ! ! onDelete } ) , [ isCardType , onDelete ] ) ;
221+
149222 return (
150- < TabsContext . Provider value = { { activeKey, onChange, onDelete, type, size, setContent } } >
223+ < TabsContext . Provider
224+ value = { { activeKey, onChange, onDelete, type, size, setTabContent, prerender, keepMounted } }
225+ >
151226 < TabsElement
152227 qa = "Tabs"
153228 aria-label = { label ?? 'Tabs' }
154229 data-size = { size ?? 'normal' }
155- mods = { { card : isCardType } }
230+ mods = { mods }
156231 styles = { styles }
157232 >
158233 < div data-element = "Container" > { children } </ div >
159234 { extra ? < div data-element = "Extra" > { extra } </ div > : null }
160235 </ TabsElement >
161- { content }
236+ { [ ...contentMap . entries ( ) ] . map ( ( [ id , { content, prerender, keepMounted } ] ) =>
237+ prerender || id === activeKey || keepMounted ? (
238+ < div
239+ key = { id }
240+ data-qa = "TabPanel"
241+ data-qaval = { id }
242+ style = { {
243+ display : id === activeKey ? 'contents' : 'none' ,
244+ } }
245+ >
246+ { content }
247+ </ div >
248+ ) : null
249+ ) }
162250 </ TabsContext . Provider >
163251 ) ;
164252}
165253
166254export function Tab ( props : TabProps ) {
167- const { title, id, isDisabled, qa, styles, children } = props ;
168- const { activeKey, size, type, onChange, onDelete, setContent } = useContext ( TabsContext ) || { } ;
255+ let { title, id, isDisabled, prerender, keepMounted, qa, qaVal, styles, children } = props ;
256+
257+ const ref = useRef < FocusableRefValue > ( null ) ;
258+
259+ const { activeKey, size, type, onChange, onDelete, setTabContent, ...contextProps } =
260+ useContext ( TabsContext ) || ( { } as TabsContextValue ) ;
261+
262+ prerender = prerender ?? contextProps . prerender ;
263+ keepMounted = keepMounted ?? contextProps . keepMounted ;
169264
170265 const isActive = id === activeKey ;
171266
172- const onDeleteCallback = useCallback ( ( ) => {
267+ const onDeleteCallback = useEvent ( ( ) => {
173268 onDelete ?.( id ) ;
174- } , [ onDelete , id ] ) ;
175- const onChangeCallback = useCallback ( ( ) => {
269+ } ) ;
270+ const onChangeCallback = useEvent ( ( ) => {
176271 onChange ?.( id ) ;
177- } , [ id ] ) ;
272+ } ) ;
178273
179274 const isCardType = type === 'card' ;
180- const isDeletable = onDelete && isCardType ;
275+ const isDeletable = ! ! onDelete ;
276+
277+ useLayoutEffect ( ( ) => {
278+ if ( prerender || isActive ) {
279+ setTabContent ?.( id , {
280+ content : children ,
281+ prerender : prerender ?? false ,
282+ keepMounted : keepMounted ?? false ,
283+ } ) ;
284+ } else if ( ! keepMounted ) {
285+ setTabContent ?.( id , null ) ;
286+ }
287+ } , [ children , isActive , keepMounted , prerender , setTabContent ] ) ;
288+
289+ useLayoutEffect ( ( ) => {
290+ return ( ) => {
291+ setTabContent ?.( id , null ) ;
292+ } ;
293+ } , [ ] ) ;
294+
295+ const mods = useMemo (
296+ ( ) => ( { card : isCardType , active : isActive , deletable : isDeletable , disabled : isDisabled } ) ,
297+ [ isCardType , isActive , isDeletable , isDisabled ]
298+ ) ;
181299
182300 useEffect ( ( ) => {
183- if ( isActive ) {
184- setContent ?. ( children || null ) ;
301+ if ( ref . current && isActive ) {
302+ ref . current . UNSAFE_getDOMNode ( ) ?. scrollIntoView ?. ( ) ;
185303 }
186- } , [ activeKey , children ] ) ;
304+ } , [ isActive ] ) ;
187305
188306 return (
189- < TabContainer >
307+ < TabContainer mods = { mods } >
190308 < TabElement
191- qa = { `Tab-${ id } ` ?? qa }
309+ ref = { ref }
310+ qa = { qa ?? `Tab-${ id } ` }
311+ qaVal = { qaVal }
192312 isDisabled = { isDisabled }
193313 styles = { styles }
194- mods = { {
195- active : isActive ,
196- card : isCardType ,
197- deletable : isDeletable ,
198- } }
314+ mods = { mods }
199315 data-size = { size }
200316 onPress = { onChangeCallback }
201317 >
0 commit comments