12
12
13
13
import { FocusableElement } from '@react-types/shared' ;
14
14
import { focusSafely } from './focusSafely' ;
15
+ import { getOwnerDocument , useLayoutEffect } from '@react-aria/utils' ;
15
16
import { isElementVisible } from './isElementVisible' ;
16
17
import React , { ReactNode , RefObject , useContext , useEffect , useMemo , useRef } from 'react' ;
17
- import { useLayoutEffect } from '@react-aria/utils' ;
18
-
19
18
20
19
export interface FocusScopeProps {
21
20
/** The contents of the focus scope. */
@@ -134,7 +133,7 @@ export function FocusScope(props: FocusScopeProps) {
134
133
// This needs to be an effect so that activeScope is updated after the FocusScope tree is complete.
135
134
// It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet.
136
135
useEffect ( ( ) => {
137
- let activeElement = document . activeElement ;
136
+ const activeElement = getOwnerDocument ( scopeRef . current ? scopeRef . current [ 0 ] : undefined ) . activeElement ;
138
137
let scope : TreeNode | null = null ;
139
138
140
139
if ( isElementInScope ( activeElement , scopeRef . current ) ) {
@@ -198,7 +197,7 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
198
197
focusNext ( opts : FocusManagerOptions = { } ) {
199
198
let scope = scopeRef . current ! ;
200
199
let { from, tabbable, wrap, accept} = opts ;
201
- let node = from || document . activeElement ! ;
200
+ let node = from || getOwnerDocument ( scope [ 0 ] ) . activeElement ! ;
202
201
let sentinel = scope [ 0 ] . previousElementSibling ! ;
203
202
let scopeRoot = getScopeRoot ( scope ) ;
204
203
let walker = getFocusableTreeWalker ( scopeRoot , { tabbable, accept} , scope ) ;
@@ -216,7 +215,7 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
216
215
focusPrevious ( opts : FocusManagerOptions = { } ) {
217
216
let scope = scopeRef . current ! ;
218
217
let { from, tabbable, wrap, accept} = opts ;
219
- let node = from || document . activeElement ! ;
218
+ let node = from || getOwnerDocument ( scope [ 0 ] ) . activeElement ! ;
220
219
let sentinel = scope [ scope . length - 1 ] . nextElementSibling ! ;
221
220
let scopeRoot = getScopeRoot ( scope ) ;
222
221
let walker = getFocusableTreeWalker ( scopeRoot , { tabbable, accept} , scope ) ;
@@ -311,13 +310,15 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean)
311
310
return ;
312
311
}
313
312
313
+ const ownerDocument = getOwnerDocument ( scope ? scope [ 0 ] : undefined ) ;
314
+
314
315
// Handle the Tab key to contain focus within the scope
315
316
let onKeyDown = ( e ) => {
316
317
if ( e . key !== 'Tab' || e . altKey || e . ctrlKey || e . metaKey || ! shouldContainFocus ( scopeRef ) ) {
317
318
return ;
318
319
}
319
320
320
- let focusedElement = document . activeElement ;
321
+ let focusedElement = ownerDocument . activeElement ;
321
322
let scope = scopeRef . current ;
322
323
if ( ! scope || ! isElementInScope ( focusedElement , scope ) ) {
323
324
return ;
@@ -367,9 +368,9 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean)
367
368
}
368
369
raf . current = requestAnimationFrame ( ( ) => {
369
370
// Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe
370
- if ( document . activeElement && shouldContainFocus ( scopeRef ) && ! isElementInChildScope ( document . activeElement , scopeRef ) ) {
371
+ if ( ownerDocument . activeElement && shouldContainFocus ( scopeRef ) && ! isElementInChildScope ( ownerDocument . activeElement , scopeRef ) ) {
371
372
activeScope = scopeRef ;
372
- if ( document . body . contains ( e . target ) ) {
373
+ if ( ownerDocument . body . contains ( e . target ) ) {
373
374
focusedNode . current = e . target ;
374
375
focusedNode . current ?. focus ( ) ;
375
376
} else if ( activeScope . current ) {
@@ -379,13 +380,13 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean)
379
380
} ) ;
380
381
} ;
381
382
382
- document . addEventListener ( 'keydown' , onKeyDown , false ) ;
383
- document . addEventListener ( 'focusin' , onFocus , false ) ;
383
+ ownerDocument . addEventListener ( 'keydown' , onKeyDown , false ) ;
384
+ ownerDocument . addEventListener ( 'focusin' , onFocus , false ) ;
384
385
scope ?. forEach ( element => element . addEventListener ( 'focusin' , onFocus , false ) ) ;
385
386
scope ?. forEach ( element => element . addEventListener ( 'focusout' , onBlur , false ) ) ;
386
387
return ( ) => {
387
- document . removeEventListener ( 'keydown' , onKeyDown , false ) ;
388
- document . removeEventListener ( 'focusin' , onFocus , false ) ;
388
+ ownerDocument . removeEventListener ( 'keydown' , onKeyDown , false ) ;
389
+ ownerDocument . removeEventListener ( 'focusin' , onFocus , false ) ;
389
390
scope ?. forEach ( element => element . removeEventListener ( 'focusin' , onFocus , false ) ) ;
390
391
scope ?. forEach ( element => element . removeEventListener ( 'focusout' , onBlur , false ) ) ;
391
392
} ;
@@ -488,7 +489,8 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus?: boolean) {
488
489
useEffect ( ( ) => {
489
490
if ( autoFocusRef . current ) {
490
491
activeScope = scopeRef ;
491
- if ( ! isElementInScope ( document . activeElement , activeScope . current ) && scopeRef . current ) {
492
+ const ownerDocument = getOwnerDocument ( scopeRef . current ? scopeRef . current [ 0 ] : undefined ) ;
493
+ if ( ! isElementInScope ( ownerDocument . activeElement , activeScope . current ) && scopeRef . current ) {
492
494
focusFirstInScope ( scopeRef . current ) ;
493
495
}
494
496
}
@@ -505,6 +507,7 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore?: boolean
505
507
}
506
508
507
509
let scope = scopeRef . current ;
510
+ const ownerDocument = getOwnerDocument ( scope ? scope [ 0 ] : undefined ) ;
508
511
509
512
let onFocus = ( e ) => {
510
513
let target = e . target as Element ;
@@ -515,10 +518,10 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore?: boolean
515
518
}
516
519
} ;
517
520
518
- document . addEventListener ( 'focusin' , onFocus , false ) ;
521
+ ownerDocument . addEventListener ( 'focusin' , onFocus , false ) ;
519
522
scope ?. forEach ( element => element . addEventListener ( 'focusin' , onFocus , false ) ) ;
520
523
return ( ) => {
521
- document . removeEventListener ( 'focusin' , onFocus , false ) ;
524
+ ownerDocument . removeEventListener ( 'focusin' , onFocus , false ) ;
522
525
scope ?. forEach ( element => element . removeEventListener ( 'focusin' , onFocus , false ) ) ;
523
526
} ;
524
527
} , [ scopeRef , restore , contain ] ) ;
@@ -539,12 +542,14 @@ function shouldRestoreFocus(scopeRef: ScopeRef) {
539
542
540
543
function useRestoreFocus ( scopeRef : RefObject < Element [ ] > , restoreFocus ?: boolean , contain ?: boolean ) {
541
544
// create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
542
- const nodeToRestoreRef = useRef ( typeof document !== 'undefined' ? document . activeElement as FocusableElement : null ) ;
545
+ // eslint-disable-next-line no-restricted-globals
546
+ const nodeToRestoreRef = useRef ( typeof document !== 'undefined' ? getOwnerDocument ( scopeRef . current ? scopeRef . current [ 0 ] : undefined ) . activeElement as FocusableElement : null ) ;
543
547
544
548
// restoring scopes should all track if they are active regardless of contain, but contain already tracks it plus logic to contain the focus
545
549
// restoring-non-containing scopes should only care if they become active so they can perform the restore
546
550
useLayoutEffect ( ( ) => {
547
551
let scope = scopeRef . current ;
552
+ const ownerDocument = getOwnerDocument ( scope ? scope [ 0 ] : undefined ) ;
548
553
if ( ! restoreFocus || contain ) {
549
554
return ;
550
555
}
@@ -553,22 +558,24 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
553
558
// If focusing an element in a child scope of the currently active scope, the child becomes active.
554
559
// Moving out of the active scope to an ancestor is not allowed.
555
560
if ( ( ! activeScope || isAncestorScope ( activeScope , scopeRef ) ) &&
556
- isElementInScope ( document . activeElement , scopeRef . current )
561
+ isElementInScope ( ownerDocument . activeElement , scopeRef . current )
557
562
) {
558
563
activeScope = scopeRef ;
559
564
}
560
565
} ;
561
566
562
- document . addEventListener ( 'focusin' , onFocus , false ) ;
567
+ ownerDocument . addEventListener ( 'focusin' , onFocus , false ) ;
563
568
scope ?. forEach ( element => element . addEventListener ( 'focusin' , onFocus , false ) ) ;
564
569
return ( ) => {
565
- document . removeEventListener ( 'focusin' , onFocus , false ) ;
570
+ ownerDocument . removeEventListener ( 'focusin' , onFocus , false ) ;
566
571
scope ?. forEach ( element => element . removeEventListener ( 'focusin' , onFocus , false ) ) ;
567
572
} ;
568
573
// eslint-disable-next-line react-hooks/exhaustive-deps
569
574
} , [ scopeRef , contain ] ) ;
570
575
571
576
useLayoutEffect ( ( ) => {
577
+ const ownerDocument = getOwnerDocument ( scopeRef . current ? scopeRef . current [ 0 ] : undefined ) ;
578
+
572
579
if ( ! restoreFocus ) {
573
580
return ;
574
581
}
@@ -582,7 +589,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
582
589
return ;
583
590
}
584
591
585
- let focusedElement = document . activeElement as FocusableElement ;
592
+ let focusedElement = ownerDocument . activeElement as FocusableElement ;
586
593
if ( ! isElementInScope ( focusedElement , scopeRef . current ) ) {
587
594
return ;
588
595
}
@@ -593,13 +600,13 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
593
600
let nodeToRestore = treeNode . nodeToRestore ;
594
601
595
602
// Create a DOM tree walker that matches all tabbable elements
596
- let walker = getFocusableTreeWalker ( document . body , { tabbable : true } ) ;
603
+ let walker = getFocusableTreeWalker ( ownerDocument . body , { tabbable : true } ) ;
597
604
598
605
// Find the next tabbable element after the currently focused element
599
606
walker . currentNode = focusedElement ;
600
607
let nextElement = ( e . shiftKey ? walker . previousNode ( ) : walker . nextNode ( ) ) as FocusableElement ;
601
608
602
- if ( ! nodeToRestore || ! document . body . contains ( nodeToRestore ) || nodeToRestore === document . body ) {
609
+ if ( ! nodeToRestore || ! ownerDocument . body . contains ( nodeToRestore ) || nodeToRestore === ownerDocument . body ) {
603
610
nodeToRestore = undefined ;
604
611
treeNode . nodeToRestore = undefined ;
605
612
}
@@ -632,18 +639,20 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
632
639
} ;
633
640
634
641
if ( ! contain ) {
635
- document . addEventListener ( 'keydown' , onKeyDown , true ) ;
642
+ ownerDocument . addEventListener ( 'keydown' , onKeyDown , true ) ;
636
643
}
637
644
638
645
return ( ) => {
639
646
if ( ! contain ) {
640
- document . removeEventListener ( 'keydown' , onKeyDown , true ) ;
647
+ ownerDocument . removeEventListener ( 'keydown' , onKeyDown , true ) ;
641
648
}
642
649
} ;
643
650
} , [ scopeRef , restoreFocus , contain ] ) ;
644
651
645
652
// useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously.
646
653
useLayoutEffect ( ( ) => {
654
+ const ownerDocument = getOwnerDocument ( scopeRef . current ? scopeRef . current [ 0 ] : undefined ) ;
655
+
647
656
if ( ! restoreFocus ) {
648
657
return ;
649
658
}
@@ -653,7 +662,6 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
653
662
return ;
654
663
}
655
664
treeNode . nodeToRestore = nodeToRestoreRef . current ?? undefined ;
656
-
657
665
return ( ) => {
658
666
let treeNode = focusScopeTree . getTreeNode ( scopeRef ) ;
659
667
if ( ! treeNode ) {
@@ -667,19 +675,19 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
667
675
&& nodeToRestore
668
676
&& (
669
677
// eslint-disable-next-line react-hooks/exhaustive-deps
670
- isElementInScope ( document . activeElement , scopeRef . current )
671
- || ( document . activeElement === document . body && shouldRestoreFocus ( scopeRef ) )
678
+ isElementInScope ( ownerDocument . activeElement , scopeRef . current )
679
+ || ( ownerDocument . activeElement === ownerDocument . body && shouldRestoreFocus ( scopeRef ) )
672
680
)
673
681
) {
674
682
// freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
675
683
let clonedTree = focusScopeTree . clone ( ) ;
676
684
requestAnimationFrame ( ( ) => {
677
685
// Only restore focus if we've lost focus to the body, the alternative is that focus has been purposefully moved elsewhere
678
- if ( document . activeElement === document . body ) {
686
+ if ( ownerDocument . activeElement === ownerDocument . body ) {
679
687
// look up the tree starting with our scope to find a nodeToRestore still in the DOM
680
688
let treeNode = clonedTree . getTreeNode ( scopeRef ) ;
681
689
while ( treeNode ) {
682
- if ( treeNode . nodeToRestore && document . body . contains ( treeNode . nodeToRestore ) ) {
690
+ if ( treeNode . nodeToRestore && treeNode . nodeToRestore . isConnected ) {
683
691
focusElement ( treeNode . nodeToRestore ) ;
684
692
return ;
685
693
}
@@ -709,7 +717,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
709
717
*/
710
718
export function getFocusableTreeWalker ( root : Element , opts ?: FocusManagerOptions , scope ?: Element [ ] ) {
711
719
let selector = opts ?. tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR ;
712
- let walker = document . createTreeWalker (
720
+ let walker = getOwnerDocument ( root ) . createTreeWalker (
713
721
root ,
714
722
NodeFilter . SHOW_ELEMENT ,
715
723
{
@@ -750,7 +758,7 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
750
758
return null ;
751
759
}
752
760
let { from, tabbable = defaultOptions . tabbable , wrap = defaultOptions . wrap , accept = defaultOptions . accept } = opts ;
753
- let node = from || document . activeElement ;
761
+ let node = from || getOwnerDocument ( root ) . activeElement ;
754
762
let walker = getFocusableTreeWalker ( root , { tabbable, accept} ) ;
755
763
if ( root . contains ( node ) ) {
756
764
walker . currentNode = node ! ;
@@ -771,7 +779,7 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
771
779
return null ;
772
780
}
773
781
let { from, tabbable = defaultOptions . tabbable , wrap = defaultOptions . wrap , accept = defaultOptions . accept } = opts ;
774
- let node = from || document . activeElement ;
782
+ let node = from || getOwnerDocument ( root ) . activeElement ;
775
783
let walker = getFocusableTreeWalker ( root , { tabbable, accept} ) ;
776
784
if ( root . contains ( node ) ) {
777
785
walker . currentNode = node ! ;
0 commit comments