@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.
- package/dist/main.js +67 -9
- package/dist/main.js.map +1 -1
- package/dist/module.js +68 -11
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/FocusScope.tsx +75 -7
- package/src/index.ts +1 -1
package/src/FocusScope.tsx
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
|
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 &&
|
|
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';
|