11
11
*/
12
12
13
13
import { focusSafely } from './focusSafely' ;
14
+ import { isElementVisible } from './isElementVisible' ;
14
15
import React , { ReactNode , RefObject , useContext , useEffect , useRef } from 'react' ;
15
16
import { useLayoutEffect } from '@react-aria/utils' ;
16
17
@@ -119,30 +120,36 @@ export function useFocusManager(): FocusManager {
119
120
function createFocusManager ( scopeRef : React . RefObject < HTMLElement [ ] > ) : FocusManager {
120
121
return {
121
122
focusNext ( opts : FocusManagerOptions = { } ) {
122
- let node = opts . from || document . activeElement ;
123
- let focusable = getFocusableElementsInScope ( scopeRef . current , opts ) ;
124
- let nextNode = focusable . find ( n =>
125
- ! ! ( node . compareDocumentPosition ( n ) & ( Node . DOCUMENT_POSITION_FOLLOWING | Node . DOCUMENT_POSITION_CONTAINED_BY ) )
126
- ) ;
127
- if ( ! nextNode && opts . wrap ) {
128
- nextNode = focusable [ 0 ] ;
123
+ let scope = scopeRef . current ;
124
+ let { from, tabbable, wrap} = opts ;
125
+ let node = from || document . activeElement ;
126
+ let sentinel = scope [ 0 ] . previousElementSibling ;
127
+ let walker = getFocusableTreeWalker ( getScopeRoot ( scope ) , { tabbable} , scope ) ;
128
+ walker . currentNode = isElementInScope ( node , scope ) ? node : sentinel ;
129
+ let nextNode = walker . nextNode ( ) as HTMLElement ;
130
+ if ( ! nextNode && wrap ) {
131
+ walker . currentNode = sentinel ;
132
+ nextNode = walker . nextNode ( ) as HTMLElement ;
129
133
}
130
134
if ( nextNode ) {
131
- nextNode . focus ( ) ;
135
+ focusElement ( nextNode , true ) ;
132
136
}
133
137
return nextNode ;
134
138
} ,
135
139
focusPrevious ( opts : FocusManagerOptions = { } ) {
136
- let node = opts . from || document . activeElement ;
137
- let focusable = getFocusableElementsInScope ( scopeRef . current , opts ) . reverse ( ) ;
138
- let previousNode = focusable . find ( n =>
139
- ! ! ( node . compareDocumentPosition ( n ) & ( Node . DOCUMENT_POSITION_PRECEDING | Node . DOCUMENT_POSITION_CONTAINED_BY ) )
140
- ) ;
141
- if ( ! previousNode && opts . wrap ) {
142
- previousNode = focusable [ 0 ] ;
140
+ let scope = scopeRef . current ;
141
+ let { from, tabbable, wrap} = opts ;
142
+ let node = from || document . activeElement ;
143
+ let sentinel = scope [ scope . length - 1 ] . nextElementSibling ;
144
+ let walker = getFocusableTreeWalker ( getScopeRoot ( scope ) , { tabbable} , scope ) ;
145
+ walker . currentNode = isElementInScope ( node , scope ) ? node : sentinel ;
146
+ let previousNode = walker . previousNode ( ) as HTMLElement ;
147
+ if ( ! previousNode && wrap ) {
148
+ walker . currentNode = sentinel ;
149
+ previousNode = walker . previousNode ( ) as HTMLElement ;
143
150
}
144
151
if ( previousNode ) {
145
- previousNode . focus ( ) ;
152
+ focusElement ( previousNode , true ) ;
146
153
}
147
154
return previousNode ;
148
155
}
@@ -165,21 +172,13 @@ const focusableElements = [
165
172
'[contenteditable]'
166
173
] ;
167
174
168
- const FOCUSABLE_ELEMENT_SELECTOR = focusableElements . join ( ',' ) + ',[tabindex]' ;
175
+ const FOCUSABLE_ELEMENT_SELECTOR = focusableElements . join ( ':not([hidden]) ,' ) + ',[tabindex]:not([hidden]) ' ;
169
176
170
177
focusableElements . push ( '[tabindex]:not([tabindex="-1"]):not([disabled])' ) ;
171
- const TABBABLE_ELEMENT_SELECTOR = focusableElements . join ( ':not([tabindex="-1"]),' ) ;
172
-
173
- function getFocusableElementsInScope ( scope : HTMLElement [ ] , opts : FocusManagerOptions ) : HTMLElement [ ] {
174
- let res = [ ] ;
175
- let selector = opts . tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR ;
176
- for ( let node of scope ) {
177
- if ( node . matches ( selector ) ) {
178
- res . push ( node ) ;
179
- }
180
- res . push ( ...Array . from ( node . querySelectorAll ( selector ) ) ) ;
181
- }
182
- return res ;
178
+ const TABBABLE_ELEMENT_SELECTOR = focusableElements . join ( ':not([hidden]):not([tabindex="-1"]),' ) ;
179
+
180
+ function getScopeRoot ( scope : HTMLElement [ ] ) {
181
+ return scope [ 0 ] . parentElement ;
183
182
}
184
183
185
184
function useFocusContainment ( scopeRef : RefObject < HTMLElement [ ] > , contain : boolean ) {
@@ -203,23 +202,12 @@ function useFocusContainment(scopeRef: RefObject<HTMLElement[]>, contain: boolea
203
202
return ;
204
203
}
205
204
206
- let elements = getFocusableElementsInScope ( scope , { tabbable : true } ) ;
207
- let position = elements . indexOf ( focusedElement ) ;
208
- let lastPosition = elements . length - 1 ;
209
- let nextElement = null ;
210
-
211
- if ( e . shiftKey ) {
212
- if ( position <= 0 ) {
213
- nextElement = elements [ lastPosition ] ;
214
- } else {
215
- nextElement = elements [ position - 1 ] ;
216
- }
217
- } else {
218
- if ( position === lastPosition ) {
219
- nextElement = elements [ 0 ] ;
220
- } else {
221
- nextElement = elements [ position + 1 ] ;
222
- }
205
+ let walker = getFocusableTreeWalker ( getScopeRoot ( scope ) , { tabbable : true } , scope ) ;
206
+ walker . currentNode = focusedElement ;
207
+ let nextElement = ( e . shiftKey ? walker . previousNode ( ) : walker . nextNode ( ) ) as HTMLElement ;
208
+ if ( ! nextElement ) {
209
+ walker . currentNode = e . shiftKey ? scope [ scope . length - 1 ] . nextElementSibling : scope [ 0 ] . previousElementSibling ;
210
+ nextElement = ( e . shiftKey ? walker . previousNode ( ) : walker . nextNode ( ) ) as HTMLElement ;
223
211
}
224
212
225
213
e . preventDefault ( ) ;
@@ -306,8 +294,10 @@ function focusElement(element: HTMLElement | null, scroll = false) {
306
294
}
307
295
308
296
function focusFirstInScope ( scope : HTMLElement [ ] ) {
309
- let elements = getFocusableElementsInScope ( scope , { tabbable : true } ) ;
310
- focusElement ( elements [ 0 ] ) ;
297
+ let sentinel = scope [ 0 ] . previousElementSibling ;
298
+ let walker = getFocusableTreeWalker ( getScopeRoot ( scope ) , { tabbable : true } , scope ) ;
299
+ walker . currentNode = sentinel ;
300
+ focusElement ( walker . nextNode ( ) as HTMLElement ) ;
311
301
}
312
302
313
303
function useAutoFocus ( scopeRef : RefObject < HTMLElement [ ] > , autoFocus : boolean ) {
@@ -348,6 +338,10 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
348
338
walker . currentNode = focusedElement ;
349
339
let nextElement = ( e . shiftKey ? walker . previousNode ( ) : walker . nextNode ( ) ) as HTMLElement ;
350
340
341
+ if ( ! document . body . contains ( nodeToRestore ) || nodeToRestore === document . body ) {
342
+ nodeToRestore = null ;
343
+ }
344
+
351
345
// If there is no next element, or it is outside the current scope, move focus to the
352
346
// next element after the node to restore to instead.
353
347
if ( ( ! nextElement || ! isElementInScope ( nextElement , scope ) ) && nodeToRestore ) {
@@ -361,7 +355,7 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
361
355
e . preventDefault ( ) ;
362
356
e . stopPropagation ( ) ;
363
357
if ( nextElement ) {
364
- nextElement . focus ( ) ;
358
+ focusElement ( nextElement , true ) ;
365
359
} else {
366
360
// If there is no next element, blur the focused element to move focus to the body.
367
361
focusedElement . blur ( ) ;
@@ -393,7 +387,7 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
393
387
* Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
394
388
* that matches all focusable/tabbable elements.
395
389
*/
396
- export function getFocusableTreeWalker ( root : HTMLElement , opts ?: FocusManagerOptions ) {
390
+ export function getFocusableTreeWalker ( root : HTMLElement , opts ?: FocusManagerOptions , scope ?: HTMLElement [ ] ) {
397
391
let selector = opts ?. tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR ;
398
392
let walker = document . createTreeWalker (
399
393
root ,
@@ -405,14 +399,15 @@ export function getFocusableTreeWalker(root: HTMLElement, opts?: FocusManagerOpt
405
399
return NodeFilter . FILTER_REJECT ;
406
400
}
407
401
408
- if ( ( node as HTMLElement ) . matches ( selector ) ) {
402
+ if ( ( node as HTMLElement ) . matches ( selector )
403
+ && isElementVisible ( node as HTMLElement )
404
+ && ( ! scope || isElementInScope ( node as HTMLElement , scope ) ) ) {
409
405
return NodeFilter . FILTER_ACCEPT ;
410
406
}
411
407
412
408
return NodeFilter . FILTER_SKIP ;
413
409
}
414
- } ,
415
- false
410
+ }
416
411
) ;
417
412
418
413
if ( opts ?. from ) {
0 commit comments