diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index b8b0050de7..6cf072260d 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -73,7 +73,6 @@ import {
enableUseEventAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
-import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
@@ -95,6 +94,10 @@ import {
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
+// TODO: This is an exposed internal, we should move this around
+// so this isn't the case.
+import {isFiberInsideHiddenOrRemovedTree} from 'react-reconciler/src/ReactFiberTreeReflection';
+
export type ReactListenerEvent = ReactDOMListenerEvent;
export type ReactListenerMap = ReactDOMListenerMap;
export type ReactListener = ReactDOMListener;
@@ -250,6 +253,15 @@ export function getPublicInstance(instance: Instance): * {
export function prepareForCommit(containerInfo: Container): void {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation();
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
+ const focusedElem = selectionInformation.focusedElem;
+ if (focusedElem !== null) {
+ const instance = getClosestInstanceFromNode(focusedElem);
+ if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
+ dispatchBeforeDetachedBlur(focusedElem);
+ }
+ }
+ }
ReactBrowserEventEmitterSetEnabled(false);
}
@@ -532,18 +544,11 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
);
}
if (enableUseEventAPI) {
- try {
- // We need to temporarily enable the event system
- // to dispatch the "beforeblur" event.
- ReactBrowserEventEmitterSetEnabled(true);
- const event = createEvent(TOP_BEFORE_BLUR);
- // Dispatch "beforeblur" directly on the target,
- // so it gets picked up by the event system and
- // can propagate through the React internal tree.
- target.dispatchEvent(event);
- } finally {
- ReactBrowserEventEmitterSetEnabled(false);
- }
+ const event = createEvent(TOP_BEFORE_BLUR);
+ // Dispatch "beforeblur" directly on the target,
+ // so it gets picked up by the event system and
+ // can propagate through the React internal tree.
+ target.dispatchEvent(event);
}
}
@@ -571,20 +576,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
}
}
-// This is a specific event for the React Flare
-// event system, so event responders can act
-// accordingly to a DOM node being unmounted that
-// previously had active document focus.
export function beforeRemoveInstance(
instance: Instance | TextInstance | SuspenseInstance,
): void {
- if (
- (enableDeprecatedFlareAPI || enableUseEventAPI) &&
- selectionInformation &&
- instance === selectionInformation.focusedElem
- ) {
- dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
- }
if (enableUseEventAPI) {
// It's unfortunate that we have to do this cleanup, but
// it's necessary otherwise we will leak the host instances
@@ -674,28 +668,7 @@ export function clearSuspenseBoundaryFromContainer(
retryIfBlockedOn(container);
}
-function instanceContainsElem(instance: Instance, element: HTMLElement) {
- let fiber = getClosestInstanceFromNode(element);
- while (fiber !== null) {
- if (fiber.tag === HostComponent && fiber.stateNode === instance) {
- return true;
- }
- fiber = fiber.return;
- }
- return false;
-}
-
export function hideInstance(instance: Instance): void {
- // Ensure we trigger `onBeforeBlur` if the active focused elment
- // is ether the instance of a child or the instance. We need
- // to traverse the Fiber tree here rather than use node.contains()
- // as the child node might be inside a Portal.
- if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
- const focusedElem = selectionInformation.focusedElem;
- if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
- dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
- }
- }
// TODO: Does this work for all element types? What about MathML? Should we
// pass host context to this method?
instance = ((instance: any): HTMLElement);
diff --git a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
index 76b065a1ee..827fade166 100644
--- a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
@@ -23,6 +23,7 @@ const initializeModules = hasPointerEvents => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
+ ReactFeatureFlags.enableScopeAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
@@ -364,6 +365,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
});
+ // @gate experimental
+ it('is called after a nested focused element is unmounted (with scope query)', () => {
+ const TestScope = React.unstable_createScope();
+ const testScopeQuery = (type, props) => true;
+ let targetNodes;
+ let targetNode;
+
+ const Component = ({show}) => {
+ const scopeRef = React.useRef(null);
+ const listener = useFocusWithin({
+ onBeforeBlurWithin(event) {
+ const scope = scopeRef.current;
+ targetNode = innerRef.current;
+ targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
+ },
+ });
+
+ return (
+
+ {show && }
+
+ );
+ };
+
+ ReactDOM.render(, container);
+
+ const inner = innerRef.current;
+ const target = createEventTarget(inner);
+ target.keydown({key: 'Tab'});
+ target.focus();
+ ReactDOM.render(, container);
+ expect(targetNodes).toEqual([targetNode]);
+ });
+
// @gate experimental
it('is called after a focused suspended element is hidden', () => {
const Suspense = React.Suspense;
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index a93574c26a..2bb319708a 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
- fiber.return = null;
- fiber.child = null;
- fiber.memoizedState = null;
- fiber.updateQueue = null;
- fiber.dependencies = null;
fiber.alternate = null;
+ fiber.child = null;
+ fiber.dependencies = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
- fiber.pendingProps = null;
fiber.memoizedProps = null;
+ fiber.memoizedState = null;
+ fiber.pendingProps = null;
+ fiber.return = null;
+ fiber.sibling = null;
fiber.stateNode = null;
+ fiber.updateQueue = null;
+ if (__DEV__) {
+ fiber._debugOwner = null;
+ }
}
function emptyPortalContainer(current: Fiber) {
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index 4482ee7814..4755f614eb 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
- fiber.return = null;
- fiber.child = null;
- fiber.memoizedState = null;
- fiber.updateQueue = null;
- fiber.dependencies = null;
fiber.alternate = null;
+ fiber.child = null;
+ fiber.dependencies = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
- fiber.pendingProps = null;
fiber.memoizedProps = null;
+ fiber.memoizedState = null;
+ fiber.pendingProps = null;
+ fiber.return = null;
+ fiber.sibling = null;
fiber.stateNode = null;
+ fiber.updateQueue = null;
+ if (__DEV__) {
+ fiber._debugOwner = null;
+ }
}
function emptyPortalContainer(current: Fiber) {
diff --git a/packages/react-reconciler/src/ReactFiberScope.new.js b/packages/react-reconciler/src/ReactFiberScope.new.js
index 2bdd53285d..1ee2083560 100644
--- a/packages/react-reconciler/src/ReactFiberScope.new.js
+++ b/packages/react-reconciler/src/ReactFiberScope.new.js
@@ -17,24 +17,11 @@ import type {
} from 'shared/ReactTypes';
import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
+import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
-import {
- HostComponent,
- SuspenseComponent,
- ScopeComponent,
- ContextProvider,
-} from './ReactWorkTags';
+import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
-function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
- const memoizedState = fiber.memoizedState;
- return (
- fiber.tag === SuspenseComponent &&
- memoizedState !== null &&
- memoizedState.dehydrated === null
- );
-}
-
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
diff --git a/packages/react-reconciler/src/ReactFiberScope.old.js b/packages/react-reconciler/src/ReactFiberScope.old.js
index 2bdd53285d..1ee2083560 100644
--- a/packages/react-reconciler/src/ReactFiberScope.old.js
+++ b/packages/react-reconciler/src/ReactFiberScope.old.js
@@ -17,24 +17,11 @@ import type {
} from 'shared/ReactTypes';
import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
+import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
-import {
- HostComponent,
- SuspenseComponent,
- ScopeComponent,
- ContextProvider,
-} from './ReactWorkTags';
+import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
-function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
- const memoizedState = fiber.memoizedState;
- return (
- fiber.tag === SuspenseComponent &&
- memoizedState !== null &&
- memoizedState.dehydrated === null
- );
-}
-
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js
index 56bce1abab..2dddc86dc3 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -25,7 +25,7 @@ import {
FundamentalComponent,
SuspenseComponent,
} from './ReactWorkTags';
-import {NoEffect, Placement, Hydrating} from './ReactSideEffectTags';
+import {NoEffect, Placement, Hydrating, Deletion} from './ReactSideEffectTags';
import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -332,3 +332,25 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
// eslint-disable-next-line no-unreachable
return null;
}
+
+export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
+ const memoizedState = fiber.memoizedState;
+ return (
+ fiber.tag === SuspenseComponent &&
+ memoizedState !== null &&
+ memoizedState.dehydrated === null
+ );
+}
+
+// This is only safe to call in the commit phase when the return tree is consistent.
+// It should not be used anywhere else. See PR #18609 for details.
+export function isFiberInsideHiddenOrRemovedTree(fiber: Fiber): boolean {
+ let node = fiber;
+ while (node !== null) {
+ if (node.effectTag & Deletion || isFiberSuspenseAndTimedOut(node)) {
+ return true;
+ }
+ node = node.return;
+ }
+ return false;
+}