Skip to content

Move beforeblur phase to prepareForCommit #18609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 18 additions & 45 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ import {
enableUseEventAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
Expand All @@ -95,6 +94,10 @@ import {
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';

// TODO: This is an exposed internal, we should move this around
// so this isn't the case.
import {isFiberInsideHiddenOrRemovedTree} from 'react-reconciler/src/ReactFiberTreeReflection';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @acdlite in case this affects your fork. I don't think it will.


export type ReactListenerEvent = ReactDOMListenerEvent;
export type ReactListenerMap = ReactDOMListenerMap;
export type ReactListener = ReactDOMListener;
Expand Down Expand Up @@ -250,6 +253,15 @@ export function getPublicInstance(instance: Instance): * {
export function prepareForCommit(containerInfo: Container): void {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation();
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null) {
const instance = getClosestInstanceFromNode(focusedElem);
if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
dispatchBeforeDetachedBlur(focusedElem);
}
}
}
ReactBrowserEventEmitterSetEnabled(false);
}

Expand Down Expand Up @@ -532,18 +544,11 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
);
}
if (enableUseEventAPI) {
try {
// We need to temporarily enable the event system
// to dispatch the "beforeblur" event.
ReactBrowserEventEmitterSetEnabled(true);
const event = createEvent(TOP_BEFORE_BLUR);
// Dispatch "beforeblur" directly on the target,
// so it gets picked up by the event system and
// can propagate through the React internal tree.
target.dispatchEvent(event);
} finally {
ReactBrowserEventEmitterSetEnabled(false);
}
const event = createEvent(TOP_BEFORE_BLUR);
// Dispatch "beforeblur" directly on the target,
// so it gets picked up by the event system and
// can propagate through the React internal tree.
target.dispatchEvent(event);
}
}

Expand Down Expand Up @@ -571,20 +576,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
}
}

// This is a specific event for the React Flare
// event system, so event responders can act
// accordingly to a DOM node being unmounted that
// previously had active document focus.
export function beforeRemoveInstance(
instance: Instance | TextInstance | SuspenseInstance,
): void {
if (
(enableDeprecatedFlareAPI || enableUseEventAPI) &&
selectionInformation &&
instance === selectionInformation.focusedElem
) {
dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
}
if (enableUseEventAPI) {
// It's unfortunate that we have to do this cleanup, but
// it's necessary otherwise we will leak the host instances
Expand Down Expand Up @@ -674,28 +668,7 @@ export function clearSuspenseBoundaryFromContainer(
retryIfBlockedOn(container);
}

function instanceContainsElem(instance: Instance, element: HTMLElement) {
let fiber = getClosestInstanceFromNode(element);
while (fiber !== null) {
if (fiber.tag === HostComponent && fiber.stateNode === instance) {
return true;
}
fiber = fiber.return;
}
return false;
}

export function hideInstance(instance: Instance): void {
// Ensure we trigger `onBeforeBlur` if the active focused elment
// is ether the instance of a child or the instance. We need
// to traverse the Fiber tree here rather than use node.contains()
// as the child node might be inside a Portal.
if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
}
}
// TODO: Does this work for all element types? What about MathML? Should we
// pass host context to this method?
instance = ((instance: any): HTMLElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const initializeModules = hasPointerEvents => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
ReactFeatureFlags.enableScopeAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
Expand Down Expand Up @@ -364,6 +365,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
);
});

// @gate experimental
it('is called after a nested focused element is unmounted (with scope query)', () => {
const TestScope = React.unstable_createScope();
const testScopeQuery = (type, props) => true;
let targetNodes;
let targetNode;

const Component = ({show}) => {
const scopeRef = React.useRef(null);
const listener = useFocusWithin({
onBeforeBlurWithin(event) {
const scope = scopeRef.current;
targetNode = innerRef.current;
targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
},
});

return (
<TestScope ref={scopeRef} DEPRECATED_flareListeners={[listener]}>
{show && <input ref={innerRef} />}
</TestScope>
);
};

ReactDOM.render(<Component show={true} />, container);

const inner = innerRef.current;
const target = createEventTarget(inner);
target.keydown({key: 'Tab'});
target.focus();
ReactDOM.render(<Component show={false} />, container);
expect(targetNodes).toEqual([targetNode]);
});

// @gate experimental
it('is called after a focused suspended element is hidden', () => {
const Suspense = React.Suspense;
Expand Down
14 changes: 9 additions & 5 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
fiber.return = null;
fiber.alternate = null;
fiber.child = null;
fiber.memoizedState = null;
fiber.updateQueue = null;
fiber.dependencies = null;
fiber.alternate = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
fiber.pendingProps = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.return = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;
if (__DEV__) {
fiber._debugOwner = null;
}
}

function emptyPortalContainer(current: Fiber) {
Expand Down
14 changes: 9 additions & 5 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -1116,17 +1116,21 @@ function detachFiber(fiber: Fiber) {
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
fiber.return = null;
fiber.alternate = null;
fiber.child = null;
fiber.memoizedState = null;
fiber.updateQueue = null;
fiber.dependencies = null;
fiber.alternate = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
fiber.pendingProps = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.return = null;
fiber.sibling = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a nice change 👍

fiber.stateNode = null;
fiber.updateQueue = null;
if (__DEV__) {
fiber._debugOwner = null;
}
}

function emptyPortalContainer(current: Fiber) {
Expand Down
17 changes: 2 additions & 15 deletions packages/react-reconciler/src/ReactFiberScope.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,11 @@ import type {
} from 'shared/ReactTypes';

import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';

import {
HostComponent,
SuspenseComponent,
ScopeComponent,
ContextProvider,
} from './ReactWorkTags';
import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';

function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
const memoizedState = fiber.memoizedState;
return (
fiber.tag === SuspenseComponent &&
memoizedState !== null &&
memoizedState.dehydrated === null
);
}

function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
Expand Down
17 changes: 2 additions & 15 deletions packages/react-reconciler/src/ReactFiberScope.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,11 @@ import type {
} from 'shared/ReactTypes';

import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';

import {
HostComponent,
SuspenseComponent,
ScopeComponent,
ContextProvider,
} from './ReactWorkTags';
import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';

function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
const memoizedState = fiber.memoizedState;
return (
fiber.tag === SuspenseComponent &&
memoizedState !== null &&
memoizedState.dehydrated === null
);
}

function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
Expand Down
24 changes: 23 additions & 1 deletion packages/react-reconciler/src/ReactFiberTreeReflection.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
FundamentalComponent,
SuspenseComponent,
} from './ReactWorkTags';
import {NoEffect, Placement, Hydrating} from './ReactSideEffectTags';
import {NoEffect, Placement, Hydrating, Deletion} from './ReactSideEffectTags';
import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
Expand Down Expand Up @@ -332,3 +332,25 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
// eslint-disable-next-line no-unreachable
return null;
}

export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
const memoizedState = fiber.memoizedState;
return (
fiber.tag === SuspenseComponent &&
memoizedState !== null &&
memoizedState.dehydrated === null
);
}

// This is only safe to call in the commit phase when the return tree is consistent.
// It should not be used anywhere else. See PR #18609 for details.
export function isFiberInsideHiddenOrRemovedTree(fiber: Fiber): boolean {
let node = fiber;
while (node !== null) {
if (node.effectTag & Deletion || isFiberSuspenseAndTimedOut(node)) {
return true;
}
node = node.return;
}
return false;
}