@@ -24,6 +24,7 @@ import React, {
24
24
useReducer ,
25
25
Reducer ,
26
26
Dispatch ,
27
+ RefObject ,
27
28
} from "react" ;
28
29
29
30
import { Key } from "../Keyboard" ;
@@ -63,7 +64,7 @@ const RovingTabIndexContext = createContext<IContext>({
63
64
} ) ;
64
65
RovingTabIndexContext . displayName = "RovingTabIndexContext" ;
65
66
66
- enum Type {
67
+ export enum Type {
67
68
Register = "REGISTER" ,
68
69
Unregister = "UNREGISTER" ,
69
70
SetFocus = "SET_FOCUS" ,
@@ -76,73 +77,67 @@ interface IAction {
76
77
} ;
77
78
}
78
79
79
- const reducer = ( state : IState , action : IAction ) => {
80
+ export const reducer = ( state : IState , action : IAction ) => {
80
81
switch ( action . type ) {
81
82
case Type . Register : {
82
- if ( state . refs . length === 0 ) {
83
- // Our list of refs was empty, set activeRef to this first item
84
- return {
85
- ...state ,
86
- activeRef : action . payload . ref ,
87
- refs : [ action . payload . ref ] ,
88
- } ;
89
- }
90
-
91
- if ( state . refs . includes ( action . payload . ref ) ) {
92
- return state ; // already in refs, this should not happen
83
+ let left = 0 ;
84
+ let right = state . refs . length - 1 ;
85
+ let index = state . refs . length ; // by default append to the end
86
+
87
+ // do a binary search to find the right slot
88
+ while ( left <= right ) {
89
+ index = Math . floor ( ( left + right ) / 2 ) ;
90
+ const ref = state . refs [ index ] ;
91
+
92
+ if ( ref === action . payload . ref ) {
93
+ return state ; // already in refs, this should not happen
94
+ }
95
+
96
+ if ( action . payload . ref . current . compareDocumentPosition ( ref . current ) & DOCUMENT_POSITION_PRECEDING ) {
97
+ left = ++ index ;
98
+ } else {
99
+ right = index - 1 ;
100
+ }
93
101
}
94
102
95
- // find the index of the first ref which is not preceding this one in DOM order
96
- let newIndex = state . refs . findIndex ( ref => {
97
- return ref . current . compareDocumentPosition ( action . payload . ref . current ) & DOCUMENT_POSITION_PRECEDING ;
98
- } ) ;
99
-
100
- if ( newIndex < 0 ) {
101
- newIndex = state . refs . length ; // append to the end
103
+ if ( ! state . activeRef ) {
104
+ // Our list of refs was empty, set activeRef to this first item
105
+ state . activeRef = action . payload . ref ;
102
106
}
103
107
104
108
// update the refs list
105
- return {
106
- ...state ,
107
- refs : [
108
- ...state . refs . slice ( 0 , newIndex ) ,
109
- action . payload . ref ,
110
- ...state . refs . slice ( newIndex ) ,
111
- ] ,
112
- } ;
109
+ if ( index < state . refs . length ) {
110
+ state . refs . splice ( index , 0 , action . payload . ref ) ;
111
+ } else {
112
+ state . refs . push ( action . payload . ref ) ;
113
+ }
114
+ return { ...state } ;
113
115
}
116
+
114
117
case Type . Unregister : {
115
- // filter out the ref which we are removing
116
- const refs = state . refs . filter ( r => r !== action . payload . ref ) ;
118
+ const oldIndex = state . refs . findIndex ( r => r === action . payload . ref ) ;
117
119
118
- if ( refs . length === state . refs . length ) {
120
+ if ( oldIndex === - 1 ) {
119
121
return state ; // already removed, this should not happen
120
122
}
121
123
122
- if ( state . activeRef === action . payload . ref ) {
124
+ if ( state . refs . splice ( oldIndex , 1 ) [ 0 ] === state . activeRef ) {
123
125
// we just removed the active ref, need to replace it
124
126
// pick the ref which is now in the index the old ref was in
125
- const oldIndex = state . refs . findIndex ( r => r === action . payload . ref ) ;
126
- return {
127
- ...state ,
128
- activeRef : oldIndex >= refs . length ? refs [ refs . length - 1 ] : refs [ oldIndex ] ,
129
- refs,
130
- } ;
127
+ const len = state . refs . length ;
128
+ state . activeRef = oldIndex >= len ? state . refs [ len - 1 ] : state . refs [ oldIndex ] ;
131
129
}
132
130
133
131
// update the refs list
134
- return {
135
- ...state ,
136
- refs,
137
- } ;
132
+ return { ...state } ;
138
133
}
134
+
139
135
case Type . SetFocus : {
140
136
// update active ref
141
- return {
142
- ...state ,
143
- activeRef : action . payload . ref ,
144
- } ;
137
+ state . activeRef = action . payload . ref ;
138
+ return { ...state } ;
145
139
}
140
+
146
141
default :
147
142
return state ;
148
143
}
@@ -151,13 +146,40 @@ const reducer = (state: IState, action: IAction) => {
151
146
interface IProps {
152
147
handleHomeEnd ?: boolean ;
153
148
handleUpDown ?: boolean ;
149
+ handleLeftRight ?: boolean ;
154
150
children ( renderProps : {
155
151
onKeyDownHandler ( ev : React . KeyboardEvent ) ;
156
152
} ) ;
157
153
onKeyDown ?( ev : React . KeyboardEvent , state : IState ) ;
158
154
}
159
155
160
- export const RovingTabIndexProvider : React . FC < IProps > = ( { children, handleHomeEnd, handleUpDown, onKeyDown } ) => {
156
+ export const findSiblingElement = (
157
+ refs : RefObject < HTMLElement > [ ] ,
158
+ startIndex : number ,
159
+ backwards = false ,
160
+ ) : RefObject < HTMLElement > => {
161
+ if ( backwards ) {
162
+ for ( let i = startIndex ; i < refs . length && i >= 0 ; i -- ) {
163
+ if ( refs [ i ] . current . offsetParent !== null ) {
164
+ return refs [ i ] ;
165
+ }
166
+ }
167
+ } else {
168
+ for ( let i = startIndex ; i < refs . length && i >= 0 ; i ++ ) {
169
+ if ( refs [ i ] . current . offsetParent !== null ) {
170
+ return refs [ i ] ;
171
+ }
172
+ }
173
+ }
174
+ } ;
175
+
176
+ export const RovingTabIndexProvider : React . FC < IProps > = ( {
177
+ children,
178
+ handleHomeEnd,
179
+ handleUpDown,
180
+ handleLeftRight,
181
+ onKeyDown,
182
+ } ) => {
161
183
const [ state , dispatch ] = useReducer < Reducer < IState , IAction > > ( reducer , {
162
184
activeRef : null ,
163
185
refs : [ ] ,
@@ -166,6 +188,13 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
166
188
const context = useMemo < IContext > ( ( ) => ( { state, dispatch } ) , [ state ] ) ;
167
189
168
190
const onKeyDownHandler = useCallback ( ( ev ) => {
191
+ if ( onKeyDown ) {
192
+ onKeyDown ( ev , context . state ) ;
193
+ if ( ev . defaultPrevented ) {
194
+ return ;
195
+ }
196
+ }
197
+
169
198
let handled = false ;
170
199
// Don't interfere with input default keydown behaviour
171
200
if ( ev . target . tagName !== "INPUT" && ev . target . tagName !== "TEXTAREA" ) {
@@ -174,43 +203,37 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
174
203
case Key . HOME :
175
204
if ( handleHomeEnd ) {
176
205
handled = true ;
177
- // move focus to first item
178
- if ( context . state . refs . length > 0 ) {
179
- context . state . refs [ 0 ] . current . focus ( ) ;
180
- }
206
+ // move focus to first (visible) item
207
+ findSiblingElement ( context . state . refs , 0 ) ?. current ?. focus ( ) ;
181
208
}
182
209
break ;
183
210
184
211
case Key . END :
185
212
if ( handleHomeEnd ) {
186
213
handled = true ;
187
- // move focus to last item
188
- if ( context . state . refs . length > 0 ) {
189
- context . state . refs [ context . state . refs . length - 1 ] . current . focus ( ) ;
190
- }
214
+ // move focus to last (visible) item
215
+ findSiblingElement ( context . state . refs , context . state . refs . length - 1 , true ) ?. current ?. focus ( ) ;
191
216
}
192
217
break ;
193
218
194
219
case Key . ARROW_UP :
195
- if ( handleUpDown ) {
220
+ case Key . ARROW_RIGHT :
221
+ if ( ( ev . key === Key . ARROW_UP && handleUpDown ) || ( ev . key === Key . ARROW_RIGHT && handleLeftRight ) ) {
196
222
handled = true ;
197
223
if ( context . state . refs . length > 0 ) {
198
224
const idx = context . state . refs . indexOf ( context . state . activeRef ) ;
199
- if ( idx > 0 ) {
200
- context . state . refs [ idx - 1 ] . current . focus ( ) ;
201
- }
225
+ findSiblingElement ( context . state . refs , idx - 1 ) ?. current ?. focus ( ) ;
202
226
}
203
227
}
204
228
break ;
205
229
206
230
case Key . ARROW_DOWN :
207
- if ( handleUpDown ) {
231
+ case Key . ARROW_LEFT :
232
+ if ( ( ev . key === Key . ARROW_DOWN && handleUpDown ) || ( ev . key === Key . ARROW_LEFT && handleLeftRight ) ) {
208
233
handled = true ;
209
234
if ( context . state . refs . length > 0 ) {
210
235
const idx = context . state . refs . indexOf ( context . state . activeRef ) ;
211
- if ( idx < context . state . refs . length - 1 ) {
212
- context . state . refs [ idx + 1 ] . current . focus ( ) ;
213
- }
236
+ findSiblingElement ( context . state . refs , idx + 1 , true ) ?. current ?. focus ( ) ;
214
237
}
215
238
}
216
239
break ;
@@ -220,10 +243,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
220
243
if ( handled ) {
221
244
ev . preventDefault ( ) ;
222
245
ev . stopPropagation ( ) ;
223
- } else if ( onKeyDown ) {
224
- return onKeyDown ( ev , context . state ) ;
225
246
}
226
- } , [ context . state , onKeyDown , handleHomeEnd , handleUpDown ] ) ;
247
+ } , [ context . state , onKeyDown , handleHomeEnd , handleUpDown , handleLeftRight ] ) ;
227
248
228
249
return < RovingTabIndexContext . Provider value = { context } >
229
250
{ children ( { onKeyDownHandler } ) }
0 commit comments