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