diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js
index e0e883c4b2..8c50b26ecc 100644
--- a/packages/react-art/src/ReactARTHostConfig.js
+++ b/packages/react-art/src/ReactARTHostConfig.js
@@ -306,6 +306,7 @@ export function getPublicInstance(instance) {
export function prepareForCommit() {
// Noop
+ return null;
}
export function prepareUpdate(domElement, type, oldProps, newProps) {
@@ -507,3 +508,11 @@ export function unmountEventListener(listener: any) {
export function validateEventListenerTarget(target: any, listener: any) {
throw new Error('Not yet implemented.');
}
+
+export function beforeActiveInstanceBlur() {
+ // noop
+}
+
+export function afterActiveInstanceBlur() {
+ // noop
+}
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index e2e9211234..70f23dba42 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -91,10 +91,6 @@ 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;
@@ -159,7 +155,6 @@ export opaque type OpaqueIDType =
};
type SelectionInformation = {|
- activeElementDetached: null | HTMLElement,
focusedElem: null | HTMLElement,
selectionRange: mixed,
|};
@@ -247,32 +242,40 @@ export function getPublicInstance(instance: Instance): * {
return instance;
}
-export function prepareForCommit(containerInfo: Container): void {
+export function prepareForCommit(containerInfo: Container): Object | null {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation();
+ let activeInstance = null;
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null) {
- const instance = getClosestInstanceFromNode(focusedElem);
- if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
- dispatchBeforeDetachedBlur(focusedElem);
- }
+ activeInstance = getClosestInstanceFromNode(focusedElem);
}
}
ReactBrowserEventEmitterSetEnabled(false);
+ return activeInstance;
+}
+
+export function beforeActiveInstanceBlur(): void {
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
+ ReactBrowserEventEmitterSetEnabled(true);
+ dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
+ ReactBrowserEventEmitterSetEnabled(false);
+ }
+}
+
+export function afterActiveInstanceBlur(): void {
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
+ ReactBrowserEventEmitterSetEnabled(true);
+ dispatchAfterDetachedBlur((selectionInformation: any).focusedElem);
+ ReactBrowserEventEmitterSetEnabled(false);
+ }
}
export function resetAfterCommit(containerInfo: Container): void {
restoreSelection(selectionInformation);
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
eventsEnabled = null;
- if (enableDeprecatedFlareAPI || enableUseEventAPI) {
- const activeElementDetached = (selectionInformation: any)
- .activeElementDetached;
- if (activeElementDetached !== null) {
- dispatchAfterDetachedBlur(activeElementDetached);
- }
- }
selectionInformation = null;
}
@@ -525,8 +528,6 @@ function createEvent(type: TopLevelType): Event {
}
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
- ((selectionInformation: any): SelectionInformation).activeElementDetached = target;
-
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
const event = createEvent(TOP_BEFORE_BLUR);
// Dispatch "beforeblur" directly on the target,
diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js
index 7d05a3d733..1465872a0e 100644
--- a/packages/react-dom/src/client/ReactInputSelection.js
+++ b/packages/react-dom/src/client/ReactInputSelection.js
@@ -100,8 +100,6 @@ export function hasSelectionCapabilities(elem) {
export function getSelectionInformation() {
const focusedElem = getActiveElementDeep();
return {
- // Used by Flare
- activeElementDetached: null,
focusedElem: focusedElem,
selectionRange: hasSelectionCapabilities(focusedElem)
? getSelection(focusedElem)
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 be5052fdbd..aedf567f58 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
@@ -16,7 +16,8 @@ let ReactFeatureFlags;
let ReactDOM;
let FocusWithinResponder;
let useFocusWithin;
-let Scheduler;
+let ReactTestRenderer;
+let act;
const initializeModules = hasPointerEvents => {
setPointerEvent(hasPointerEvents);
@@ -26,7 +27,8 @@ const initializeModules = hasPointerEvents => {
ReactFeatureFlags.enableScopeAPI = true;
React = require('react');
ReactDOM = require('react-dom');
- Scheduler = require('scheduler');
+ ReactTestRenderer = require('react-test-renderer');
+ act = ReactTestRenderer.act;
// TODO: This import throws outside of experimental mode. Figure out better
// strategy for gated imports.
@@ -43,17 +45,22 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)('FocusWithin responder', hasPointerEvents => {
let container;
+ let container2;
beforeEach(() => {
initializeModules();
container = document.createElement('div');
document.body.appendChild(container);
+ container2 = document.createElement('div');
+ document.body.appendChild(container2);
});
afterEach(() => {
ReactDOM.render(null, container);
document.body.removeChild(container);
+ document.body.removeChild(container2);
container = null;
+ container2 = null;
});
describe('disabled', () => {
@@ -366,6 +373,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
});
+ // @gate experimental
+ it('is called after many elements are unmounted', () => {
+ const buttonRef = React.createRef();
+ const inputRef = React.createRef();
+
+ const Component = ({show}) => {
+ const listener = useFocusWithin({
+ onBeforeBlurWithin,
+ onAfterBlurWithin,
+ });
+ return (
+
+ {show && }
+ {show && }
+ {show && }
+ {show && }
+ {!show && }
+ {show && }
+
+
+
+ );
+ };
+
+ ReactDOM.render(, container);
+
+ inputRef.current.focus();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+ ReactDOM.render(, container);
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
+ });
+
// @gate experimental
it('is called after a nested focused element is unmounted (with scope query)', () => {
const TestScope = React.unstable_createScope();
@@ -430,12 +471,10 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
};
- const container2 = document.createElement('div');
- document.body.appendChild(container2);
-
const root = ReactDOM.createRoot(container2);
- root.render();
- Scheduler.unstable_flushAll();
+ act(() => {
+ root.render();
+ });
jest.runAllTimers();
expect(container2.innerHTML).toBe('');
@@ -447,8 +486,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
suspend = true;
- root.render();
- Scheduler.unstable_flushAll();
+ act(() => {
+ root.render();
+ });
jest.runAllTimers();
expect(container2.innerHTML).toBe(
'Loading...
',
@@ -456,8 +496,74 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
resolve();
+ });
- document.body.removeChild(container2);
+ // @gate experimental
+ it('is called after a focused suspended element is hidden then shown', () => {
+ const Suspense = React.Suspense;
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+ const buttonRef = React.createRef();
+
+ function Child() {
+ if (suspend) {
+ throw promise;
+ } else {
+ return ;
+ }
+ }
+
+ const Component = ({show}) => {
+ const listener = useFocusWithin({
+ onBeforeBlurWithin,
+ onAfterBlurWithin,
+ });
+
+ return (
+
+ Loading...}>
+
+
+
+ );
+ };
+
+ const root = ReactDOM.createRoot(container2);
+
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ suspend = true;
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ buttonRef.current.focus();
+ suspend = false;
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
+
+ resolve();
});
});
diff --git a/packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js
index 416830c455..70d3e8791e 100644
--- a/packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js
+++ b/packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js
@@ -42,17 +42,22 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
describe.each(table)(`useFocus`, hasPointerEvents => {
let container;
+ let container2;
beforeEach(() => {
initializeModules(hasPointerEvents);
container = document.createElement('div');
document.body.appendChild(container);
+ container2 = document.createElement('div');
+ document.body.appendChild(container2);
});
afterEach(() => {
ReactDOM.render(null, container);
document.body.removeChild(container);
+ document.body.removeChild(container2);
container = null;
+ container2 = null;
});
describe('disabled', () => {
@@ -367,6 +372,40 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
);
});
+ // @gate experimental
+ it('is called after many elements are unmounted', () => {
+ const buttonRef = React.createRef();
+ const inputRef = React.createRef();
+
+ const Component = ({show}) => {
+ useFocusWithin(ref, {
+ onBeforeBlurWithin,
+ onAfterBlurWithin,
+ });
+ return (
+
+ {show && }
+ {show && }
+ {show && }
+ {show && }
+ {!show && }
+ {show && }
+
+
+
+ );
+ };
+
+ ReactDOM.render(, container);
+
+ inputRef.current.focus();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+ ReactDOM.render(, container);
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
+ });
+
// @gate experimental
it('is called after a nested focused element is unmounted (with scope query)', () => {
const TestScope = React.unstable_createScope();
@@ -431,9 +470,6 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
);
};
- const container2 = document.createElement('div');
- document.body.appendChild(container2);
-
const root = ReactDOM.createRoot(container2);
act(() => {
@@ -460,8 +496,74 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
resolve();
+ });
- document.body.removeChild(container2);
+ // @gate experimental
+ it('is called after a focused suspended element is hidden then shown', () => {
+ const Suspense = React.Suspense;
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+ const buttonRef = React.createRef();
+
+ function Child() {
+ if (suspend) {
+ throw promise;
+ } else {
+ return ;
+ }
+ }
+
+ const Component = ({show}) => {
+ useFocusWithin(ref, {
+ onBeforeBlurWithin,
+ onAfterBlurWithin,
+ });
+
+ return (
+
+ Loading...}>
+
+
+
+ );
+ };
+
+ const root = ReactDOM.createRoot(container2);
+
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ suspend = true;
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
+
+ buttonRef.current.focus();
+ suspend = false;
+ act(() => {
+ root.render();
+ });
+ jest.runAllTimers();
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
+
+ resolve();
});
});
});
diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index 91aa983981..5b1d0edb8d 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -304,8 +304,9 @@ export function getPublicInstance(instance: Instance): * {
return instance.canonical;
}
-export function prepareForCommit(containerInfo: Container): void {
+export function prepareForCommit(containerInfo: Container): null | Object {
// Noop
+ return null;
}
export function prepareUpdate(
@@ -524,3 +525,11 @@ export function unmountEventListener(listener: any) {
export function validateEventListenerTarget(target: any, listener: any) {
throw new Error('Not yet implemented.');
}
+
+export function beforeActiveInstanceBlur() {
+ // noop
+}
+
+export function afterActiveInstanceBlur() {
+ // noop
+}
diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js
index a6ed4de72c..726c743247 100644
--- a/packages/react-native-renderer/src/ReactNativeHostConfig.js
+++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js
@@ -223,8 +223,9 @@ export function getPublicInstance(instance: Instance): * {
return instance;
}
-export function prepareForCommit(containerInfo: Container): void {
+export function prepareForCommit(containerInfo: Container): null | Object {
// Noop
+ return null;
}
export function prepareUpdate(
@@ -573,3 +574,11 @@ export function unmountEventListener(listener: any) {
export function validateEventListenerTarget(target: any, listener: any) {
throw new Error('Not yet implemented.');
}
+
+export function beforeActiveInstanceBlur() {
+ // noop
+}
+
+export function afterActiveInstanceBlur() {
+ // noop
+}
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index e99c250b3b..7d26eb47bb 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -363,7 +363,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
cancelTimeout: clearTimeout,
noTimeout: -1,
- prepareForCommit(): void {},
+ prepareForCommit(): null | Object {
+ return null;
+ },
resetAfterCommit(): void {},
@@ -439,6 +441,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
beforeRemoveInstance(instance: any): void {
// NO-OP
},
+
+ beforeActiveInstanceBlur() {
+ // NO-OP
+ },
+
+ afterActiveInstanceBlur() {
+ // NO-OP
+ },
};
const hostConfig = useMutation
diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js
index a6a4b454e8..1a8be6e497 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -342,20 +342,45 @@ export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
);
}
-// 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;
- let lastChild = null;
+function doesFiberContain(parentFiber: Fiber, childFiber: Fiber): boolean {
+ let node = childFiber;
+ const parentFiberAlternate = parentFiber.alternate;
while (node !== null) {
- if (
- node.effectTag & Deletion ||
- (isFiberSuspenseAndTimedOut(node) && node.child === lastChild)
- ) {
+ if (node === parentFiber || node === parentFiberAlternate) {
return true;
}
- lastChild = node;
node = node.return;
}
return false;
}
+
+function isFiberTimedOutSuspenseThatContainsTargetFiber(
+ fiber: Fiber,
+ targetFiber: Fiber,
+): boolean {
+ const child = fiber.child;
+ return (
+ isFiberSuspenseAndTimedOut(fiber) &&
+ child !== null &&
+ doesFiberContain(child, targetFiber)
+ );
+}
+
+function isFiberDeletedAndContainsTargetFiber(
+ fiber: Fiber,
+ targetFiber: Fiber,
+): boolean {
+ return (
+ (fiber.effectTag & Deletion) !== 0 && doesFiberContain(fiber, targetFiber)
+ );
+}
+
+export function isFiberHiddenOrDeletedAndContains(
+ parentFiber: Fiber,
+ childFiber: Fiber,
+): boolean {
+ return (
+ isFiberDeletedAndContainsTargetFiber(parentFiber, childFiber) ||
+ isFiberTimedOutSuspenseThatContainsTargetFiber(parentFiber, childFiber)
+ );
+}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 376a7c9670..0428797388 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -72,6 +72,8 @@ import {
cancelTimeout,
noTimeout,
warnsIfNotActing,
+ beforeActiveInstanceBlur,
+ afterActiveInstanceBlur,
} from './ReactFiberHostConfig';
import {
@@ -193,6 +195,7 @@ import {onCommitRoot} from './ReactFiberDevToolsHook.new';
// Used by `act`
import enqueueTask from 'shared/enqueueTask';
+import {isFiberHiddenOrDeletedAndContains} from './ReactFiberTreeReflection';
const ceil = Math.ceil;
@@ -298,6 +301,9 @@ let currentEventTime: ExpirationTime = NoWork;
// We warn about state updates for unmounted components differently in this case.
let isFlushingPassiveEffects = false;
+let focusedInstanceHandle: null | Fiber = null;
+let shouldFireAfterActiveInstanceBlur: boolean = false;
+
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
@@ -1902,7 +1908,9 @@ function commitRootImpl(root, renderPriorityLevel) {
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
- prepareForCommit(root.containerInfo);
+ focusedInstanceHandle = prepareForCommit(root.containerInfo);
+ shouldFireAfterActiveInstanceBlur = false;
+
nextEffect = firstEffect;
do {
if (__DEV__) {
@@ -1924,6 +1932,9 @@ function commitRootImpl(root, renderPriorityLevel) {
}
} while (nextEffect !== null);
+ // We no longer need to track the active instance fiber
+ focusedInstanceHandle = null;
+
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
@@ -1957,6 +1968,10 @@ function commitRootImpl(root, renderPriorityLevel) {
}
}
} while (nextEffect !== null);
+
+ if (shouldFireAfterActiveInstanceBlur) {
+ afterActiveInstanceBlur();
+ }
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
@@ -2124,6 +2139,14 @@ function commitRootImpl(root, renderPriorityLevel) {
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
+ if (
+ !shouldFireAfterActiveInstanceBlur &&
+ focusedInstanceHandle !== null &&
+ isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
+ ) {
+ shouldFireAfterActiveInstanceBlur = true;
+ beforeActiveInstanceBlur();
+ }
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
setCurrentDebugFiberInDEV(nextEffect);
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 713bcc23f8..5dd70549f2 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -72,6 +72,8 @@ import {
cancelTimeout,
noTimeout,
warnsIfNotActing,
+ beforeActiveInstanceBlur,
+ afterActiveInstanceBlur,
} from './ReactFiberHostConfig';
import {
@@ -191,6 +193,7 @@ import {onCommitRoot} from './ReactFiberDevToolsHook.old';
// Used by `act`
import enqueueTask from 'shared/enqueueTask';
+import {isFiberHiddenOrDeletedAndContains} from './ReactFiberTreeReflection';
const ceil = Math.ceil;
@@ -296,6 +299,9 @@ let currentEventTime: ExpirationTime = NoWork;
// We warn about state updates for unmounted components differently in this case.
let isFlushingPassiveEffects = false;
+let focusedInstanceHandle: null | Fiber = null;
+let shouldFireAfterActiveInstanceBlur: boolean = false;
+
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
@@ -1921,7 +1927,9 @@ function commitRootImpl(root, renderPriorityLevel) {
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
- prepareForCommit(root.containerInfo);
+ focusedInstanceHandle = prepareForCommit(root.containerInfo);
+ shouldFireAfterActiveInstanceBlur = false;
+
nextEffect = firstEffect;
do {
if (__DEV__) {
@@ -1943,6 +1951,9 @@ function commitRootImpl(root, renderPriorityLevel) {
}
} while (nextEffect !== null);
+ // We no longer need to track the active instance fiber
+ focusedInstanceHandle = null;
+
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
@@ -1976,6 +1987,10 @@ function commitRootImpl(root, renderPriorityLevel) {
}
}
} while (nextEffect !== null);
+
+ if (shouldFireAfterActiveInstanceBlur) {
+ afterActiveInstanceBlur();
+ }
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
@@ -2143,6 +2158,14 @@ function commitRootImpl(root, renderPriorityLevel) {
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
+ if (
+ !shouldFireAfterActiveInstanceBlur &&
+ focusedInstanceHandle !== null &&
+ isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
+ ) {
+ shouldFireAfterActiveInstanceBlur = true;
+ beforeActiveInstanceBlur();
+ }
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
setCurrentDebugFiberInDEV(nextEffect);
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index 9fc4f552da..1237a89c74 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -25,7 +25,9 @@ describe('ReactFiberHostContext', () => {
it('works with null host context', () => {
let creates = 0;
const Renderer = ReactFiberReconciler({
- prepareForCommit: function() {},
+ prepareForCommit: function() {
+ return null;
+ },
resetAfterCommit: function() {},
getRootHostContext: function() {
return null;
@@ -76,6 +78,7 @@ describe('ReactFiberHostContext', () => {
const Renderer = ReactFiberReconciler({
prepareForCommit: function(hostContext) {
expect(hostContext).toBe(rootContext);
+ return null;
},
resetAfterCommit: function(hostContext) {
expect(hostContext).toBe(rootContext);
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 7af28eac24..eb3447f3f9 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -89,6 +89,8 @@ export const makeOpaqueHydratingObject =
export const makeClientId = $$$hostConfig.makeClientId;
export const makeClientIdInDEV = $$$hostConfig.makeClientIdInDEV;
export const makeServerId = $$$hostConfig.makeServerId;
+export const beforeActiveInstanceBlur = $$$hostConfig.beforeActiveInstanceBlur;
+export const afterActiveInstanceBlur = $$$hostConfig.afterActiveInstanceBlur;
// -------------------
// Mutation
diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js
index c3eda48d2a..b9c2c4adf4 100644
--- a/packages/react-test-renderer/src/ReactTestHostConfig.js
+++ b/packages/react-test-renderer/src/ReactTestHostConfig.js
@@ -143,8 +143,9 @@ export function getChildHostContext(
return NO_CONTEXT;
}
-export function prepareForCommit(containerInfo: Container): void {
+export function prepareForCommit(containerInfo: Container): null | Object {
// noop
+ return null;
}
export function resetAfterCommit(containerInfo: Container): void {
@@ -445,3 +446,11 @@ export function unmountEventListener(listener: any) {
export function validateEventListenerTarget(target: any, listener: any) {
throw new Error('Not yet implemented.');
}
+
+export function beforeActiveInstanceBlur() {
+ // noop
+}
+
+export function afterActiveInstanceBlur() {
+ // noop
+}