1111 */
1212
1313import { focusSafely } from './focusSafely' ;
14+ import { isElementVisible } from './isElementVisible' ;
1415import React , { ReactNode , RefObject , useContext , useEffect , useRef } from 'react' ;
1516import { useLayoutEffect } from '@react-aria/utils' ;
1617
@@ -119,30 +120,36 @@ export function useFocusManager(): FocusManager {
119120function createFocusManager ( scopeRef : React . RefObject < HTMLElement [ ] > ) : FocusManager {
120121 return {
121122 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 ;
129133 }
130134 if ( nextNode ) {
131- nextNode . focus ( ) ;
135+ focusElement ( nextNode , true ) ;
132136 }
133137 return nextNode ;
134138 } ,
135139 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 ;
143150 }
144151 if ( previousNode ) {
145- previousNode . focus ( ) ;
152+ focusElement ( previousNode , true ) ;
146153 }
147154 return previousNode ;
148155 }
@@ -165,21 +172,13 @@ const focusableElements = [
165172 '[contenteditable]'
166173] ;
167174
168- const FOCUSABLE_ELEMENT_SELECTOR = focusableElements . join ( ',' ) + ',[tabindex]' ;
175+ const FOCUSABLE_ELEMENT_SELECTOR = focusableElements . join ( ':not([hidden]) ,' ) + ',[tabindex]:not([hidden]) ' ;
169176
170177focusableElements . 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 ;
183182}
184183
185184function useFocusContainment ( scopeRef : RefObject < HTMLElement [ ] > , contain : boolean ) {
@@ -203,23 +202,12 @@ function useFocusContainment(scopeRef: RefObject<HTMLElement[]>, contain: boolea
203202 return ;
204203 }
205204
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 ;
223211 }
224212
225213 e . preventDefault ( ) ;
@@ -306,8 +294,10 @@ function focusElement(element: HTMLElement | null, scroll = false) {
306294}
307295
308296function 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 ) ;
311301}
312302
313303function useAutoFocus ( scopeRef : RefObject < HTMLElement [ ] > , autoFocus : boolean ) {
@@ -348,6 +338,10 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
348338 walker . currentNode = focusedElement ;
349339 let nextElement = ( e . shiftKey ? walker . previousNode ( ) : walker . nextNode ( ) ) as HTMLElement ;
350340
341+ if ( ! document . body . contains ( nodeToRestore ) || nodeToRestore === document . body ) {
342+ nodeToRestore = null ;
343+ }
344+
351345 // If there is no next element, or it is outside the current scope, move focus to the
352346 // next element after the node to restore to instead.
353347 if ( ( ! nextElement || ! isElementInScope ( nextElement , scope ) ) && nodeToRestore ) {
@@ -361,7 +355,7 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
361355 e . preventDefault ( ) ;
362356 e . stopPropagation ( ) ;
363357 if ( nextElement ) {
364- nextElement . focus ( ) ;
358+ focusElement ( nextElement , true ) ;
365359 } else {
366360 // If there is no next element, blur the focused element to move focus to the body.
367361 focusedElement . blur ( ) ;
@@ -393,7 +387,7 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole
393387 * Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
394388 * that matches all focusable/tabbable elements.
395389 */
396- export function getFocusableTreeWalker ( root : HTMLElement , opts ?: FocusManagerOptions ) {
390+ export function getFocusableTreeWalker ( root : HTMLElement , opts ?: FocusManagerOptions , scope ?: HTMLElement [ ] ) {
397391 let selector = opts ?. tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR ;
398392 let walker = document . createTreeWalker (
399393 root ,
@@ -405,14 +399,15 @@ export function getFocusableTreeWalker(root: HTMLElement, opts?: FocusManagerOpt
405399 return NodeFilter . FILTER_REJECT ;
406400 }
407401
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 ) ) ) {
409405 return NodeFilter . FILTER_ACCEPT ;
410406 }
411407
412408 return NodeFilter . FILTER_SKIP ;
413409 }
414- } ,
415- false
410+ }
416411 ) ;
417412
418413 if ( opts ?. from ) {
0 commit comments