@react-aria/focus 3.10.1 → 3.11.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/import.mjs +861 -0
- package/dist/main.js +73 -37
- package/dist/main.js.map +1 -1
- package/dist/module.js +73 -37
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -5
- package/src/FocusScope.tsx +74 -28
package/src/FocusScope.tsx
CHANGED
|
@@ -61,8 +61,8 @@ export interface FocusManager {
|
|
|
61
61
|
|
|
62
62
|
type ScopeRef = RefObject<Element[]>;
|
|
63
63
|
interface IFocusContext {
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
focusManager: FocusManager,
|
|
65
|
+
parentNode: TreeNode | null
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const FocusContext = React.createContext<IFocusContext>(null);
|
|
@@ -84,13 +84,33 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
84
84
|
let startRef = useRef<HTMLSpanElement>();
|
|
85
85
|
let endRef = useRef<HTMLSpanElement>();
|
|
86
86
|
let scopeRef = useRef<Element[]>([]);
|
|
87
|
-
let
|
|
87
|
+
let {parentNode} = useContext(FocusContext) || {};
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
// Create a tree node here so we can add children to it even before it is added to the tree.
|
|
90
|
+
let node = useMemo(() => new TreeNode({scopeRef}), [scopeRef]);
|
|
91
|
+
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
// If a new scope mounts outside the active scope, (e.g. DialogContainer launched from a menu),
|
|
94
|
+
// use the active scope as the parent instead of the parent from context. Layout effects run bottom
|
|
95
|
+
// up, so if the parent is not yet added to the tree, don't do this. Only the outer-most FocusScope
|
|
96
|
+
// that is being added should get the activeScope as its parent.
|
|
97
|
+
let parent = parentNode || focusScopeTree.root;
|
|
98
|
+
if (focusScopeTree.getTreeNode(parent.scopeRef) && activeScope && !isAncestorScope(activeScope, parent.scopeRef)) {
|
|
99
|
+
let activeNode = focusScopeTree.getTreeNode(activeScope);
|
|
100
|
+
if (activeNode) {
|
|
101
|
+
parent = activeNode;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add the node to the parent, and to the tree.
|
|
106
|
+
parent.addChild(node);
|
|
107
|
+
focusScopeTree.addNode(node);
|
|
108
|
+
}, [node, parentNode]);
|
|
109
|
+
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
let node = focusScopeTree.getTreeNode(scopeRef);
|
|
112
|
+
node.contain = contain;
|
|
113
|
+
}, [contain]);
|
|
94
114
|
|
|
95
115
|
useLayoutEffect(() => {
|
|
96
116
|
// Find all rendered nodes between the sentinels and add them to the scope.
|
|
@@ -102,16 +122,7 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
scopeRef.current = nodes;
|
|
105
|
-
}, [children
|
|
106
|
-
|
|
107
|
-
// add to the focus scope tree in render order because useEffects/useLayoutEffects run children first whereas render runs parent first
|
|
108
|
-
// which matters when constructing a tree
|
|
109
|
-
if (focusScopeTree.getTreeNode(parentScope) && !focusScopeTree.getTreeNode(scopeRef)) {
|
|
110
|
-
focusScopeTree.addTreeNode(scopeRef, parentScope);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let node = focusScopeTree.getTreeNode(scopeRef);
|
|
114
|
-
node.contain = contain;
|
|
125
|
+
}, [children]);
|
|
115
126
|
|
|
116
127
|
useActiveScopeTracker(scopeRef, restoreFocus, contain);
|
|
117
128
|
useFocusContainment(scopeRef, contain);
|
|
@@ -119,8 +130,26 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
119
130
|
useAutoFocus(scopeRef, autoFocus);
|
|
120
131
|
|
|
121
132
|
// this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
|
|
122
|
-
|
|
133
|
+
useEffect(() => {
|
|
123
134
|
if (scopeRef) {
|
|
135
|
+
let activeElement = document.activeElement;
|
|
136
|
+
let scope = null;
|
|
137
|
+
// In strict mode, active scope is incorrectly updated since cleanup will run even though scope hasn't unmounted.
|
|
138
|
+
// To fix this, we need to update the actual activeScope here
|
|
139
|
+
if (isElementInScope(activeElement, scopeRef.current)) {
|
|
140
|
+
// Since useLayoutEffect runs for children first, we need to traverse the focusScope tree and find the bottom most scope that
|
|
141
|
+
// contains the active element and set that as the activeScope
|
|
142
|
+
for (let node of focusScopeTree.traverse()) {
|
|
143
|
+
if (isElementInScope(activeElement, node.scopeRef.current)) {
|
|
144
|
+
scope = node;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (scope === focusScopeTree.getTreeNode(scopeRef)) {
|
|
149
|
+
activeScope = scope.scopeRef;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
return () => {
|
|
125
154
|
// Scope may have been re-parented.
|
|
126
155
|
let parentScope = focusScopeTree.getTreeNode(scopeRef).parent.scopeRef;
|
|
@@ -137,12 +166,16 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
137
166
|
focusScopeTree.removeTreeNode(scopeRef);
|
|
138
167
|
};
|
|
139
168
|
}
|
|
140
|
-
}, [scopeRef
|
|
169
|
+
}, [scopeRef]);
|
|
141
170
|
|
|
142
|
-
let focusManager = createFocusManagerForScope(scopeRef);
|
|
171
|
+
let focusManager = useMemo(() => createFocusManagerForScope(scopeRef), []);
|
|
172
|
+
let value = useMemo(() => ({
|
|
173
|
+
focusManager,
|
|
174
|
+
parentNode: node
|
|
175
|
+
}), [node, focusManager]);
|
|
143
176
|
|
|
144
177
|
return (
|
|
145
|
-
<FocusContext.Provider value={
|
|
178
|
+
<FocusContext.Provider value={value}>
|
|
146
179
|
<span data-focus-scope-start hidden ref={startRef} />
|
|
147
180
|
{children}
|
|
148
181
|
<span data-focus-scope-end hidden ref={endRef} />
|
|
@@ -320,6 +353,9 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
320
353
|
|
|
321
354
|
let onBlur = (e) => {
|
|
322
355
|
// Firefox doesn't shift focus back to the Dialog properly without this
|
|
356
|
+
if (raf.current) {
|
|
357
|
+
cancelAnimationFrame(raf.current);
|
|
358
|
+
}
|
|
323
359
|
raf.current = requestAnimationFrame(() => {
|
|
324
360
|
// Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe
|
|
325
361
|
if (shouldContainFocus(scopeRef) && !isElementInChildScope(document.activeElement, scopeRef)) {
|
|
@@ -365,6 +401,11 @@ function isElementInScope(element: Element, scope: Element[]) {
|
|
|
365
401
|
}
|
|
366
402
|
|
|
367
403
|
function isElementInChildScope(element: Element, scope: ScopeRef = null) {
|
|
404
|
+
// If the element is within a top layer element (e.g. toasts), always allow moving focus there.
|
|
405
|
+
if (element instanceof Element && element.closest('[data-react-aria-top-layer]')) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
368
409
|
// node.contains in isElementInScope covers child scopes that are also DOM children,
|
|
369
410
|
// but does not cover child scopes in portals.
|
|
370
411
|
for (let {scopeRef: s} of focusScopeTree.traverse(focusScopeTree.getTreeNode(scope))) {
|
|
@@ -753,7 +794,7 @@ function last(walker: TreeWalker) {
|
|
|
753
794
|
|
|
754
795
|
|
|
755
796
|
class Tree {
|
|
756
|
-
|
|
797
|
+
root: TreeNode;
|
|
757
798
|
private fastMap = new Map<ScopeRef, TreeNode>();
|
|
758
799
|
|
|
759
800
|
constructor() {
|
|
@@ -780,6 +821,10 @@ class Tree {
|
|
|
780
821
|
}
|
|
781
822
|
}
|
|
782
823
|
|
|
824
|
+
addNode(node: TreeNode) {
|
|
825
|
+
this.fastMap.set(node.scopeRef, node);
|
|
826
|
+
}
|
|
827
|
+
|
|
783
828
|
removeTreeNode(scopeRef: ScopeRef) {
|
|
784
829
|
// never remove the root
|
|
785
830
|
if (scopeRef === null) {
|
|
@@ -802,9 +847,10 @@ class Tree {
|
|
|
802
847
|
}
|
|
803
848
|
let children = node.children;
|
|
804
849
|
parentNode.removeChild(node);
|
|
805
|
-
if (children.
|
|
850
|
+
if (children.size > 0) {
|
|
806
851
|
children.forEach(child => parentNode.addChild(child));
|
|
807
852
|
}
|
|
853
|
+
|
|
808
854
|
this.fastMap.delete(node.scopeRef);
|
|
809
855
|
}
|
|
810
856
|
|
|
@@ -813,7 +859,7 @@ class Tree {
|
|
|
813
859
|
if (node.scopeRef != null) {
|
|
814
860
|
yield node;
|
|
815
861
|
}
|
|
816
|
-
if (node.children.
|
|
862
|
+
if (node.children.size > 0) {
|
|
817
863
|
for (let child of node.children) {
|
|
818
864
|
yield* this.traverse(child);
|
|
819
865
|
}
|
|
@@ -833,18 +879,18 @@ class TreeNode {
|
|
|
833
879
|
public scopeRef: ScopeRef;
|
|
834
880
|
public nodeToRestore: FocusableElement;
|
|
835
881
|
public parent: TreeNode;
|
|
836
|
-
public children: TreeNode
|
|
882
|
+
public children: Set<TreeNode> = new Set();
|
|
837
883
|
public contain = false;
|
|
838
884
|
|
|
839
885
|
constructor(props: {scopeRef: ScopeRef}) {
|
|
840
886
|
this.scopeRef = props.scopeRef;
|
|
841
887
|
}
|
|
842
888
|
addChild(node: TreeNode) {
|
|
843
|
-
this.children.
|
|
889
|
+
this.children.add(node);
|
|
844
890
|
node.parent = this;
|
|
845
891
|
}
|
|
846
892
|
removeChild(node: TreeNode) {
|
|
847
|
-
this.children.
|
|
893
|
+
this.children.delete(node);
|
|
848
894
|
node.parent = undefined;
|
|
849
895
|
}
|
|
850
896
|
}
|