@@ -131,43 +131,42 @@ export function FocusScope(props: FocusScopeProps) {
131
131
useRestoreFocus ( scopeRef , restoreFocus , contain ) ;
132
132
useAutoFocus ( scopeRef , autoFocus ) ;
133
133
134
- // this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
134
+ // This needs to be an effect so that activeScope is updated after the FocusScope tree is complete.
135
+ // It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet.
135
136
useEffect ( ( ) => {
136
- if ( scopeRef ) {
137
- let activeElement = document . activeElement ;
138
- let scope : TreeNode | null = null ;
139
- // In strict mode, active scope is incorrectly updated since cleanup will run even though scope hasn't unmounted.
140
- // To fix this, we need to update the actual activeScope here
141
- if ( isElementInScope ( activeElement , scopeRef . current ) ) {
142
- // Since useLayoutEffect runs for children first, we need to traverse the focusScope tree and find the bottom most scope that
143
- // contains the active element and set that as the activeScope
144
- for ( let node of focusScopeTree . traverse ( ) ) {
145
- if ( node . scopeRef && isElementInScope ( activeElement , node . scopeRef . current ) ) {
146
- scope = node ;
147
- }
137
+ let activeElement = document . activeElement ;
138
+ let scope : TreeNode | null = null ;
139
+
140
+ if ( isElementInScope ( activeElement , scopeRef . current ) ) {
141
+ // We need to traverse the focusScope tree and find the bottom most scope that
142
+ // contains the active element and set that as the activeScope.
143
+ for ( let node of focusScopeTree . traverse ( ) ) {
144
+ if ( node . scopeRef && isElementInScope ( activeElement , node . scopeRef . current ) ) {
145
+ scope = node ;
148
146
}
147
+ }
149
148
150
- if ( scope === focusScopeTree . getTreeNode ( scopeRef ) ) {
151
- activeScope = scope . scopeRef ;
152
- }
149
+ if ( scope === focusScopeTree . getTreeNode ( scopeRef ) ) {
150
+ activeScope = scope . scopeRef ;
153
151
}
152
+ }
153
+ } , [ scopeRef ] ) ;
154
154
155
- return ( ) => {
156
- // Scope may have been re-parented.
157
- let parentScope = focusScopeTree . getTreeNode ( scopeRef ) ?. parent ?. scopeRef ?? null ;
155
+ // This layout effect cleanup is so that the tree node is removed synchronously with react before the RAF
156
+ // in useRestoreFocus cleanup runs.
157
+ useLayoutEffect ( ( ) => {
158
+ return ( ) => {
159
+ // Scope may have been re-parented.
160
+ let parentScope = focusScopeTree . getTreeNode ( scopeRef ) ?. parent ?. scopeRef ?? null ;
158
161
159
- // Restore the active scope on unmount if this scope or a descendant scope is active.
160
- // Parent effect cleanups run before children, so we need to check if the
161
- // parent scope actually still exists before restoring the active scope to it.
162
- if (
163
- ( scopeRef === activeScope || isAncestorScope ( scopeRef , activeScope ) ) &&
164
- ( ! parentScope || focusScopeTree . getTreeNode ( parentScope ) )
165
- ) {
166
- activeScope = parentScope ;
167
- }
168
- focusScopeTree . removeTreeNode ( scopeRef ) ;
169
- } ;
170
- }
162
+ if (
163
+ ( scopeRef === activeScope || isAncestorScope ( scopeRef , activeScope ) ) &&
164
+ ( ! parentScope || focusScopeTree . getTreeNode ( parentScope ) )
165
+ ) {
166
+ activeScope = parentScope ;
167
+ }
168
+ focusScopeTree . removeTreeNode ( scopeRef ) ;
169
+ } ;
171
170
} , [ scopeRef ] ) ;
172
171
173
172
let focusManager = useMemo ( ( ) => createFocusManagerForScope ( scopeRef ) , [ ] ) ;
0 commit comments