@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.
- package/dist/import.mjs +44 -34
- package/dist/main.js +44 -34
- package/dist/main.js.map +1 -1
- package/dist/module.js +44 -34
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/FocusScope.tsx +40 -32
- package/src/focusSafely.ts +4 -3
- package/src/isElementVisible.ts +4 -1
- package/src/useFocusable.tsx +1 -1
package/src/FocusScope.tsx
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
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 ||
|
|
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 =
|
|
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 (
|
|
371
|
+
if (ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) {
|
|
371
372
|
activeScope = scopeRef;
|
|
372
|
-
if (
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
+
ownerDocument.addEventListener('focusin', onFocus, false);
|
|
519
522
|
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
520
523
|
return () => {
|
|
521
|
-
|
|
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
|
-
|
|
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(
|
|
561
|
+
isElementInScope(ownerDocument.activeElement, scopeRef.current)
|
|
557
562
|
) {
|
|
558
563
|
activeScope = scopeRef;
|
|
559
564
|
}
|
|
560
565
|
};
|
|
561
566
|
|
|
562
|
-
|
|
567
|
+
ownerDocument.addEventListener('focusin', onFocus, false);
|
|
563
568
|
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
564
569
|
return () => {
|
|
565
|
-
|
|
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 =
|
|
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(
|
|
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 || !
|
|
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
|
-
|
|
642
|
+
ownerDocument.addEventListener('keydown', onKeyDown, true);
|
|
636
643
|
}
|
|
637
644
|
|
|
638
645
|
return () => {
|
|
639
646
|
if (!contain) {
|
|
640
|
-
|
|
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(
|
|
671
|
-
|| (
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
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!;
|
package/src/focusSafely.ts
CHANGED
|
@@ -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 =
|
|
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 (
|
|
32
|
+
if (ownerDocument.activeElement === lastFocusedElement && element.isConnected) {
|
|
32
33
|
focusWithoutScrolling(element);
|
|
33
34
|
}
|
|
34
35
|
});
|
package/src/isElementVisible.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
+
const windowObject = getOwnerWindow(element);
|
|
17
|
+
if (!(element instanceof windowObject.HTMLElement) && !(element instanceof windowObject.SVGElement)) {
|
|
15
18
|
return false;
|
|
16
19
|
}
|
|
17
20
|
|
package/src/useFocusable.tsx
CHANGED
|
@@ -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);
|