diff --git a/packages/@react-aria/focus/src/FocusScope.tsx b/packages/@react-aria/focus/src/FocusScope.tsx index bd84a79c577..e3766f5a8aa 100644 --- a/packages/@react-aria/focus/src/FocusScope.tsx +++ b/packages/@react-aria/focus/src/FocusScope.tsx @@ -61,8 +61,8 @@ export interface FocusManager { type ScopeRef = RefObject; interface IFocusContext { - scopeRef: ScopeRef, - focusManager: FocusManager + focusManager: FocusManager, + parentNode: TreeNode | null } const FocusContext = React.createContext(null); @@ -84,13 +84,33 @@ export function FocusScope(props: FocusScopeProps) { let startRef = useRef(); let endRef = useRef(); let scopeRef = useRef([]); - let ctx = useContext(FocusContext); + let {parentNode} = useContext(FocusContext) || {}; - // The parent scope is based on the JSX tree, using context. - // However, if a new scope mounts outside the active scope (e.g. DialogContainer launched from a menu), - // we want the parent scope to be the active scope instead. - let ctxParent = ctx?.scopeRef ?? null; - let parentScope = useMemo(() => activeScope && focusScopeTree.getTreeNode(activeScope) && !isAncestorScope(activeScope, ctxParent) ? activeScope : ctxParent, [ctxParent]); + // Create a tree node here so we can add children to it even before it is added to the tree. + let node = useMemo(() => new TreeNode({scopeRef}), [scopeRef]); + + useLayoutEffect(() => { + // If a new scope mounts outside the active scope, (e.g. DialogContainer launched from a menu), + // use the active scope as the parent instead of the parent from context. Layout effects run bottom + // up, so if the parent is not yet added to the tree, don't do this. Only the outer-most FocusScope + // that is being added should get the activeScope as its parent. + let parent = parentNode || focusScopeTree.root; + if (focusScopeTree.getTreeNode(parent.scopeRef) && activeScope && !isAncestorScope(activeScope, parent.scopeRef)) { + let activeNode = focusScopeTree.getTreeNode(activeScope); + if (activeNode) { + parent = activeNode; + } + } + + // Add the node to the parent, and to the tree. + parent.addChild(node); + focusScopeTree.addNode(node); + }, [node, parentNode]); + + useLayoutEffect(() => { + let node = focusScopeTree.getTreeNode(scopeRef); + node.contain = contain; + }, [contain]); useLayoutEffect(() => { // Find all rendered nodes between the sentinels and add them to the scope. @@ -102,16 +122,7 @@ export function FocusScope(props: FocusScopeProps) { } scopeRef.current = nodes; - }, [children, parentScope]); - - // add to the focus scope tree in render order because useEffects/useLayoutEffects run children first whereas render runs parent first - // which matters when constructing a tree - if (focusScopeTree.getTreeNode(parentScope) && !focusScopeTree.getTreeNode(scopeRef)) { - focusScopeTree.addTreeNode(scopeRef, parentScope); - } - - let node = focusScopeTree.getTreeNode(scopeRef); - node.contain = contain; + }, [children]); useActiveScopeTracker(scopeRef, restoreFocus, contain); useFocusContainment(scopeRef, contain); @@ -119,8 +130,26 @@ export function FocusScope(props: FocusScopeProps) { useAutoFocus(scopeRef, autoFocus); // this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible - useLayoutEffect(() => { + useEffect(() => { if (scopeRef) { + let activeElement = document.activeElement; + let scope = null; + // In strict mode, active scope is incorrectly updated since cleanup will run even though scope hasn't unmounted. + // To fix this, we need to update the actual activeScope here + if (isElementInScope(activeElement, scopeRef.current)) { + // Since useLayoutEffect runs for children first, we need to traverse the focusScope tree and find the bottom most scope that + // contains the active element and set that as the activeScope + for (let node of focusScopeTree.traverse()) { + if (isElementInScope(activeElement, node.scopeRef.current)) { + scope = node; + } + } + + if (scope === focusScopeTree.getTreeNode(scopeRef)) { + activeScope = scope.scopeRef; + } + } + return () => { // Scope may have been re-parented. let parentScope = focusScopeTree.getTreeNode(scopeRef).parent.scopeRef; @@ -137,12 +166,16 @@ export function FocusScope(props: FocusScopeProps) { focusScopeTree.removeTreeNode(scopeRef); }; } - }, [scopeRef, parentScope]); + }, [scopeRef]); - let focusManager = createFocusManagerForScope(scopeRef); + let focusManager = useMemo(() => createFocusManagerForScope(scopeRef), []); + let value = useMemo(() => ({ + focusManager, + parentNode: node + }), [node, focusManager]); return ( - +