@react-aria/focus 3.8.0 → 3.10.0

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.
@@ -13,7 +13,7 @@
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)) {
@@ -368,6 +376,11 @@ function isElementInChildScope(element: Element, scope: ScopeRef = null) {
368
376
  return false;
369
377
  }
370
378
 
379
+ /** @private */
380
+ export function isElementInChildOfActiveScope(element: Element) {
381
+ return isElementInChildScope(element, activeScope);
382
+ }
383
+
371
384
  function isAncestorScope(ancestor: ScopeRef, scope: ScopeRef) {
372
385
  let parent = focusScopeTree.getTreeNode(scope)?.parent;
373
386
  while (parent) {
@@ -424,6 +437,47 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
424
437
  }, [scopeRef]);
425
438
  }
426
439
 
440
+ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean, contain: boolean) {
441
+ // tracks the active scope, in case restore and contain are both false.
442
+ // if either are true, this is tracked in useRestoreFocus or useFocusContainment.
443
+ useLayoutEffect(() => {
444
+ if (restore || contain) {
445
+ return;
446
+ }
447
+
448
+ let scope = scopeRef.current;
449
+
450
+ let onFocus = (e: FocusEvent) => {
451
+ let target = e.target as Element;
452
+ if (isElementInScope(target, scopeRef.current)) {
453
+ activeScope = scopeRef;
454
+ } else if (!isElementInAnyScope(target)) {
455
+ activeScope = null;
456
+ }
457
+ };
458
+
459
+ document.addEventListener('focusin', onFocus, false);
460
+ scope.forEach(element => element.addEventListener('focusin', onFocus, false));
461
+ return () => {
462
+ document.removeEventListener('focusin', onFocus, false);
463
+ scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
464
+ };
465
+ }, [scopeRef, restore, contain]);
466
+ }
467
+
468
+ function shouldRestoreFocus(scopeRef: ScopeRef) {
469
+ let scope = focusScopeTree.getTreeNode(activeScope);
470
+ while (scope && scope.scopeRef !== scopeRef) {
471
+ if (scope.nodeToRestore) {
472
+ return false;
473
+ }
474
+
475
+ scope = scope.parent;
476
+ }
477
+
478
+ return scope?.scopeRef === scopeRef;
479
+ }
480
+
427
481
  function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean, contain: boolean) {
428
482
  // create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
429
483
  const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? document.activeElement as FocusableElement : null);
@@ -450,15 +504,17 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
450
504
  document.removeEventListener('focusin', onFocus, false);
451
505
  scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
452
506
  };
507
+ // eslint-disable-next-line react-hooks/exhaustive-deps
453
508
  }, [scopeRef, contain]);
454
509
 
455
510
  // useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously.
456
511
  useLayoutEffect(() => {
457
- focusScopeTree.getTreeNode(scopeRef).nodeToRestore = nodeToRestoreRef.current;
458
512
  if (!restoreFocus) {
459
513
  return;
460
514
  }
461
515
 
516
+ focusScopeTree.getTreeNode(scopeRef).nodeToRestore = nodeToRestoreRef.current;
517
+
462
518
  // Handle the Tab key so that tabbing out of the scope goes to the next element
463
519
  // after the node that had focus when the scope mounted. This is important when
464
520
  // using portals for overlays, so that focus goes to the expected element when
@@ -528,8 +584,9 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
528
584
  restoreFocus
529
585
  && nodeToRestore
530
586
  && (
587
+ // eslint-disable-next-line react-hooks/exhaustive-deps
531
588
  isElementInScope(document.activeElement, scopeRef.current)
532
- || (document.activeElement === document.body && activeScope === scopeRef)
589
+ || (document.activeElement === document.body && shouldRestoreFocus(scopeRef))
533
590
  )
534
591
  ) {
535
592
  // freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
@@ -546,6 +603,17 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
546
603
  }
547
604
  treeNode = treeNode.parent;
548
605
  }
606
+
607
+ // If no nodeToRestore was found, focus the first element in the nearest
608
+ // ancestor scope that is still in the tree.
609
+ treeNode = clonedTree.getTreeNode(scopeRef);
610
+ while (treeNode) {
611
+ if (treeNode.scopeRef && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
612
+ focusFirstInScope(treeNode.scopeRef.current, true);
613
+ return;
614
+ }
615
+ treeNode = treeNode.parent;
616
+ }
549
617
  }
550
618
  });
551
619
  }
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- export {FocusScope, useFocusManager, getFocusableTreeWalker, createFocusManager} from './FocusScope';
13
+ export {FocusScope, useFocusManager, getFocusableTreeWalker, createFocusManager, isElementInChildOfActiveScope} from './FocusScope';
14
14
  export {FocusRing} from './FocusRing';
15
15
  export {FocusableProvider, useFocusable} from './useFocusable';
16
16
  export {useFocusRing} from './useFocusRing';