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

Move beforeblur phase to prepareForCommit (#18609)

Dominic Gannaway committed Apr 14, 2020 at 21:30 UTC bf55ea743496f4fb71df982af0324e322f25046c
7 files changed +98 -86
packages/react-dom/src/client/ReactDOMHostConfig.js
+18 -45
@@ -73,7 +73,6 @@ import {
73 enableUseEventAPI,
74 enableScopeAPI,
75 } from 'shared/ReactFeatureFlags';
76 -import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
76 import {
77 RESPONDER_EVENT_SYSTEM,
78 IS_PASSIVE,
@@ -95,6 +94,10 @@ import {
94 import {getListenerMapForElement} from '../events/DOMEventListenerMap';
95 import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
96
97 +// TODO: This is an exposed internal, we should move this around
98 +// so this isn't the case.
99 +import {isFiberInsideHiddenOrRemovedTree} from 'react-reconciler/src/ReactFiberTreeReflection';
100 +
101 export type ReactListenerEvent = ReactDOMListenerEvent;
102 export type ReactListenerMap = ReactDOMListenerMap;
103 export type ReactListener = ReactDOMListener;
@@ -250,6 +253,15 @@ export function getPublicInstance(instance: Instance): * {
253 export function prepareForCommit(containerInfo: Container): void {
254 eventsEnabled = ReactBrowserEventEmitterIsEnabled();
255 selectionInformation = getSelectionInformation();
256 + if (enableDeprecatedFlareAPI || enableUseEventAPI) {
257 + const focusedElem = selectionInformation.focusedElem;
258 + if (focusedElem !== null) {
259 + const instance = getClosestInstanceFromNode(focusedElem);
260 + if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
261 + dispatchBeforeDetachedBlur(focusedElem);
262 + }
263 + }
264 + }
265 ReactBrowserEventEmitterSetEnabled(false);
266 }
267
@@ -532,18 +544,11 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
544 );
545 }
546 if (enableUseEventAPI) {
535 - try {
536 - // We need to temporarily enable the event system
537 - // to dispatch the "beforeblur" event.
538 - ReactBrowserEventEmitterSetEnabled(true);
539 - const event = createEvent(TOP_BEFORE_BLUR);
540 - // Dispatch "beforeblur" directly on the target,
541 - // so it gets picked up by the event system and
542 - // can propagate through the React internal tree.
543 - target.dispatchEvent(event);
544 - } finally {
545 - ReactBrowserEventEmitterSetEnabled(false);
546 - }
547 + const event = createEvent(TOP_BEFORE_BLUR);
548 + // Dispatch "beforeblur" directly on the target,
549 + // so it gets picked up by the event system and
550 + // can propagate through the React internal tree.
551 + target.dispatchEvent(event);
552 }
553 }
554
@@ -571,20 +576,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
576 }
577 }
578
574 -// This is a specific event for the React Flare
575 -// event system, so event responders can act
576 -// accordingly to a DOM node being unmounted that
577 -// previously had active document focus.
579 export function beforeRemoveInstance(
580 instance: Instance | TextInstance | SuspenseInstance,
581 ): void {
581 - if (
582 - (enableDeprecatedFlareAPI || enableUseEventAPI) &&
583 - selectionInformation &&
584 - instance === selectionInformation.focusedElem
585 - ) {
586 - dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
587 - }
582 if (enableUseEventAPI) {
583 // It's unfortunate that we have to do this cleanup, but
584 // it's necessary otherwise we will leak the host instances
@@ -674,28 +668,7 @@ export function clearSuspenseBoundaryFromContainer(
668 retryIfBlockedOn(container);
669 }
670
677 -function instanceContainsElem(instance: Instance, element: HTMLElement) {
678 - let fiber = getClosestInstanceFromNode(element);
679 - while (fiber !== null) {
680 - if (fiber.tag === HostComponent && fiber.stateNode === instance) {
681 - return true;
682 - }
683 - fiber = fiber.return;
684 - }
685 - return false;
686 -}
687 -
671 export function hideInstance(instance: Instance): void {
689 - // Ensure we trigger `onBeforeBlur` if the active focused elment
690 - // is ether the instance of a child or the instance. We need
691 - // to traverse the Fiber tree here rather than use node.contains()
692 - // as the child node might be inside a Portal.
693 - if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
694 - const focusedElem = selectionInformation.focusedElem;
695 - if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
696 - dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
697 - }
698 - }
672 // TODO: Does this work for all element types? What about MathML? Should we
673 // pass host context to this method?
674 instance = ((instance: any): HTMLElement);
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+35
@@ -23,6 +23,7 @@ const initializeModules = hasPointerEvents => {
23 jest.resetModules();
24 ReactFeatureFlags = require('shared/ReactFeatureFlags');
25 ReactFeatureFlags.enableDeprecatedFlareAPI = true;
26 + ReactFeatureFlags.enableScopeAPI = true;
27 React = require('react');
28 ReactDOM = require('react-dom');
29 Scheduler = require('scheduler');
@@ -364,6 +365,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
365 );
366 });
367
368 + // @gate experimental
369 + it('is called after a nested focused element is unmounted (with scope query)', () => {
370 + const TestScope = React.unstable_createScope();
371 + const testScopeQuery = (type, props) => true;
372 + let targetNodes;
373 + let targetNode;
374 +
375 + const Component = ({show}) => {
376 + const scopeRef = React.useRef(null);
377 + const listener = useFocusWithin({
378 + onBeforeBlurWithin(event) {
379 + const scope = scopeRef.current;
380 + targetNode = innerRef.current;
381 + targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
382 + },
383 + });
384 +
385 + return (
386 + <TestScope ref={scopeRef} DEPRECATED_flareListeners={[listener]}>
387 + {show && <input ref={innerRef} />}
388 + </TestScope>
389 + );
390 + };
391 +
392 + ReactDOM.render(<Component show={true} />, container);
393 +
394 + const inner = innerRef.current;
395 + const target = createEventTarget(inner);
396 + target.keydown({key: 'Tab'});
397 + target.focus();
398 + ReactDOM.render(<Component show={false} />, container);
399 + expect(targetNodes).toEqual([targetNode]);
400 + });
401 +
402 // @gate experimental
403 it('is called after a focused suspended element is hidden', () => {
404 const Suspense = React.Suspense;
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+9 -5
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
1116 // get GC:ed but we don't know which for sure which parent is the current
1117 // one so we'll settle for GC:ing the subtree of this child. This child
1118 // itself will be GC:ed when the parent updates the next time.
1119 - fiber.return = null;
1119 + fiber.alternate = null;
1120 fiber.child = null;
1121 - fiber.memoizedState = null;
1122 - fiber.updateQueue = null;
1121 fiber.dependencies = null;
1124 - fiber.alternate = null;
1122 fiber.firstEffect = null;
1123 fiber.lastEffect = null;
1127 - fiber.pendingProps = null;
1124 fiber.memoizedProps = null;
1125 + fiber.memoizedState = null;
1126 + fiber.pendingProps = null;
1127 + fiber.return = null;
1128 + fiber.sibling = null;
1129 fiber.stateNode = null;
1130 + fiber.updateQueue = null;
1131 + if (__DEV__) {
1132 + fiber._debugOwner = null;
1133 + }
1134 }
1135
1136 function emptyPortalContainer(current: Fiber) {
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+9 -5
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
1116 // get GC:ed but we don't know which for sure which parent is the current
1117 // one so we'll settle for GC:ing the subtree of this child. This child
1118 // itself will be GC:ed when the parent updates the next time.
1119 - fiber.return = null;
1119 + fiber.alternate = null;
1120 fiber.child = null;
1121 - fiber.memoizedState = null;
1122 - fiber.updateQueue = null;
1121 fiber.dependencies = null;
1124 - fiber.alternate = null;
1122 fiber.firstEffect = null;
1123 fiber.lastEffect = null;
1127 - fiber.pendingProps = null;
1124 fiber.memoizedProps = null;
1125 + fiber.memoizedState = null;
1126 + fiber.pendingProps = null;
1127 + fiber.return = null;
1128 + fiber.sibling = null;
1129 fiber.stateNode = null;
1130 + fiber.updateQueue = null;
1131 + if (__DEV__) {
1132 + fiber._debugOwner = null;
1133 + }
1134 }
1135
1136 function emptyPortalContainer(current: Fiber) {
packages/react-reconciler/src/ReactFiberScope.new.js
+2 -15
@@ -17,24 +17,11 @@ import type {
17 } from 'shared/ReactTypes';
18
19 import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
20 +import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
21
21 -import {
22 - HostComponent,
23 - SuspenseComponent,
24 - ScopeComponent,
25 - ContextProvider,
26 -} from './ReactWorkTags';
22 +import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
23 import {enableScopeAPI} from 'shared/ReactFeatureFlags';
24
29 -function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
30 - const memoizedState = fiber.memoizedState;
31 - return (
32 - fiber.tag === SuspenseComponent &&
33 - memoizedState !== null &&
34 - memoizedState.dehydrated === null
35 - );
36 -}
37 -
25 function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
26 return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
27 }
packages/react-reconciler/src/ReactFiberScope.old.js
+2 -15
@@ -17,24 +17,11 @@ import type {
17 } from 'shared/ReactTypes';
18
19 import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
20 +import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
21
21 -import {
22 - HostComponent,
23 - SuspenseComponent,
24 - ScopeComponent,
25 - ContextProvider,
26 -} from './ReactWorkTags';
22 +import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
23 import {enableScopeAPI} from 'shared/ReactFeatureFlags';
24
29 -function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
30 - const memoizedState = fiber.memoizedState;
31 - return (
32 - fiber.tag === SuspenseComponent &&
33 - memoizedState !== null &&
34 - memoizedState.dehydrated === null
35 - );
36 -}
37 -
25 function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
26 return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
27 }
packages/react-reconciler/src/ReactFiberTreeReflection.js
+23 -1
@@ -25,7 +25,7 @@ import {
25 FundamentalComponent,
26 SuspenseComponent,
27 } from './ReactWorkTags';
28 -import {NoEffect, Placement, Hydrating} from './ReactSideEffectTags';
28 +import {NoEffect, Placement, Hydrating, Deletion} from './ReactSideEffectTags';
29 import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
30
31 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -332,3 +332,25 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
332 // eslint-disable-next-line no-unreachable
333 return null;
334 }
335 +
336 +export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
337 + const memoizedState = fiber.memoizedState;
338 + return (
339 + fiber.tag === SuspenseComponent &&
340 + memoizedState !== null &&
341 + memoizedState.dehydrated === null
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 + while (node !== null) {
350 + if (node.effectTag & Deletion || isFiberSuspenseAndTimedOut(node)) {
351 + return true;
352 + }
353 + node = node.return;
354 + }
355 + return false;
356 +}