Refine the heuristics around beforeblur/afterblur (#18668)
* Refine the heuristics around beforeblur/afterblur
Dominic Gannaway committed
Apr 20, 2020 at 19:32 UTC
a152827ef697c55f89926f9b6b7aa436f1c0504e
14 files changed
+381
-52
packages/react-art/src/ReactARTHostConfig.js
+9
@@ -306,6 +306,7 @@ export function getPublicInstance(instance) {
306
307
export function prepareForCommit() {
308
// Noop
309
+ return null;
310
}
311
312
export function prepareUpdate(domElement, type, oldProps, newProps) {
@@ -507,3 +508,11 @@ export function unmountEventListener(listener: any) {
508
export function validateEventListenerTarget(target: any, listener: any) {
509
throw new Error('Not yet implemented.');
510
}
511
+
512
+export function beforeActiveInstanceBlur() {
513
+ // noop
514
+}
515
+
516
+export function afterActiveInstanceBlur() {
517
+ // noop
518
+}
packages/react-dom/src/client/ReactDOMHostConfig.js
+20
-19
@@ -91,10 +91,6 @@ import {
91
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
92
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
93
94
-// TODO: This is an exposed internal, we should move this around
95
-// so this isn't the case.
96
-import {isFiberInsideHiddenOrRemovedTree} from 'react-reconciler/src/ReactFiberTreeReflection';
97
-
94
export type ReactListenerEvent = ReactDOMListenerEvent;
95
export type ReactListenerMap = ReactDOMListenerMap;
96
export type ReactListener = ReactDOMListener;
@@ -159,7 +155,6 @@ export opaque type OpaqueIDType =
155
};
156
157
type SelectionInformation = {|
162
- activeElementDetached: null | HTMLElement,
158
focusedElem: null | HTMLElement,
159
selectionRange: mixed,
160
|};
@@ -247,32 +242,40 @@ export function getPublicInstance(instance: Instance): * {
242
return instance;
243
}
244
250
-export function prepareForCommit(containerInfo: Container): void {
245
+export function prepareForCommit(containerInfo: Container): Object | null {
246
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
247
selectionInformation = getSelectionInformation();
248
+ let activeInstance = null;
249
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
250
const focusedElem = selectionInformation.focusedElem;
251
if (focusedElem !== null) {
256
- const instance = getClosestInstanceFromNode(focusedElem);
257
- if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
258
- dispatchBeforeDetachedBlur(focusedElem);
259
- }
252
+ activeInstance = getClosestInstanceFromNode(focusedElem);
253
}
254
}
255
ReactBrowserEventEmitterSetEnabled(false);
256
+ return activeInstance;
257
+}
258
+
259
+export function beforeActiveInstanceBlur(): void {
260
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
261
+ ReactBrowserEventEmitterSetEnabled(true);
262
+ dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
263
+ ReactBrowserEventEmitterSetEnabled(false);
264
+ }
265
+}
266
+
267
+export function afterActiveInstanceBlur(): void {
268
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
269
+ ReactBrowserEventEmitterSetEnabled(true);
270
+ dispatchAfterDetachedBlur((selectionInformation: any).focusedElem);
271
+ ReactBrowserEventEmitterSetEnabled(false);
272
+ }
273
}
274
275
export function resetAfterCommit(containerInfo: Container): void {
276
restoreSelection(selectionInformation);
277
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
278
eventsEnabled = null;
269
- if (enableDeprecatedFlareAPI || enableUseEventAPI) {
270
- const activeElementDetached = (selectionInformation: any)
271
- .activeElementDetached;
272
- if (activeElementDetached !== null) {
273
- dispatchAfterDetachedBlur(activeElementDetached);
274
- }
275
- }
279
selectionInformation = null;
280
}
281
@@ -525,8 +528,6 @@ function createEvent(type: TopLevelType): Event {
528
}
529
530
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
528
- ((selectionInformation: any): SelectionInformation).activeElementDetached = target;
529
-
531
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
532
const event = createEvent(TOP_BEFORE_BLUR);
533
// Dispatch "beforeblur" directly on the target,
packages/react-dom/src/client/ReactInputSelection.js
-2
@@ -100,8 +100,6 @@ export function hasSelectionCapabilities(elem) {
100
export function getSelectionInformation() {
101
const focusedElem = getActiveElementDeep();
102
return {
103
- // Used by Flare
104
- activeElementDetached: null,
103
focusedElem: focusedElem,
104
selectionRange: hasSelectionCapabilities(focusedElem)
105
? getSelection(focusedElem)
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+116
-10
@@ -16,7 +16,8 @@ let ReactFeatureFlags;
16
let ReactDOM;
17
let FocusWithinResponder;
18
let useFocusWithin;
19
-let Scheduler;
19
+let ReactTestRenderer;
20
+let act;
21
22
const initializeModules = hasPointerEvents => {
23
setPointerEvent(hasPointerEvents);
@@ -26,7 +27,8 @@ const initializeModules = hasPointerEvents => {
27
ReactFeatureFlags.enableScopeAPI = true;
28
React = require('react');
29
ReactDOM = require('react-dom');
29
- Scheduler = require('scheduler');
30
+ ReactTestRenderer = require('react-test-renderer');
31
+ act = ReactTestRenderer.act;
32
33
// TODO: This import throws outside of experimental mode. Figure out better
34
// strategy for gated imports.
@@ -43,17 +45,22 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
45
46
describe.each(table)('FocusWithin responder', hasPointerEvents => {
47
let container;
48
+ let container2;
49
50
beforeEach(() => {
51
initializeModules();
52
container = document.createElement('div');
53
document.body.appendChild(container);
54
+ container2 = document.createElement('div');
55
+ document.body.appendChild(container2);
56
});
57
58
afterEach(() => {
59
ReactDOM.render(null, container);
60
document.body.removeChild(container);
61
+ document.body.removeChild(container2);
62
container = null;
63
+ container2 = null;
64
});
65
66
describe('disabled', () => {
@@ -366,6 +373,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
373
);
374
});
375
376
+ // @gate experimental
377
+ it('is called after many elements are unmounted', () => {
378
+ const buttonRef = React.createRef();
379
+ const inputRef = React.createRef();
380
+
381
+ const Component = ({show}) => {
382
+ const listener = useFocusWithin({
383
+ onBeforeBlurWithin,
384
+ onAfterBlurWithin,
385
+ });
386
+ return (
387
+ <div ref={ref} DEPRECATED_flareListeners={listener}>
388
+ {show && <button>Press me!</button>}
389
+ {show && <button>Press me!</button>}
390
+ {show && <input ref={inputRef} />}
391
+ {show && <button>Press me!</button>}
392
+ {!show && <button ref={buttonRef}>Press me!</button>}
393
+ {show && <button>Press me!</button>}
394
+ <button>Press me!</button>
395
+ <button>Press me!</button>
396
+ </div>
397
+ );
398
+ };
399
+
400
+ ReactDOM.render(<Component show={true} />, container);
401
+
402
+ inputRef.current.focus();
403
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
404
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
405
+ ReactDOM.render(<Component show={false} />, container);
406
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
407
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
408
+ });
409
+
410
// @gate experimental
411
it('is called after a nested focused element is unmounted (with scope query)', () => {
412
const TestScope = React.unstable_createScope();
@@ -430,12 +471,10 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
471
);
472
};
473
433
- const container2 = document.createElement('div');
434
- document.body.appendChild(container2);
435
-
474
const root = ReactDOM.createRoot(container2);
437
- root.render(<Component />);
438
- Scheduler.unstable_flushAll();
475
+ act(() => {
476
+ root.render(<Component />);
477
+ });
478
jest.runAllTimers();
479
expect(container2.innerHTML).toBe('<div><input></div>');
480
@@ -447,8 +486,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
486
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
487
488
suspend = true;
450
- root.render(<Component />);
451
- Scheduler.unstable_flushAll();
489
+ act(() => {
490
+ root.render(<Component />);
491
+ });
492
jest.runAllTimers();
493
expect(container2.innerHTML).toBe(
494
'<div><input style="display: none;">Loading...</div>',
@@ -456,8 +496,74 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
496
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
497
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
498
resolve();
499
+ });
500
+
501
+ // @gate experimental
502
+ it('is called after a focused suspended element is hidden then shown', () => {
503
+ const Suspense = React.Suspense;
504
+ let suspend = false;
505
+ let resolve;
506
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
507
+ const buttonRef = React.createRef();
508
+
509
+ function Child() {
510
+ if (suspend) {
511
+ throw promise;
512
+ } else {
513
+ return <input ref={innerRef} />;
514
+ }
515
+ }
516
460
- document.body.removeChild(container2);
517
+ const Component = ({show}) => {
518
+ const listener = useFocusWithin({
519
+ onBeforeBlurWithin,
520
+ onAfterBlurWithin,
521
+ });
522
+
523
+ return (
524
+ <div ref={ref} DEPRECATED_flareListeners={listener}>
525
+ <Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
526
+ <Child />
527
+ </Suspense>
528
+ </div>
529
+ );
530
+ };
531
+
532
+ const root = ReactDOM.createRoot(container2);
533
+
534
+ act(() => {
535
+ root.render(<Component />);
536
+ });
537
+ jest.runAllTimers();
538
+
539
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
540
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
541
+
542
+ suspend = true;
543
+ act(() => {
544
+ root.render(<Component />);
545
+ });
546
+ jest.runAllTimers();
547
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
548
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
549
+
550
+ act(() => {
551
+ root.render(<Component />);
552
+ });
553
+ jest.runAllTimers();
554
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
555
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
556
+
557
+ buttonRef.current.focus();
558
+ suspend = false;
559
+ act(() => {
560
+ root.render(<Component />);
561
+ });
562
+ jest.runAllTimers();
563
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
564
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
565
+
566
+ resolve();
567
});
568
});
569
packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js
+106
-4
@@ -42,17 +42,22 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
42
43
describe.each(table)(`useFocus`, hasPointerEvents => {
44
let container;
45
+ let container2;
46
47
beforeEach(() => {
48
initializeModules(hasPointerEvents);
49
container = document.createElement('div');
50
document.body.appendChild(container);
51
+ container2 = document.createElement('div');
52
+ document.body.appendChild(container2);
53
});
54
55
afterEach(() => {
56
ReactDOM.render(null, container);
57
document.body.removeChild(container);
58
+ document.body.removeChild(container2);
59
container = null;
60
+ container2 = null;
61
});
62
63
describe('disabled', () => {
@@ -367,6 +372,40 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
372
);
373
});
374
375
+ // @gate experimental
376
+ it('is called after many elements are unmounted', () => {
377
+ const buttonRef = React.createRef();
378
+ const inputRef = React.createRef();
379
+
380
+ const Component = ({show}) => {
381
+ useFocusWithin(ref, {
382
+ onBeforeBlurWithin,
383
+ onAfterBlurWithin,
384
+ });
385
+ return (
386
+ <div ref={ref}>
387
+ {show && <button>Press me!</button>}
388
+ {show && <button>Press me!</button>}
389
+ {show && <input ref={inputRef} />}
390
+ {show && <button>Press me!</button>}
391
+ {!show && <button ref={buttonRef}>Press me!</button>}
392
+ {show && <button>Press me!</button>}
393
+ <button>Press me!</button>
394
+ <button>Press me!</button>
395
+ </div>
396
+ );
397
+ };
398
+
399
+ ReactDOM.render(<Component show={true} />, container);
400
+
401
+ inputRef.current.focus();
402
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
403
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
404
+ ReactDOM.render(<Component show={false} />, container);
405
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
406
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
407
+ });
408
+
409
// @gate experimental
410
it('is called after a nested focused element is unmounted (with scope query)', () => {
411
const TestScope = React.unstable_createScope();
@@ -431,9 +470,6 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
470
);
471
};
472
434
- const container2 = document.createElement('div');
435
- document.body.appendChild(container2);
436
-
473
const root = ReactDOM.createRoot(container2);
474
475
act(() => {
@@ -460,8 +496,74 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
496
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
497
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
498
resolve();
499
+ });
500
464
- document.body.removeChild(container2);
501
+ // @gate experimental
502
+ it('is called after a focused suspended element is hidden then shown', () => {
503
+ const Suspense = React.Suspense;
504
+ let suspend = false;
505
+ let resolve;
506
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
507
+ const buttonRef = React.createRef();
508
+
509
+ function Child() {
510
+ if (suspend) {
511
+ throw promise;
512
+ } else {
513
+ return <input ref={innerRef} />;
514
+ }
515
+ }
516
+
517
+ const Component = ({show}) => {
518
+ useFocusWithin(ref, {
519
+ onBeforeBlurWithin,
520
+ onAfterBlurWithin,
521
+ });
522
+
523
+ return (
524
+ <div ref={ref}>
525
+ <Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
526
+ <Child />
527
+ </Suspense>
528
+ </div>
529
+ );
530
+ };
531
+
532
+ const root = ReactDOM.createRoot(container2);
533
+
534
+ act(() => {
535
+ root.render(<Component />);
536
+ });
537
+ jest.runAllTimers();
538
+
539
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
540
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
541
+
542
+ suspend = true;
543
+ act(() => {
544
+ root.render(<Component />);
545
+ });
546
+ jest.runAllTimers();
547
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
548
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
549
+
550
+ act(() => {
551
+ root.render(<Component />);
552
+ });
553
+ jest.runAllTimers();
554
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
555
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
556
+
557
+ buttonRef.current.focus();
558
+ suspend = false;
559
+ act(() => {
560
+ root.render(<Component />);
561
+ });
562
+ jest.runAllTimers();
563
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
564
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
565
+
566
+ resolve();
567
});
568
});
569
});
packages/react-native-renderer/src/ReactFabricHostConfig.js
+10
-1
@@ -304,8 +304,9 @@ export function getPublicInstance(instance: Instance): * {
304
return instance.canonical;
305
}
306
307
-export function prepareForCommit(containerInfo: Container): void {
307
+export function prepareForCommit(containerInfo: Container): null | Object {
308
// Noop
309
+ return null;
310
}
311
312
export function prepareUpdate(
@@ -524,3 +525,11 @@ export function unmountEventListener(listener: any) {
525
export function validateEventListenerTarget(target: any, listener: any) {
526
throw new Error('Not yet implemented.');
527
}
528
+
529
+export function beforeActiveInstanceBlur() {
530
+ // noop
531
+}
532
+
533
+export function afterActiveInstanceBlur() {
534
+ // noop
535
+}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+10
-1
@@ -223,8 +223,9 @@ export function getPublicInstance(instance: Instance): * {
223
return instance;
224
}
225
226
-export function prepareForCommit(containerInfo: Container): void {
226
+export function prepareForCommit(containerInfo: Container): null | Object {
227
// Noop
228
+ return null;
229
}
230
231
export function prepareUpdate(
@@ -573,3 +574,11 @@ export function unmountEventListener(listener: any) {
574
export function validateEventListenerTarget(target: any, listener: any) {
575
throw new Error('Not yet implemented.');
576
}
577
+
578
+export function beforeActiveInstanceBlur() {
579
+ // noop
580
+}
581
+
582
+export function afterActiveInstanceBlur() {
583
+ // noop
584
+}
packages/react-noop-renderer/src/createReactNoop.js
+11
-1
@@ -363,7 +363,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
363
cancelTimeout: clearTimeout,
364
noTimeout: -1,
365
366
- prepareForCommit(): void {},
366
+ prepareForCommit(): null | Object {
367
+ return null;
368
+ },
369
370
resetAfterCommit(): void {},
371
@@ -439,6 +441,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
441
beforeRemoveInstance(instance: any): void {
442
// NO-OP
443
},
444
+
445
+ beforeActiveInstanceBlur() {
446
+ // NO-OP
447
+ },
448
+
449
+ afterActiveInstanceBlur() {
450
+ // NO-OP
451
+ },
452
};
453
454
const hostConfig = useMutation
packages/react-reconciler/src/ReactFiberTreeReflection.js
+35
-10
@@ -342,20 +342,45 @@ export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
342
);
343
}
344
345
-// This is only safe to call in the commit phase when the return tree is consistent.
346
-// It should not be used anywhere else. See PR #18609 for details.
347
-export function isFiberInsideHiddenOrRemovedTree(fiber: Fiber): boolean {
348
- let node = fiber;
349
- let lastChild = null;
345
+function doesFiberContain(parentFiber: Fiber, childFiber: Fiber): boolean {
346
+ let node = childFiber;
347
+ const parentFiberAlternate = parentFiber.alternate;
348
while (node !== null) {
351
- if (
352
- node.effectTag & Deletion ||
353
- (isFiberSuspenseAndTimedOut(node) && node.child === lastChild)
354
- ) {
349
+ if (node === parentFiber || node === parentFiberAlternate) {
350
return true;
351
}
357
- lastChild = node;
352
node = node.return;
353
}
354
return false;
355
}
356
+
357
+function isFiberTimedOutSuspenseThatContainsTargetFiber(
358
+ fiber: Fiber,
359
+ targetFiber: Fiber,
360
+): boolean {
361
+ const child = fiber.child;
362
+ return (
363
+ isFiberSuspenseAndTimedOut(fiber) &&
364
+ child !== null &&
365
+ doesFiberContain(child, targetFiber)
366
+ );
367
+}
368
+
369
+function isFiberDeletedAndContainsTargetFiber(
370
+ fiber: Fiber,
371
+ targetFiber: Fiber,
372
+): boolean {
373
+ return (
374
+ (fiber.effectTag & Deletion) !== 0 && doesFiberContain(fiber, targetFiber)
375
+ );
376
+}
377
+
378
+export function isFiberHiddenOrDeletedAndContains(
379
+ parentFiber: Fiber,
380
+ childFiber: Fiber,
381
+): boolean {
382
+ return (
383
+ isFiberDeletedAndContainsTargetFiber(parentFiber, childFiber) ||
384
+ isFiberTimedOutSuspenseThatContainsTargetFiber(parentFiber, childFiber)
385
+ );
386
+}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+24
-1
@@ -72,6 +72,8 @@ import {
72
cancelTimeout,
73
noTimeout,
74
warnsIfNotActing,
75
+ beforeActiveInstanceBlur,
76
+ afterActiveInstanceBlur,
77
} from './ReactFiberHostConfig';
78
79
import {
@@ -193,6 +195,7 @@ import {onCommitRoot} from './ReactFiberDevToolsHook.new';
195
196
// Used by `act`
197
import enqueueTask from 'shared/enqueueTask';
198
+import {isFiberHiddenOrDeletedAndContains} from './ReactFiberTreeReflection';
199
200
const ceil = Math.ceil;
201
@@ -298,6 +301,9 @@ let currentEventTime: ExpirationTime = NoWork;
301
// We warn about state updates for unmounted components differently in this case.
302
let isFlushingPassiveEffects = false;
303
304
+let focusedInstanceHandle: null | Fiber = null;
305
+let shouldFireAfterActiveInstanceBlur: boolean = false;
306
+
307
export function getWorkInProgressRoot(): FiberRoot | null {
308
return workInProgressRoot;
309
}
@@ -1902,7 +1908,9 @@ function commitRootImpl(root, renderPriorityLevel) {
1908
// The first phase a "before mutation" phase. We use this phase to read the
1909
// state of the host tree right before we mutate it. This is where
1910
// getSnapshotBeforeUpdate is called.
1905
- prepareForCommit(root.containerInfo);
1911
+ focusedInstanceHandle = prepareForCommit(root.containerInfo);
1912
+ shouldFireAfterActiveInstanceBlur = false;
1913
+
1914
nextEffect = firstEffect;
1915
do {
1916
if (__DEV__) {
@@ -1924,6 +1932,9 @@ function commitRootImpl(root, renderPriorityLevel) {
1932
}
1933
} while (nextEffect !== null);
1934
1935
+ // We no longer need to track the active instance fiber
1936
+ focusedInstanceHandle = null;
1937
+
1938
if (enableProfilerTimer) {
1939
// Mark the current commit time to be shared by all Profilers in this
1940
// batch. This enables them to be grouped later.
@@ -1957,6 +1968,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1968
}
1969
}
1970
} while (nextEffect !== null);
1971
+
1972
+ if (shouldFireAfterActiveInstanceBlur) {
1973
+ afterActiveInstanceBlur();
1974
+ }
1975
resetAfterCommit(root.containerInfo);
1976
1977
// The work-in-progress tree is now the current tree. This must come after
@@ -2124,6 +2139,14 @@ function commitRootImpl(root, renderPriorityLevel) {
2139
2140
function commitBeforeMutationEffects() {
2141
while (nextEffect !== null) {
2142
+ if (
2143
+ !shouldFireAfterActiveInstanceBlur &&
2144
+ focusedInstanceHandle !== null &&
2145
+ isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
2146
+ ) {
2147
+ shouldFireAfterActiveInstanceBlur = true;
2148
+ beforeActiveInstanceBlur();
2149
+ }
2150
const effectTag = nextEffect.effectTag;
2151
if ((effectTag & Snapshot) !== NoEffect) {
2152
setCurrentDebugFiberInDEV(nextEffect);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+24
-1
@@ -72,6 +72,8 @@ import {
72
cancelTimeout,
73
noTimeout,
74
warnsIfNotActing,
75
+ beforeActiveInstanceBlur,
76
+ afterActiveInstanceBlur,
77
} from './ReactFiberHostConfig';
78
79
import {
@@ -191,6 +193,7 @@ import {onCommitRoot} from './ReactFiberDevToolsHook.old';
193
194
// Used by `act`
195
import enqueueTask from 'shared/enqueueTask';
196
+import {isFiberHiddenOrDeletedAndContains} from './ReactFiberTreeReflection';
197
198
const ceil = Math.ceil;
199
@@ -296,6 +299,9 @@ let currentEventTime: ExpirationTime = NoWork;
299
// We warn about state updates for unmounted components differently in this case.
300
let isFlushingPassiveEffects = false;
301
302
+let focusedInstanceHandle: null | Fiber = null;
303
+let shouldFireAfterActiveInstanceBlur: boolean = false;
304
+
305
export function getWorkInProgressRoot(): FiberRoot | null {
306
return workInProgressRoot;
307
}
@@ -1921,7 +1927,9 @@ function commitRootImpl(root, renderPriorityLevel) {
1927
// The first phase a "before mutation" phase. We use this phase to read the
1928
// state of the host tree right before we mutate it. This is where
1929
// getSnapshotBeforeUpdate is called.
1924
- prepareForCommit(root.containerInfo);
1930
+ focusedInstanceHandle = prepareForCommit(root.containerInfo);
1931
+ shouldFireAfterActiveInstanceBlur = false;
1932
+
1933
nextEffect = firstEffect;
1934
do {
1935
if (__DEV__) {
@@ -1943,6 +1951,9 @@ function commitRootImpl(root, renderPriorityLevel) {
1951
}
1952
} while (nextEffect !== null);
1953
1954
+ // We no longer need to track the active instance fiber
1955
+ focusedInstanceHandle = null;
1956
+
1957
if (enableProfilerTimer) {
1958
// Mark the current commit time to be shared by all Profilers in this
1959
// batch. This enables them to be grouped later.
@@ -1976,6 +1987,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1987
}
1988
}
1989
} while (nextEffect !== null);
1990
+
1991
+ if (shouldFireAfterActiveInstanceBlur) {
1992
+ afterActiveInstanceBlur();
1993
+ }
1994
resetAfterCommit(root.containerInfo);
1995
1996
// The work-in-progress tree is now the current tree. This must come after
@@ -2143,6 +2158,14 @@ function commitRootImpl(root, renderPriorityLevel) {
2158
2159
function commitBeforeMutationEffects() {
2160
while (nextEffect !== null) {
2161
+ if (
2162
+ !shouldFireAfterActiveInstanceBlur &&
2163
+ focusedInstanceHandle !== null &&
2164
+ isFiberHiddenOrDeletedAndContains(nextEffect, focusedInstanceHandle)
2165
+ ) {
2166
+ shouldFireAfterActiveInstanceBlur = true;
2167
+ beforeActiveInstanceBlur();
2168
+ }
2169
const effectTag = nextEffect.effectTag;
2170
if ((effectTag & Snapshot) !== NoEffect) {
2171
setCurrentDebugFiberInDEV(nextEffect);
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+4
-1
@@ -25,7 +25,9 @@ describe('ReactFiberHostContext', () => {
25
it('works with null host context', () => {
26
let creates = 0;
27
const Renderer = ReactFiberReconciler({
28
- prepareForCommit: function() {},
28
+ prepareForCommit: function() {
29
+ return null;
30
+ },
31
resetAfterCommit: function() {},
32
getRootHostContext: function() {
33
return null;
@@ -76,6 +78,7 @@ describe('ReactFiberHostContext', () => {
78
const Renderer = ReactFiberReconciler({
79
prepareForCommit: function(hostContext) {
80
expect(hostContext).toBe(rootContext);
81
+ return null;
82
},
83
resetAfterCommit: function(hostContext) {
84
expect(hostContext).toBe(rootContext);
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+2
@@ -89,6 +89,8 @@ export const makeOpaqueHydratingObject =
89
export const makeClientId = $$$hostConfig.makeClientId;
90
export const makeClientIdInDEV = $$$hostConfig.makeClientIdInDEV;
91
export const makeServerId = $$$hostConfig.makeServerId;
92
+export const beforeActiveInstanceBlur = $$$hostConfig.beforeActiveInstanceBlur;
93
+export const afterActiveInstanceBlur = $$$hostConfig.afterActiveInstanceBlur;
94
95
// -------------------
96
// Mutation
packages/react-test-renderer/src/ReactTestHostConfig.js
+10
-1
@@ -143,8 +143,9 @@ export function getChildHostContext(
143
return NO_CONTEXT;
144
}
145
146
-export function prepareForCommit(containerInfo: Container): void {
146
+export function prepareForCommit(containerInfo: Container): null | Object {
147
// noop
148
+ return null;
149
}
150
151
export function resetAfterCommit(containerInfo: Container): void {
@@ -445,3 +446,11 @@ export function unmountEventListener(listener: any) {
446
export function validateEventListenerTarget(target: any, listener: any) {
447
throw new Error('Not yet implemented.');
448
}
449
+
450
+export function beforeActiveInstanceBlur() {
451
+ // noop
452
+}
453
+
454
+export function afterActiveInstanceBlur() {
455
+ // noop
456
+}