1- import React , { createContext , useEffect , useState } from 'react' ;
1+ import React , {
2+ createContext ,
3+ useCallback ,
4+ useEffect ,
5+ useState ,
6+ useRef ,
7+ } from 'react' ;
28import {
39 TabsContextProps ,
410 ITabsContext ,
@@ -29,17 +35,188 @@ const TabsProvider = ({
2935 themeContainerId,
3036 statgrouptheme,
3137 value,
38+ disabledTabIndexes = [ ] ,
39+ enableArrowNav = true ,
3240 variant = TabVariant . default ,
3341} : TabsContextProps ) => {
34- const [ currentActiveTab , setCurrentActiveTab ] = useState < TabValue > ( value ) ;
42+ const [ currentActiveTab , setCurrentActiveTab ] = useState ( value ) ;
43+ const [ focusedTabIndex , setFocusedTabIndex ] = useState < number | null > ( null ) ;
44+ const tabsRef = useRef < HTMLElement [ ] > ( [ ] ) ;
45+ const tabListRef = useRef < HTMLElement | null > ( null ) ;
46+ const tabsValuesRef = useRef < TabValue [ ] > ( [ ] ) ;
3547
3648 useEffect ( ( ) => {
3749 setCurrentActiveTab ( value ) ;
3850 } , [ value ] ) ;
3951
40- const onTabClick = ( value : TabValue , e : SelectTabEvent ) => {
41- onChange ( value , e ) ;
42- } ;
52+ const registerTab = useCallback (
53+ ( tabElement : HTMLElement | null , index : number ) => {
54+ if ( tabElement ) {
55+ tabsRef . current [ index ] = tabElement ;
56+ const tabValue = tabElement . getAttribute ( 'data-value' ) ;
57+ if ( tabValue ) {
58+ tabsValuesRef . current [ index ] = tabValue ;
59+ }
60+ }
61+ } ,
62+ [ ]
63+ ) ;
64+
65+ const registerTablist = useCallback ( ( tabListElement : HTMLElement | null ) => {
66+ tabListRef . current = tabListElement ;
67+ } , [ ] ) ;
68+
69+ const onTabClick = useCallback (
70+ ( value : TabValue , e : SelectTabEvent ) => {
71+ if ( ! readOnly ) {
72+ setCurrentActiveTab ( value ) ;
73+ onChange ( value , e ) ;
74+ }
75+ } ,
76+ [ onChange , readOnly ]
77+ ) ;
78+
79+ const moveFocusToNextTab = useCallback ( ( ) => {
80+ const tabValues = tabsValuesRef . current . filter ( Boolean ) ;
81+ if ( tabValues . length === 0 ) return ;
82+
83+ let currentIndex =
84+ focusedTabIndex !== null
85+ ? focusedTabIndex
86+ : tabValues . indexOf ( currentActiveTab ) ;
87+ if ( currentIndex === - 1 ) currentIndex = 0 ;
88+
89+ let nextIndex = currentIndex ;
90+ do {
91+ nextIndex = ( nextIndex + 1 ) % tabValues . length ;
92+ if ( nextIndex === currentIndex ) break ;
93+ } while ( disabledTabIndexes . includes ( nextIndex ) ) ;
94+
95+ const nextTab = tabsRef . current [ nextIndex ] ;
96+ if ( nextTab && ! disabledTabIndexes . includes ( nextIndex ) ) {
97+ nextTab . focus ( ) ;
98+ setFocusedTabIndex ( nextIndex ) ;
99+ }
100+ } , [ focusedTabIndex , currentActiveTab , disabledTabIndexes ] ) ;
101+
102+ const moveFocusToPreviousTab = useCallback ( ( ) => {
103+ const tabValues = tabsValuesRef . current . filter ( Boolean ) ;
104+ if ( tabValues . length == 0 ) return ;
105+
106+ let currentIndex =
107+ focusedTabIndex !== null
108+ ? focusedTabIndex
109+ : tabValues . indexOf ( currentActiveTab ) ;
110+ if ( currentIndex === - 1 ) currentIndex = 0 ;
111+
112+ let prevIndex = currentIndex ;
113+ do {
114+ prevIndex = ( prevIndex - 1 + tabValues . length ) % tabValues . length ;
115+ if ( prevIndex === currentIndex ) break ;
116+ } while ( disabledTabIndexes . includes ( prevIndex ) ) ;
117+
118+ const prevTab = tabsRef . current [ prevIndex ] ;
119+ if ( prevTab && ! disabledTabIndexes . includes ( prevIndex ) ) {
120+ prevTab . focus ( ) ;
121+ setFocusedTabIndex ( prevIndex ) ;
122+ }
123+ } , [ focusedTabIndex , currentActiveTab , disabledTabIndexes ] ) ;
124+
125+ useEffect ( ( ) => {
126+ const handleKeyDown = ( event : globalThis . KeyboardEvent ) => {
127+ if ( enableArrowNav ) return ;
128+ if ( event . key == 'Tab' ) {
129+ const activeElement = document . activeElement ;
130+ const tabList = tabListRef . current ;
131+
132+ if (
133+ tabList &&
134+ tabList . contains ( activeElement ) &&
135+ activeElement ?. getAttribute ( 'role' ) === 'tab'
136+ ) {
137+ event . preventDefault ( ) ;
138+ if ( event . shiftKey ) {
139+ moveFocusToPreviousTab ( ) ;
140+ } else {
141+ moveFocusToNextTab ( ) ;
142+ }
143+ }
144+ }
145+ } ;
146+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
147+ return ( ) => {
148+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
149+ } ;
150+ } , [ moveFocusToNextTab , moveFocusToPreviousTab ] ) ;
151+
152+ const handleKeyDown = useCallback (
153+ ( event : React . KeyboardEvent , tabIndex : number ) => {
154+ if ( ! enableArrowNav || readOnly ) return ;
155+
156+ if ( event . key === 'Tab' ) {
157+ return ;
158+ }
159+
160+ const enableTabIndexes = tabsRef . current
161+ . map ( ( _ , index ) => index )
162+ . filter ( ( index ) => ! disabledTabIndexes . includes ( index ) ) ;
163+
164+ const currentEnabledIndex = enableTabIndexes . indexOf ( tabIndex ) ;
165+
166+ if ( currentEnabledIndex === - 1 ) return ;
167+
168+ let nextFocusIndex : number | null = null ;
169+
170+ switch ( event . key ) {
171+ case 'ArrowLeft' :
172+ nextFocusIndex =
173+ currentEnabledIndex === 0
174+ ? enableTabIndexes [ enableTabIndexes . length - 1 ]
175+ : enableTabIndexes [ currentEnabledIndex - 1 ] ;
176+ event . preventDefault ( ) ;
177+ break ;
178+ case 'ArrowRight' :
179+ nextFocusIndex =
180+ currentEnabledIndex === enableTabIndexes . length - 1
181+ ? enableTabIndexes [ 0 ]
182+ : enableTabIndexes [ currentEnabledIndex + 1 ] ;
183+ event . preventDefault ( ) ;
184+ break ;
185+ case 'Home' :
186+ nextFocusIndex = enableTabIndexes [ 0 ] ;
187+ event . preventDefault ( ) ;
188+ break ;
189+ case 'End' :
190+ nextFocusIndex = enableTabIndexes [ enableTabIndexes . length - 1 ] ;
191+ event . preventDefault ( ) ;
192+ break ;
193+ case 'Enter' :
194+ const currentTab = tabsRef . current [ tabIndex ] ;
195+ if ( currentTab ) {
196+ const tabValue = currentTab . getAttribute ( 'data-value' ) ;
197+ if ( tabValue ) {
198+ setCurrentActiveTab ( tabValue ) ;
199+ onChange ?.( tabValue , {
200+ currentTarget : currentTab ,
201+ } as SelectTabEvent ) ;
202+ }
203+ }
204+ event . preventDefault ( ) ;
205+ return ;
206+ default :
207+ return ;
208+ }
209+
210+ if ( nextFocusIndex !== null ) {
211+ const nextTab = tabsRef . current [ nextFocusIndex ] ;
212+ if ( nextTab ) {
213+ nextTab . focus ( ) ;
214+ setFocusedTabIndex ( nextFocusIndex ) ;
215+ }
216+ }
217+ } ,
218+ [ enableArrowNav , disabledTabIndexes , readOnly , onChange ]
219+ ) ;
43220
44221 return (
45222 < TabsContext . Provider
@@ -59,6 +236,12 @@ const TabsProvider = ({
59236 theme,
60237 themeContainerId,
61238 variant,
239+ registerTab,
240+ registerTablist,
241+ handleKeyDown,
242+ enableArrowNav,
243+ disabledTabIndexes,
244+ focusedTabIndex,
62245 } }
63246 >
64247 { children }
0 commit comments