From 5e464546af4a32c5ae5e704f68bcf4b98a4c440a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 2 Apr 2020 18:26:56 +0100 Subject: [PATCH] ReactDOM.useEvent: fix scope propagation issue (#18464) --- packages/legacy-events/EventPluginUtils.js | 13 +-- .../legacy-events/ReactSyntheticEventType.js | 5 +- packages/legacy-events/SyntheticEvent.js | 2 + .../src/events/DOMModernPluginEventSystem.js | 48 +++++++- .../react-dom/src/events/EventSystemFlags.js | 20 ++++ ...OMModernPluginEventSystem-test.internal.js | 106 ++++++++++++++++++ .../events/accumulateEnterLeaveListeners.js | 25 +++-- .../events/accumulateEventTargetListeners.js | 17 ++- .../src/events/accumulateTwoPhaseListeners.js | 42 ++++--- 9 files changed, 235 insertions(+), 43 deletions(-) create mode 100644 packages/react-dom/src/events/EventSystemFlags.js diff --git a/packages/legacy-events/EventPluginUtils.js b/packages/legacy-events/EventPluginUtils.js index cd99330693..abba8e9a3d 100644 --- a/packages/legacy-events/EventPluginUtils.js +++ b/packages/legacy-events/EventPluginUtils.js @@ -64,8 +64,7 @@ if (__DEV__) { */ export function executeDispatch(event, listener, inst) { const type = event.type || 'unknown-event'; - event.currentTarget = - inst.tag !== undefined ? getNodeFromInstance(inst) : inst; + event.currentTarget = getNodeFromInstance(inst); invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); event.currentTarget = null; } @@ -80,20 +79,12 @@ export function executeDispatchesInOrder(event) { validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { - let previousInstance; for (let i = 0; i < dispatchListeners.length; i++) { - const instance = dispatchInstances[i]; - // We check if the instance was the same as the last one, - // if it was, then we're still on the same instance thus - // propagation should not stop. If we add support for - // stopImmediatePropagation at some point, then we'll - // need to handle that case here differently. - if (instance !== previousInstance && event.isPropagationStopped()) { + if (event.isPropagationStopped()) { break; } // Listeners and Instances are two parallel arrays that are always in sync. executeDispatch(event, dispatchListeners[i], dispatchInstances[i]); - previousInstance = instance; } } else if (dispatchListeners) { executeDispatch(event, dispatchListeners, dispatchInstances); diff --git a/packages/legacy-events/ReactSyntheticEventType.js b/packages/legacy-events/ReactSyntheticEventType.js index 6e61c2338f..7a1ed5cebd 100644 --- a/packages/legacy-events/ReactSyntheticEventType.js +++ b/packages/legacy-events/ReactSyntheticEventType.js @@ -40,8 +40,11 @@ export type ReactSyntheticEvent = {| nativeEventTarget: EventTarget, ) => ReactSyntheticEvent, isPersistent: () => boolean, - _dispatchInstances: null | Array | Fiber | EventTarget, + isPropagationStopped: () => boolean, + _dispatchInstances: null | Array | Fiber, _dispatchListeners: null | Array | Function, + _dispatchCurrentTargets: null | Array, _targetInst: Fiber, type: string, + currentTarget: null | EventTarget, |}; diff --git a/packages/legacy-events/SyntheticEvent.js b/packages/legacy-events/SyntheticEvent.js index c763ce4520..c04f81685d 100644 --- a/packages/legacy-events/SyntheticEvent.js +++ b/packages/legacy-events/SyntheticEvent.js @@ -78,6 +78,7 @@ function SyntheticEvent( this.nativeEvent = nativeEvent; this._dispatchListeners = null; this._dispatchInstances = null; + this._dispatchCurrentTargets = null; const Interface = this.constructor.Interface; for (const propName in Interface) { @@ -187,6 +188,7 @@ Object.assign(SyntheticEvent.prototype, { this.isPropagationStopped = functionThatReturnsFalse; this._dispatchListeners = null; this._dispatchInstances = null; + this._dispatchCurrentTargets = null; if (__DEV__) { Object.defineProperty( this, diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index f5419c5a13..6ad5f5e43a 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -25,7 +25,6 @@ import type {ReactDOMListener} from '../shared/ReactDOMTypes'; import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; -import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils'; import {plugins} from 'legacy-events/EventPluginRegistry'; import { PLUGIN_EVENT_SYSTEM, @@ -92,6 +91,7 @@ import { enableLegacyFBSupport, enableUseEventAPI, } from 'shared/ReactFeatureFlags'; +import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; const capturePhaseEvents = new Set([ TOP_FOCUS, @@ -167,6 +167,52 @@ export const reactScopeListenerStore: WeakMap< >, > = new PossiblyWeakMap(); +function executeDispatch( + event: ReactSyntheticEvent, + listener: Function, + currentTarget: EventTarget, +): void { + const type = event.type || 'unknown-event'; + event.currentTarget = currentTarget; + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); + event.currentTarget = null; +} + +function executeDispatchesInOrder(event: ReactSyntheticEvent): void { + // TODO we should remove _dispatchListeners and _dispatchInstances at some point. + const dispatchListeners = event._dispatchListeners; + const dispatchInstances = event._dispatchInstances; + const dispatchCurrentTargets = event._dispatchCurrentTargets; + let previousInstance; + + if ( + dispatchListeners !== null && + dispatchInstances !== null && + dispatchCurrentTargets !== null + ) { + for (let i = 0; i < dispatchListeners.length; i++) { + const instance = dispatchInstances[i]; + const listener = dispatchListeners[i]; + const currentTarget = dispatchCurrentTargets[i]; + + // We check if the instance was the same as the last one, + // if it was, then we're still on the same instance thus + // propagation should not stop. If we add support for + // stopImmediatePropagation at some point, then we'll + // need to handle that case here differently. + if (instance !== previousInstance && event.isPropagationStopped()) { + break; + } + // Listeners and Instances are two parallel arrays that are always in sync. + executeDispatch(event, listener, currentTarget); + previousInstance = instance; + } + } + event._dispatchListeners = null; + event._dispatchInstances = null; + event._dispatchCurrentTargets = null; +} + function dispatchEventsForPlugins( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, diff --git a/packages/react-dom/src/events/EventSystemFlags.js b/packages/react-dom/src/events/EventSystemFlags.js new file mode 100644 index 0000000000..e31af6bae3 --- /dev/null +++ b/packages/react-dom/src/events/EventSystemFlags.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type EventSystemFlags = number; + +export const PLUGIN_EVENT_SYSTEM = 1; +export const RESPONDER_EVENT_SYSTEM = 1 << 1; +export const IS_PASSIVE = 1 << 2; +export const IS_ACTIVE = 1 << 3; +export const PASSIVE_NOT_SUPPORTED = 1 << 4; +export const IS_REPLAYED = 1 << 5; +export const IS_FIRST_ANCESTOR = 1 << 6; +export const IS_TARGET_EVENT_ONLY = 1 << 7; +export const LEGACY_FB_SUPPORT = 1 << 8; diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js index 7da41d35ff..f88e0d9e85 100644 --- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -2512,6 +2512,112 @@ describe('DOMModernPluginEventSystem', () => { //