@samitouri / QOS-React-2 / commits / a152827ef6

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 +}