@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.
@@ -61,8 +61,8 @@ export interface FocusManager {
61
61
 
62
62
  type ScopeRef = RefObject<Element[]>;
63
63
  interface IFocusContext {
64
- scopeRef: ScopeRef,
65
- focusManager: FocusManager
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 ctx = useContext(FocusContext);
87
+ let {parentNode} = useContext(FocusContext) || {};
88
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]);
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, parentScope]);
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
- useLayoutEffect(() => {
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, parentScope]);
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={{scopeRef, focusManager}}>
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
- private root: TreeNode;
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.length > 0) {
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.length > 0) {
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.push(node);
889
+ this.children.add(node);
844
890
  node.parent = this;
845
891
  }
846
892
  removeChild(node: TreeNode) {
847
- this.children.splice(this.children.indexOf(node), 1);
893
+ this.children.delete(node);
848
894
  node.parent = undefined;
849
895
  }
850
896
  }