@react-aria/focus 3.15.0 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,10 +12,9 @@
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!;
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import {FocusableElement} from '@react-types/shared';
14
- import {focusWithoutScrolling, runAfterTransition} from '@react-aria/utils';
14
+ import {focusWithoutScrolling, getOwnerDocument, runAfterTransition} from '@react-aria/utils';
15
15
  import {getInteractionModality} from '@react-aria/interactions';
16
16
 
17
17
  /**
@@ -24,11 +24,12 @@ export function focusSafely(element: FocusableElement) {
24
24
  // the page before shifting focus. This avoids issues with VoiceOver on iOS
25
25
  // causing the page to scroll when moving focus if the element is transitioning
26
26
  // from off the screen.
27
+ const ownerDocument = getOwnerDocument(element);
27
28
  if (getInteractionModality() === 'virtual') {
28
- let lastFocusedElement = document.activeElement;
29
+ let lastFocusedElement = ownerDocument.activeElement;
29
30
  runAfterTransition(() => {
30
31
  // If focus did not move and the element is still in the document, focus it.
31
- if (document.activeElement === lastFocusedElement && document.contains(element)) {
32
+ if (ownerDocument.activeElement === lastFocusedElement && element.isConnected) {
32
33
  focusWithoutScrolling(element);
33
34
  }
34
35
  });
@@ -10,8 +10,11 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {getOwnerWindow} from '@react-aria/utils';
14
+
13
15
  function isStyleVisible(element: Element) {
14
- if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
16
+ const windowObject = getOwnerWindow(element);
17
+ if (!(element instanceof windowObject.HTMLElement) && !(element instanceof windowObject.SVGElement)) {
15
18
  return false;
16
19
  }
17
20
 
@@ -27,7 +27,7 @@ export interface FocusableProviderProps extends DOMAttributes {
27
27
  }
28
28
 
29
29
  interface FocusableContextValue extends FocusableProviderProps {
30
- ref?: MutableRefObject<FocusableElement>
30
+ ref?: MutableRefObject<FocusableElement | null>
31
31
  }
32
32
 
33
33
  let FocusableContext = React.createContext<FocusableContextValue | null>(null);