Skip to content

Commit bf55ea7

Browse files
authored
Move beforeblur phase to prepareForCommit (facebook#18609)
1 parent 843b50c commit bf55ea7

File tree

7 files changed

+98
-86
lines changed

7 files changed

+98
-86
lines changed

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import {
7373
enableUseEventAPI,
7474
enableScopeAPI,
7575
} from 'shared/ReactFeatureFlags';
76-
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
7776
import {
7877
RESPONDER_EVENT_SYSTEM,
7978
IS_PASSIVE,
@@ -95,6 +94,10 @@ import {
9594
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
9695
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
9796

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+
98101
export type ReactListenerEvent = ReactDOMListenerEvent;
99102
export type ReactListenerMap = ReactDOMListenerMap;
100103
export type ReactListener = ReactDOMListener;
@@ -250,6 +253,15 @@ export function getPublicInstance(instance: Instance): * {
250253
export function prepareForCommit(containerInfo: Container): void {
251254
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
252255
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+
}
253265
ReactBrowserEventEmitterSetEnabled(false);
254266
}
255267

@@ -532,18 +544,11 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
532544
);
533545
}
534546
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);
547552
}
548553
}
549554

@@ -571,20 +576,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
571576
}
572577
}
573578

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.
578579
export function beforeRemoveInstance(
579580
instance: Instance | TextInstance | SuspenseInstance,
580581
): void {
581-
if (
582-
(enableDeprecatedFlareAPI || enableUseEventAPI) &&
583-
selectionInformation &&
584-
instance === selectionInformation.focusedElem
585-
) {
586-
dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
587-
}
588582
if (enableUseEventAPI) {
589583
// It's unfortunate that we have to do this cleanup, but
590584
// it's necessary otherwise we will leak the host instances
@@ -674,28 +668,7 @@ export function clearSuspenseBoundaryFromContainer(
674668
retryIfBlockedOn(container);
675669
}
676670

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-
688671
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-
}
699672
// TODO: Does this work for all element types? What about MathML? Should we
700673
// pass host context to this method?
701674
instance = ((instance: any): HTMLElement);

packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const initializeModules = hasPointerEvents => {
2323
jest.resetModules();
2424
ReactFeatureFlags = require('shared/ReactFeatureFlags');
2525
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
26+
ReactFeatureFlags.enableScopeAPI = true;
2627
React = require('react');
2728
ReactDOM = require('react-dom');
2829
Scheduler = require('scheduler');
@@ -364,6 +365,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
364365
);
365366
});
366367

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+
367402
// @gate experimental
368403
it('is called after a focused suspended element is hidden', () => {
369404
const Suspense = React.Suspense;

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
11161116
// get GC:ed but we don't know which for sure which parent is the current
11171117
// one so we'll settle for GC:ing the subtree of this child. This child
11181118
// itself will be GC:ed when the parent updates the next time.
1119-
fiber.return = null;
1119+
fiber.alternate = null;
11201120
fiber.child = null;
1121-
fiber.memoizedState = null;
1122-
fiber.updateQueue = null;
11231121
fiber.dependencies = null;
1124-
fiber.alternate = null;
11251122
fiber.firstEffect = null;
11261123
fiber.lastEffect = null;
1127-
fiber.pendingProps = null;
11281124
fiber.memoizedProps = null;
1125+
fiber.memoizedState = null;
1126+
fiber.pendingProps = null;
1127+
fiber.return = null;
1128+
fiber.sibling = null;
11291129
fiber.stateNode = null;
1130+
fiber.updateQueue = null;
1131+
if (__DEV__) {
1132+
fiber._debugOwner = null;
1133+
}
11301134
}
11311135

11321136
function emptyPortalContainer(current: Fiber) {

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
11161116
// get GC:ed but we don't know which for sure which parent is the current
11171117
// one so we'll settle for GC:ing the subtree of this child. This child
11181118
// itself will be GC:ed when the parent updates the next time.
1119-
fiber.return = null;
1119+
fiber.alternate = null;
11201120
fiber.child = null;
1121-
fiber.memoizedState = null;
1122-
fiber.updateQueue = null;
11231121
fiber.dependencies = null;
1124-
fiber.alternate = null;
11251122
fiber.firstEffect = null;
11261123
fiber.lastEffect = null;
1127-
fiber.pendingProps = null;
11281124
fiber.memoizedProps = null;
1125+
fiber.memoizedState = null;
1126+
fiber.pendingProps = null;
1127+
fiber.return = null;
1128+
fiber.sibling = null;
11291129
fiber.stateNode = null;
1130+
fiber.updateQueue = null;
1131+
if (__DEV__) {
1132+
fiber._debugOwner = null;
1133+
}
11301134
}
11311135

11321136
function emptyPortalContainer(current: Fiber) {

packages/react-reconciler/src/ReactFiberScope.new.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,11 @@ import type {
1717
} from 'shared/ReactTypes';
1818

1919
import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
20+
import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
2021

21-
import {
22-
HostComponent,
23-
SuspenseComponent,
24-
ScopeComponent,
25-
ContextProvider,
26-
} from './ReactWorkTags';
22+
import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
2723
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
2824

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-
3825
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
3926
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
4027
}

packages/react-reconciler/src/ReactFiberScope.old.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,11 @@ import type {
1717
} from 'shared/ReactTypes';
1818

1919
import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
20+
import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
2021

21-
import {
22-
HostComponent,
23-
SuspenseComponent,
24-
ScopeComponent,
25-
ContextProvider,
26-
} from './ReactWorkTags';
22+
import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
2723
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
2824

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-
3825
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
3926
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
4027
}

packages/react-reconciler/src/ReactFiberTreeReflection.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
FundamentalComponent,
2626
SuspenseComponent,
2727
} from './ReactWorkTags';
28-
import {NoEffect, Placement, Hydrating} from './ReactSideEffectTags';
28+
import {NoEffect, Placement, Hydrating, Deletion} from './ReactSideEffectTags';
2929
import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
3030

3131
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -332,3 +332,25 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
332332
// eslint-disable-next-line no-unreachable
333333
return null;
334334
}
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+
}

0 commit comments

Comments
 (0)