13
13
import { FocusableElement } from '@react-types/shared' ;
14
14
import { focusSafely } from './focusSafely' ;
15
15
import { isElementVisible } from './isElementVisible' ;
16
- import React , { ReactNode , RefObject , useContext , useEffect , useRef } from 'react' ;
16
+ import React , { ReactNode , RefObject , useContext , useEffect , useMemo , useRef } from 'react' ;
17
17
import { useLayoutEffect } from '@react-aria/utils' ;
18
18
19
19
@@ -85,8 +85,12 @@ export function FocusScope(props: FocusScopeProps) {
85
85
let endRef = useRef < HTMLSpanElement > ( ) ;
86
86
let scopeRef = useRef < Element [ ] > ( [ ] ) ;
87
87
let ctx = useContext ( FocusContext ) ;
88
- // if there is no scopeRef on the context, then the parent is the focusScopeTree's root, represented by null
89
- let parentScope = ctx ?. scopeRef ?? null ;
88
+
89
+ // The parent scope is based on the JSX tree, using context.
90
+ // However, if a new scope mounts outside the active scope (e.g. DialogContainer launched from a menu),
91
+ // we want the parent scope to be the active scope instead.
92
+ let ctxParent = ctx ?. scopeRef ?? null ;
93
+ let parentScope = useMemo ( ( ) => activeScope && focusScopeTree . getTreeNode ( activeScope ) && ! isAncestorScope ( activeScope , ctxParent ) ? activeScope : ctxParent , [ ctxParent ] ) ;
90
94
91
95
useLayoutEffect ( ( ) => {
92
96
// Find all rendered nodes between the sentinels and add them to the scope.
@@ -109,14 +113,18 @@ export function FocusScope(props: FocusScopeProps) {
109
113
let node = focusScopeTree . getTreeNode ( scopeRef ) ;
110
114
node . contain = contain ;
111
115
116
+ useActiveScopeTracker ( scopeRef , restoreFocus , contain ) ;
112
117
useFocusContainment ( scopeRef , contain ) ;
113
118
useRestoreFocus ( scopeRef , restoreFocus , contain ) ;
114
119
useAutoFocus ( scopeRef , autoFocus ) ;
115
120
116
121
// this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
117
122
useLayoutEffect ( ( ) => {
118
- if ( scopeRef && ( parentScope || parentScope == null ) ) {
123
+ if ( scopeRef ) {
119
124
return ( ) => {
125
+ // Scope may have been re-parented.
126
+ let parentScope = focusScopeTree . getTreeNode ( scopeRef ) . parent . scopeRef ;
127
+
120
128
// Restore the active scope on unmount if this scope or a descendant scope is active.
121
129
// Parent effect cleanups run before children, so we need to check if the
122
130
// parent scope actually still exists before restoring the active scope to it.
@@ -294,7 +302,7 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
294
302
let onFocus = ( e ) => {
295
303
// If focusing an element in a child scope of the currently active scope, the child becomes active.
296
304
// Moving out of the active scope to an ancestor is not allowed.
297
- if ( ! activeScope || isAncestorScope ( activeScope , scopeRef ) ) {
305
+ if ( ( ! activeScope || isAncestorScope ( activeScope , scopeRef ) ) && isElementInScope ( e . target , scopeRef . current ) ) {
298
306
activeScope = scopeRef ;
299
307
focusedNode . current = e . target ;
300
308
} else if ( shouldContainFocus ( scopeRef ) && ! isElementInChildScope ( e . target , scopeRef ) ) {
@@ -424,6 +432,47 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
424
432
} , [ scopeRef ] ) ;
425
433
}
426
434
435
+ function useActiveScopeTracker ( scopeRef : RefObject < Element [ ] > , restore : boolean , contain : boolean ) {
436
+ // tracks the active scope, in case restore and contain are both false.
437
+ // if either are true, this is tracked in useRestoreFocus or useFocusContainment.
438
+ useLayoutEffect ( ( ) => {
439
+ if ( restore || contain ) {
440
+ return ;
441
+ }
442
+
443
+ let scope = scopeRef . current ;
444
+
445
+ let onFocus = ( e : FocusEvent ) => {
446
+ let target = e . target as Element ;
447
+ if ( isElementInScope ( target , scopeRef . current ) ) {
448
+ activeScope = scopeRef ;
449
+ } else if ( ! isElementInAnyScope ( target ) ) {
450
+ activeScope = null ;
451
+ }
452
+ } ;
453
+
454
+ document . addEventListener ( 'focusin' , onFocus , false ) ;
455
+ scope . forEach ( element => element . addEventListener ( 'focusin' , onFocus , false ) ) ;
456
+ return ( ) => {
457
+ document . removeEventListener ( 'focusin' , onFocus , false ) ;
458
+ scope . forEach ( element => element . removeEventListener ( 'focusin' , onFocus , false ) ) ;
459
+ } ;
460
+ } , [ scopeRef , restore , contain ] ) ;
461
+ }
462
+
463
+ function shouldRestoreFocus ( scopeRef : ScopeRef ) {
464
+ let scope = focusScopeTree . getTreeNode ( activeScope ) ;
465
+ while ( scope && scope . scopeRef !== scopeRef ) {
466
+ if ( scope . nodeToRestore ) {
467
+ return false ;
468
+ }
469
+
470
+ scope = scope . parent ;
471
+ }
472
+
473
+ return true ;
474
+ }
475
+
427
476
function useRestoreFocus ( scopeRef : RefObject < Element [ ] > , restoreFocus : boolean , contain : boolean ) {
428
477
// create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
429
478
const nodeToRestoreRef = useRef ( typeof document !== 'undefined' ? document . activeElement as FocusableElement : null ) ;
@@ -454,11 +503,12 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
454
503
455
504
// useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously.
456
505
useLayoutEffect ( ( ) => {
457
- focusScopeTree . getTreeNode ( scopeRef ) . nodeToRestore = nodeToRestoreRef . current ;
458
506
if ( ! restoreFocus ) {
459
507
return ;
460
508
}
461
509
510
+ focusScopeTree . getTreeNode ( scopeRef ) . nodeToRestore = nodeToRestoreRef . current ;
511
+
462
512
// Handle the Tab key so that tabbing out of the scope goes to the next element
463
513
// after the node that had focus when the scope mounted. This is important when
464
514
// using portals for overlays, so that focus goes to the expected element when
@@ -529,7 +579,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
529
579
&& nodeToRestore
530
580
&& (
531
581
isElementInScope ( document . activeElement , scopeRef . current )
532
- || ( document . activeElement === document . body && activeScope === scopeRef )
582
+ || ( document . activeElement === document . body && shouldRestoreFocus ( scopeRef ) )
533
583
)
534
584
) {
535
585
// freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
@@ -546,6 +596,17 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
546
596
}
547
597
treeNode = treeNode . parent ;
548
598
}
599
+
600
+ // If no nodeToRestore was found, focus the first element in the nearest
601
+ // ancestor scope that is still in the tree.
602
+ treeNode = clonedTree . getTreeNode ( scopeRef ) ;
603
+ while ( treeNode ) {
604
+ if ( treeNode . scopeRef && focusScopeTree . getTreeNode ( treeNode . scopeRef ) ) {
605
+ focusFirstInScope ( treeNode . scopeRef . current , true ) ;
606
+ return ;
607
+ }
608
+ treeNode = treeNode . parent ;
609
+ }
549
610
}
550
611
} ) ;
551
612
}
0 commit comments