@@ -23,15 +23,15 @@ import { render, Features, PropsForFeatures, forwardRefWithAs } from '../../util
23
23
import { useId } from '../../hooks/use-id'
24
24
import { match } from '../../utils/match'
25
25
import { Keys } from '../../components/keyboard'
26
- import { focusIn , Focus } from '../../utils/focus-management'
26
+ import { focusIn , Focus , sortByDomNode } from '../../utils/focus-management'
27
27
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
28
28
import { useSyncRefs } from '../../hooks/use-sync-refs'
29
29
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
30
30
import { useLatestValue } from '../../hooks/use-latest-value'
31
31
import { FocusSentinel } from '../../internal/focus-sentinel'
32
32
33
33
interface StateDefinition {
34
- selectedIndex : number | null
34
+ selectedIndex : number
35
35
36
36
orientation : 'horizontal' | 'vertical'
37
37
activation : 'auto' | 'manual'
@@ -71,8 +71,29 @@ let reducers: {
71
71
) => StateDefinition
72
72
} = {
73
73
[ ActionTypes . SetSelectedIndex ] ( state , action ) {
74
- if ( state . selectedIndex === action . index ) return state
75
- return { ...state , selectedIndex : action . index }
74
+ let focusableTabs = state . tabs . filter ( ( tab ) => ! tab . current ?. hasAttribute ( 'disabled' ) )
75
+
76
+ // Underflow
77
+ if ( action . index < 0 ) {
78
+ return { ...state , selectedIndex : state . tabs . indexOf ( focusableTabs [ 0 ] ) }
79
+ }
80
+
81
+ // Overflow
82
+ else if ( action . index > state . tabs . length ) {
83
+ return {
84
+ ...state ,
85
+ selectedIndex : state . tabs . indexOf ( focusableTabs [ focusableTabs . length - 1 ] ) ,
86
+ }
87
+ }
88
+
89
+ // Middle
90
+ let before = state . tabs . slice ( 0 , action . index )
91
+ let after = state . tabs . slice ( action . index )
92
+
93
+ let next = [ ...after , ...before ] . find ( ( tab ) => focusableTabs . includes ( tab ) )
94
+ if ( ! next ) return state
95
+
96
+ return { ...state , selectedIndex : state . tabs . indexOf ( next ) }
76
97
} ,
77
98
[ ActionTypes . SetOrientation ] ( state , action ) {
78
99
if ( state . orientation === action . orientation ) return state
@@ -84,10 +105,16 @@ let reducers: {
84
105
} ,
85
106
[ ActionTypes . RegisterTab ] ( state , action ) {
86
107
if ( state . tabs . includes ( action . tab ) ) return state
87
- return { ...state , tabs : [ ...state . tabs , action . tab ] }
108
+ return { ...state , tabs : sortByDomNode ( [ ...state . tabs , action . tab ] , ( tab ) => tab . current ) }
88
109
} ,
89
110
[ ActionTypes . UnregisterTab ] ( state , action ) {
90
- return { ...state , tabs : state . tabs . filter ( ( tab ) => tab !== action . tab ) }
111
+ return {
112
+ ...state ,
113
+ tabs : sortByDomNode (
114
+ state . tabs . filter ( ( tab ) => tab !== action . tab ) ,
115
+ ( tab ) => tab . current
116
+ ) ,
117
+ }
91
118
} ,
92
119
[ ActionTypes . RegisterPanel ] ( state , action ) {
93
120
if ( state . panels . includes ( action . panel ) ) return state
@@ -106,9 +133,21 @@ let TabsContext = createContext<
106
133
> ( null )
107
134
TabsContext . displayName = 'TabsContext'
108
135
109
- let TabsSSRContext = createContext < MutableRefObject < number > | null > ( null )
136
+ let TabsSSRContext = createContext < MutableRefObject < { tabs : string [ ] ; panels : string [ ] } > | null > (
137
+ null
138
+ )
110
139
TabsSSRContext . displayName = 'TabsSSRContext'
111
140
141
+ function useSSRTabsCounter ( component : string ) {
142
+ let context = useContext ( TabsSSRContext )
143
+ if ( context === null ) {
144
+ let err = new Error ( `<${ component } /> is missing a parent <Tab.Group /> component.` )
145
+ if ( Error . captureStackTrace ) Error . captureStackTrace ( err , useSSRTabsCounter )
146
+ throw err
147
+ }
148
+ return context
149
+ }
150
+
112
151
function useTabsContext ( component : string ) {
113
152
let context = useContext ( TabsContext )
114
153
if ( context === null ) {
@@ -153,7 +192,7 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
153
192
154
193
let tabsRef = useSyncRefs ( ref )
155
194
let [ state , dispatch ] = useReducer ( stateReducer , {
156
- selectedIndex : typeof window === 'undefined' ? selectedIndex ?? defaultIndex : null ,
195
+ selectedIndex : selectedIndex ?? defaultIndex ,
157
196
tabs : [ ] ,
158
197
panels : [ ] ,
159
198
orientation,
@@ -172,38 +211,9 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
172
211
} , [ activation ] )
173
212
174
213
useIsoMorphicEffect ( ( ) => {
175
- if ( state . tabs . length <= 0 ) return
176
- if ( selectedIndex === null && state . selectedIndex !== null ) return
177
-
178
- let tabs = state . tabs . map ( ( tab ) => tab . current ) . filter ( Boolean ) as HTMLElement [ ]
179
- let focusableTabs = tabs . filter ( ( tab ) => ! tab . hasAttribute ( 'disabled' ) )
180
-
181
214
let indexToSet = selectedIndex ?? defaultIndex
182
-
183
- // Underflow
184
- if ( indexToSet < 0 ) {
185
- dispatch ( { type : ActionTypes . SetSelectedIndex , index : tabs . indexOf ( focusableTabs [ 0 ] ) } )
186
- }
187
-
188
- // Overflow
189
- else if ( indexToSet > state . tabs . length ) {
190
- dispatch ( {
191
- type : ActionTypes . SetSelectedIndex ,
192
- index : tabs . indexOf ( focusableTabs [ focusableTabs . length - 1 ] ) ,
193
- } )
194
- }
195
-
196
- // Middle
197
- else {
198
- let before = tabs . slice ( 0 , indexToSet )
199
- let after = tabs . slice ( indexToSet )
200
-
201
- let next = [ ...after , ...before ] . find ( ( tab ) => focusableTabs . includes ( tab ) )
202
- if ( ! next ) return
203
-
204
- dispatch ( { type : ActionTypes . SetSelectedIndex , index : tabs . indexOf ( next ) } )
205
- }
206
- } , [ defaultIndex , selectedIndex , state . tabs , state . selectedIndex ] )
215
+ dispatch ( { type : ActionTypes . SetSelectedIndex , index : indexToSet } )
216
+ } , [ selectedIndex /* Deliberately skipping defaultIndex */ ] )
207
217
208
218
let lastChangedIndex = useRef ( state . selectedIndex )
209
219
useEffect ( ( ) => {
@@ -226,14 +236,17 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
226
236
[ state , dispatch ]
227
237
)
228
238
229
- let SSRCounter = useRef ( 0 )
239
+ let SSRCounter = useRef ( {
240
+ tabs : [ ] ,
241
+ panels : [ ] ,
242
+ } )
230
243
231
244
let ourProps = {
232
245
ref : tabsRef ,
233
246
}
234
247
235
248
return (
236
- < TabsSSRContext . Provider value = { typeof window === 'undefined' ? SSRCounter : null } >
249
+ < TabsSSRContext . Provider value = { SSRCounter } >
237
250
< TabsContext . Provider value = { providerBag } >
238
251
< FocusSentinel
239
252
onFocus = { ( ) => {
@@ -308,6 +321,7 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
308
321
309
322
let [ { selectedIndex, tabs, panels, orientation, activation } , { dispatch, change } ] =
310
323
useTabsContext ( 'Tab' )
324
+ let SSRContext = useSSRTabsCounter ( 'Tab' )
311
325
312
326
let internalTabRef = useRef < HTMLElement > ( null )
313
327
let tabRef = useSyncRefs ( internalTabRef , ref , ( element ) => {
@@ -320,7 +334,11 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
320
334
return ( ) => dispatch ( { type : ActionTypes . UnregisterTab , tab : internalTabRef } )
321
335
} , [ dispatch , internalTabRef ] )
322
336
337
+ let mySSRIndex = SSRContext . current . tabs . indexOf ( id )
338
+ if ( mySSRIndex === - 1 ) mySSRIndex = SSRContext . current . tabs . push ( id ) - 1
339
+
323
340
let myIndex = tabs . indexOf ( internalTabRef )
341
+ if ( myIndex === - 1 ) myIndex = mySSRIndex
324
342
let selected = myIndex === selectedIndex
325
343
326
344
let handleKeyDown = useCallback (
@@ -452,11 +470,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
452
470
ref : Ref < HTMLElement >
453
471
) {
454
472
let [ { selectedIndex, tabs, panels } , { dispatch } ] = useTabsContext ( 'Tab.Panel' )
455
- let SSRContext = useContext ( TabsSSRContext )
456
-
457
- if ( SSRContext !== null && selectedIndex === null ) {
458
- selectedIndex = 0 // Should normally not happen, but in case the selectedIndex is null, we can default to 0.
459
- }
473
+ let SSRContext = useSSRTabsCounter ( 'Tab.Panel' )
460
474
461
475
let id = `headlessui-tabs-panel-${ useId ( ) } `
462
476
let internalPanelRef = useRef < HTMLElement > ( null )
@@ -470,9 +484,13 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
470
484
return ( ) => dispatch ( { type : ActionTypes . UnregisterPanel , panel : internalPanelRef } )
471
485
} , [ dispatch , internalPanelRef ] )
472
486
487
+ let mySSRIndex = SSRContext . current . panels . indexOf ( id )
488
+ if ( mySSRIndex === - 1 ) mySSRIndex = SSRContext . current . panels . push ( id ) - 1
489
+
473
490
let myIndex = panels . indexOf ( internalPanelRef )
474
- let selected =
475
- SSRContext === null ? myIndex === selectedIndex : SSRContext . current ++ === selectedIndex
491
+ if ( myIndex === - 1 ) myIndex = mySSRIndex
492
+
493
+ let selected = myIndex === selectedIndex
476
494
477
495
let slot = useMemo ( ( ) => ( { selected } ) , [ selected ] )
478
496
0 commit comments