From 858608b8a762eb128696b15297bce2b81f1c189e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 6 Mar 2019 12:40:53 +0000 Subject: [PATCH 01/19] Add infrastructure for passive/non-passive event support for future event API experimentation --- packages/events/EventPluginHub.js | 4 + packages/events/PluginModuleType.js | 1 + packages/events/ReactGenericBatching.js | 8 +- .../ReactBrowserEventEmitter-test.internal.js | 8 +- .../react-dom/src/client/ReactDOMComponent.js | 51 ++++--- .../react-dom/src/events/EventListener.js | 24 ++- .../src/events/ReactBrowserEventEmitter.js | 140 +++++++++++------- .../src/events/ReactDOMEventListener.js | 91 ++++++------ .../react-dom/src/events/SelectEventPlugin.js | 2 +- .../src/events/forks/EventListener-www.js | 14 +- .../src/test-utils/ReactTestUtils.js | 2 +- .../src/ReactFabricEventEmitter.js | 1 + .../src/ReactNativeEventEmitter.js | 1 + .../src/ReactFiberScheduler.js | 11 +- scripts/flow/environment.js | 14 +- 15 files changed, 228 insertions(+), 144 deletions(-) diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index e639110713481..3a078b19558fa 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -163,6 +163,7 @@ function extractEvents( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, + passive: null | boolean, ): Array | ReactSyntheticEvent | null { let events = null; for (let i = 0; i < plugins.length; i++) { @@ -174,6 +175,7 @@ function extractEvents( targetInst, nativeEvent, nativeEventTarget, + passive, ); if (extractedEvents) { events = accumulateInto(events, extractedEvents); @@ -214,12 +216,14 @@ export function runExtractedEventsInBatch( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, + passive: null | boolean, ) { const events = extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, + passive, ); runEventsInBatch(events); } diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index cd7a07661ab4e..9f6ac6ce98c68 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -27,6 +27,7 @@ export type PluginModule = { targetInst: null | Fiber, nativeTarget: NativeEvent, nativeEventTarget: EventTarget, + passive?: null | boolean, ) => ?ReactSyntheticEvent, tapMoveThreshold?: number, }; diff --git a/packages/events/ReactGenericBatching.js b/packages/events/ReactGenericBatching.js index 8347382f48a53..78aff1d551a14 100644 --- a/packages/events/ReactGenericBatching.js +++ b/packages/events/ReactGenericBatching.js @@ -20,8 +20,8 @@ import { let _batchedUpdatesImpl = function(fn, bookkeeping) { return fn(bookkeeping); }; -let _interactiveUpdatesImpl = function(fn, a, b) { - return fn(a, b); +let _interactiveUpdatesImpl = function(fn, a, b, c) { + return fn(a, b, c); }; let _flushInteractiveUpdatesImpl = function() {}; @@ -52,8 +52,8 @@ export function batchedUpdates(fn, bookkeeping) { } } -export function interactiveUpdates(fn, a, b) { - return _interactiveUpdatesImpl(fn, a, b); +export function interactiveUpdates(fn, a, b, c) { + return _interactiveUpdatesImpl(fn, a, b, c); } export function flushInteractiveUpdates() { diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 2a6b526218c48..023a4435513c5 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -331,15 +331,15 @@ describe('ReactBrowserEventEmitter', () => { it('should listen to events only once', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1); }); it('should work with event plugins without dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( 'click', @@ -349,7 +349,7 @@ describe('ReactBrowserEventEmitter', () => { it('should work with event plugins with dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); + ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document, true); const setEventListeners = []; const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs(); diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 50f21ccf47d57..4a4e444a58bc5 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -253,14 +253,17 @@ if (__DEV__) { }; } -function ensureListeningTo(rootContainerElement, registrationName) { +function ensureListeningTo( + rootContainerElement: Element | Node, + registrationName: string, +): void { const isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; - listenTo(registrationName, doc); + listenTo(registrationName, doc, true /* isLegacy */); } function getOwnerDocumentFromRootContainer( @@ -494,41 +497,41 @@ export function setInitialProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement); + trapBubbledEvent(TOP_LOAD, domElement, true); props = rawProps; break; case 'video': case 'audio': // Create listener for each media event for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + trapBubbledEvent(mediaEventTypes[i], domElement, true); } props = rawProps; break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_ERROR, domElement, true); props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + trapBubbledEvent(TOP_ERROR, domElement, true); + trapBubbledEvent(TOP_LOAD, domElement, true); props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + trapBubbledEvent(TOP_RESET, domElement, true); + trapBubbledEvent(TOP_SUBMIT, domElement, true); props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + trapBubbledEvent(TOP_TOGGLE, domElement, true); props = rawProps; break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); props = ReactDOMInputGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -540,7 +543,7 @@ export function setInitialProperties( case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); props = ReactDOMSelectGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -548,7 +551,7 @@ export function setInitialProperties( case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); props = ReactDOMTextareaGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -888,34 +891,34 @@ export function diffHydratedProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement); + trapBubbledEvent(TOP_LOAD, domElement, true); break; case 'video': case 'audio': // Create listener for each media event for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + trapBubbledEvent(mediaEventTypes[i], domElement, true); } break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_ERROR, domElement, true); break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + trapBubbledEvent(TOP_ERROR, domElement, true); + trapBubbledEvent(TOP_LOAD, domElement, true); break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + trapBubbledEvent(TOP_RESET, domElement, true); + trapBubbledEvent(TOP_SUBMIT, domElement, true); break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + trapBubbledEvent(TOP_TOGGLE, domElement, true); break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -925,14 +928,14 @@ export function diffHydratedProperties( break; case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapBubbledEvent(TOP_INVALID, domElement, true); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 67b9bcadd868b..3cf2a860ac799 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -8,17 +8,33 @@ */ export function addEventBubbleListener( - element: Document | Element, + element: Document | Element | Node, eventType: string, listener: Function, + isPassive: boolean | null, ): void { - element.addEventListener(eventType, listener, false); + if (isPassive === null) { + element.addEventListener(eventType, listener, false); + } else { + element.addEventListener(eventType, listener, { + passive: isPassive, + capture: false, + }); + } } export function addEventCaptureListener( - element: Document | Element, + element: Document | Element | Node, eventType: string, listener: Function, + isPassive: boolean | null, ): void { - element.addEventListener(eventType, listener, true); + if (isPassive === null) { + element.addEventListener(eventType, listener, true); + } else { + element.addEventListener(eventType, listener, { + passive: isPassive, + capture: true, + }); + } } diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index ea02bdc4a6aa2..83d575534a8b9 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -8,6 +8,7 @@ */ import {registrationNameDependencies} from 'events/EventPluginRegistry'; +import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; import { TOP_BLUR, TOP_CANCEL, @@ -84,22 +85,39 @@ import isEventSupported from './isEventSupported'; * React Core . General Purpose Event Plugin System */ -const alreadyListeningTo = {}; -let reactTopListenersCounter = 0; +const elementListeningObjects: WeakMap< + Document | Element | Node, + ElementListeningObject, +> = new WeakMap(); -/** - * To ensure no conflicts with other potential React instances on the page - */ -const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2); +// We store legacy events we listen where we don't use +// passive/non-passive options on the event listener. +// For event listeners that are passive/non-passive, we store +// them in nonLegacy so they do not conflict. +export type ElementListeningObject = { + legacy: Set, + nonLegacy: Set, +}; -function getListeningForDocument(mountAt: any) { - // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty` - // directly. - if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) { - mountAt[topListenersIDKey] = reactTopListenersCounter++; - alreadyListeningTo[mountAt[topListenersIDKey]] = {}; +function createElementListeningObject(): ElementListeningObject { + return { + legacy: new Set(), + nonLegacy: new Set(), + }; +} + +function getListeningSetForElement( + element: Document | Element | Node, + isLegacy: boolean, +): Set { + if (!elementListeningObjects.has(element)) { + elementListeningObjects.set(element, createElementListeningObject()); } - return alreadyListeningTo[mountAt[topListenersIDKey]]; + const listeningObject = ((elementListeningObjects.get( + element, + ): any): ElementListeningObject); + const listeningKey = isLegacy ? 'legacy' : 'nonLegacy'; + return listeningObject[listeningKey]; } /** @@ -125,62 +143,74 @@ function getListeningForDocument(mountAt: any) { */ export function listenTo( registrationName: string, - mountAt: Document | Element, -) { - const isListening = getListeningForDocument(mountAt); + mountAt: Document | Element | Node, + isLegacy: boolean, +): void { + const listeningSet = getListeningSetForElement(mountAt, isLegacy); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; - if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { - switch (dependency) { - case TOP_SCROLL: - trapCapturedEvent(TOP_SCROLL, mountAt); - break; - case TOP_FOCUS: - case TOP_BLUR: - trapCapturedEvent(TOP_FOCUS, mountAt); - trapCapturedEvent(TOP_BLUR, mountAt); - // We set the flag for a single dependency later in this function, - // but this ensures we mark both as attached rather than just one. - isListening[TOP_BLUR] = true; - isListening[TOP_FOCUS] = true; - break; - case TOP_CANCEL: - case TOP_CLOSE: - if (isEventSupported(getRawEventName(dependency))) { - trapCapturedEvent(dependency, mountAt); - } - break; - case TOP_INVALID: - case TOP_SUBMIT: - case TOP_RESET: - // We listen to them on the target DOM elements. - // Some of them bubble so we don't want them to fire twice. - break; - default: - // By default, listen on the top level to all non-media events. - // Media events don't bubble so adding the listener wouldn't do anything. - const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; - if (!isMediaEvent) { - trapBubbledEvent(dependency, mountAt); - } - break; - } - isListening[dependency] = true; + listenToDependency(dependency, listeningSet, mountAt, isLegacy); + } +} + +function listenToDependency( + dependency: DOMTopLevelEventType, + listeningSet: Set, + mountAt: Document | Element | Node, + isLegacy: boolean, +): void { + if (!listeningSet.has(dependency)) { + switch (dependency) { + case TOP_SCROLL: + trapCapturedEvent(TOP_SCROLL, mountAt, isLegacy); + break; + case TOP_FOCUS: + case TOP_BLUR: + trapCapturedEvent(TOP_FOCUS, mountAt, isLegacy); + trapCapturedEvent(TOP_BLUR, mountAt, isLegacy); + // We set the flag for a single dependency later in this function, + // but this ensures we mark both as attached rather than just one. + listeningSet.add(TOP_BLUR); + listeningSet.add(TOP_FOCUS); + break; + case TOP_CANCEL: + case TOP_CLOSE: + if (isEventSupported(getRawEventName(dependency))) { + trapCapturedEvent(dependency, mountAt, isLegacy); + } + break; + case TOP_INVALID: + case TOP_SUBMIT: + case TOP_RESET: + // We listen to them on the target DOM elements. + // Some of them bubble so we don't want them to fire twice. + break; + default: + // By default, listen on the top level to all non-media events. + // Media events don't bubble so adding the listener wouldn't do anything. + const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; + if (!isMediaEvent) { + trapBubbledEvent(dependency, mountAt, isLegacy); + } + break; } + listeningSet.add(dependency); } } export function isListeningToAllDependencies( registrationName: string, mountAt: Document | Element, -) { - const isListening = getListeningForDocument(mountAt); + isLegacy: boolean, +): boolean { + const listeningSet = getListeningSetForElement(mountAt, isLegacy); const dependencies = registrationNameDependencies[registrationName]; + for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; - if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { + if (!listeningSet.has(dependency)) { return false; } } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index e49266d267f70..bd6c9bfbc240d 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -48,20 +48,23 @@ function findRootContainerNode(inst) { // Used to store ancestor hierarchy in top level callback function getTopLevelCallbackBookKeeping( - topLevelType, - nativeEvent, - targetInst, + topLevelType: ?DOMTopLevelEventType, + nativeEvent: ?AnyNativeEvent, + targetInst: Fiber | null, + passive: null | boolean, ): { topLevelType: ?DOMTopLevelEventType, nativeEvent: ?AnyNativeEvent, targetInst: Fiber | null, ancestors: Array, + passive: null | boolean, } { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; instance.nativeEvent = nativeEvent; instance.targetInst = targetInst; + instance.passive = passive; return instance; } return { @@ -69,6 +72,7 @@ function getTopLevelCallbackBookKeeping( nativeEvent, targetInst, ancestors: [], + passive, }; } @@ -77,6 +81,7 @@ function releaseTopLevelCallbackBookKeeping(instance) { instance.nativeEvent = null; instance.targetInst = null; instance.ancestors.length = 0; + instance.passive = null; if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { callbackBookkeepingPool.push(instance); } @@ -110,6 +115,7 @@ function handleTopLevel(bookKeeping) { targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent), + bookKeeping.passive, ); } } @@ -125,70 +131,66 @@ export function isEnabled() { return _enabled; } -/** - * Traps top-level events by using event bubbling. - * - * @param {number} topLevelType Number from `TopLevelEventTypes`. - * @param {object} element Element on which to attach listener. - * @return {?object} An object with a remove function which will forcefully - * remove the listener. - * @internal - */ export function trapBubbledEvent( topLevelType: DOMTopLevelEventType, - element: Document | Element, -) { - if (!element) { - return null; - } + element: Document | Element | Node, + isLegacy: boolean, +): void { const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; + const rawEventName = getRawEventName(topLevelType); - addEventBubbleListener( - element, - getRawEventName(topLevelType), + if (isLegacy) { // Check if interactive and wrap in interactiveUpdates - dispatch.bind(null, topLevelType), - ); + const listener = dispatch.bind(null, topLevelType, null); + // We don't listen for passive/non-passive + addEventBubbleListener(element, rawEventName, listener, null); + } else { + // Check if interactive and wrap in interactiveUpdates + const passiveListener = dispatch.bind(null, topLevelType, true); + const activeListener = dispatch.bind(null, topLevelType, false); + // We listen to the same event for both passive/non-passive + addEventBubbleListener(element, rawEventName, passiveListener, true); + addEventBubbleListener(element, rawEventName, activeListener, false); + } } -/** - * Traps a top-level event by using event capturing. - * - * @param {number} topLevelType Number from `TopLevelEventTypes`. - * @param {object} element Element on which to attach listener. - * @return {?object} An object with a remove function which will forcefully - * remove the listener. - * @internal - */ export function trapCapturedEvent( topLevelType: DOMTopLevelEventType, - element: Document | Element, -) { - if (!element) { - return null; - } + element: Document | Element | Node, + isLegacy: boolean, +): void { const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; + const rawEventName = getRawEventName(topLevelType); - addEventCaptureListener( - element, - getRawEventName(topLevelType), + if (isLegacy) { // Check if interactive and wrap in interactiveUpdates - dispatch.bind(null, topLevelType), - ); + const listener = dispatch.bind(null, topLevelType, null); + // We don't listen for passive/non-passive + addEventCaptureListener(element, rawEventName, listener, null); + } else { + // Check if interactive and wrap in interactiveUpdates + const passiveListener = dispatch.bind(null, topLevelType, true); + const activeListener = dispatch.bind(null, topLevelType, false); + // We listen to the same event for both passive/non-passive + addEventCaptureListener(element, rawEventName, passiveListener, true); + addEventCaptureListener(element, rawEventName, activeListener, false); + } } -function dispatchInteractiveEvent(topLevelType, nativeEvent) { - interactiveUpdates(dispatchEvent, topLevelType, nativeEvent); +function dispatchInteractiveEvent(topLevelType, isPassiveEvent, nativeEvent) { + interactiveUpdates(dispatchEvent, topLevelType, isPassiveEvent, nativeEvent); } export function dispatchEvent( topLevelType: DOMTopLevelEventType, + // passive will be `null` for legacy events + passive: null | boolean, nativeEvent: AnyNativeEvent, -) { +): void { if (!_enabled) { return; } @@ -211,6 +213,7 @@ export function dispatchEvent( topLevelType, nativeEvent, targetInst, + passive, ); try { diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index 9d1b7f16dda73..41fc6a4d74a7c 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -169,7 +169,7 @@ const SelectEventPlugin = { const doc = getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. - if (!doc || !isListeningToAllDependencies('onSelect', doc)) { + if (!doc || !isListeningToAllDependencies('onSelect', doc, true)) { return null; } diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 4f70d179810d4..06f4f113d6ce0 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -16,16 +16,26 @@ export function addEventBubbleListener( element: Element, eventType: string, listener: Function, + isPassive: boolean | null, ): void { - EventListenerWWW.listen(element, eventType, listener); + if (isPassive === null) { + EventListenerWWW.listen(element, eventType, listener); + } else { + EventListenerWWW.listen(element, eventType, listener, isPassive); + } } export function addEventCaptureListener( element: Element, eventType: string, listener: Function, + isPassive: boolean | null, ): void { - EventListenerWWW.capture(element, eventType, listener); + if (isPassive === null) { + EventListenerWWW.capture(element, eventType, listener); + } else { + EventListenerWWW.capture(element, eventType, listener, isPassive); + } } // Flow magic to verify the exports of this file match the original version. diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 9793b4c6e3c59..abd4c554d6c87 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -63,7 +63,7 @@ let hasWarnedAboutDeprecatedMockComponent = false; */ function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { fakeNativeEvent.target = node; - dispatchEvent(topLevelType, fakeNativeEvent); + dispatchEvent(topLevelType, null, fakeNativeEvent); } /** diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index 7e016f8ce8c51..0d9c067f61985 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -30,6 +30,7 @@ export function dispatchEvent( targetFiber, nativeEvent, nativeEvent.target, + null, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index a5ec6e298f13d..97c7299f0ef4b 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -100,6 +100,7 @@ function _receiveRootNodeIDEvent( inst, nativeEvent, nativeEvent.target, + null, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index be5062139ab13..4f552c495ae92 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -2522,9 +2522,14 @@ function flushSync(fn: (a: A) => R, a: A): R { } } -function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { +function interactiveUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { if (isBatchingInteractiveUpdates) { - return fn(a, b); + return fn(a, b, c); } // If there are any pending interactive updates, synchronously flush them. // This needs to happen before we read any handlers, because the effect of @@ -2544,7 +2549,7 @@ function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { isBatchingInteractiveUpdates = true; isBatchingUpdates = true; try { - return fn(a, b); + return fn(a, b, c); } finally { isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; isBatchingUpdates = previousIsBatchingUpdates; diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index d34bdb0d69554..407b995b07629 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -31,7 +31,17 @@ declare module 'ReactFiberErrorDialog' { // EventListener www fork declare module 'EventListener' { declare module.exports: { - listen: (target: Element, type: string, callback: Function) => mixed, - capture: (target: Element, type: string, callback: Function) => mixed, + listen: ( + target: Element, + type: string, + callback: Function, + passive?: boolean, + ) => mixed, + capture: ( + target: Element, + type: string, + callback: Function, + passive?: boolean, + ) => mixed, }; } From 0ae3ba3258e5b8f96e843a86dc5e276aa05dc9bb Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 6 Mar 2019 13:24:00 +0000 Subject: [PATCH 02/19] More Flow fixes --- .../src/events/ReactDOMEventListener.js | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index bd6c9bfbc240d..c563332711a6d 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -27,6 +27,14 @@ const {isInteractiveTopLevelEventType} = SimpleEventPlugin; const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; const callbackBookkeepingPool = []; +type BookKeepingInstance = { + topLevelType: DOMTopLevelEventType | null, + nativeEvent: AnyNativeEvent | null, + targetInst: Fiber | null, + ancestors: Array, + passive: null | boolean, +}; + /** * Find the deepest React component completely containing the root of the * passed-in instance (for use when entire React trees are nested within each @@ -48,17 +56,11 @@ function findRootContainerNode(inst) { // Used to store ancestor hierarchy in top level callback function getTopLevelCallbackBookKeeping( - topLevelType: ?DOMTopLevelEventType, - nativeEvent: ?AnyNativeEvent, - targetInst: Fiber | null, - passive: null | boolean, -): { - topLevelType: ?DOMTopLevelEventType, - nativeEvent: ?AnyNativeEvent, + topLevelType: DOMTopLevelEventType, + nativeEvent: AnyNativeEvent, targetInst: Fiber | null, - ancestors: Array, passive: null | boolean, -} { +): BookKeepingInstance { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; @@ -76,7 +78,9 @@ function getTopLevelCallbackBookKeeping( }; } -function releaseTopLevelCallbackBookKeeping(instance) { +function releaseTopLevelCallbackBookKeeping( + instance: BookKeepingInstance, +): void { instance.topLevelType = null; instance.nativeEvent = null; instance.targetInst = null; @@ -87,7 +91,7 @@ function releaseTopLevelCallbackBookKeeping(instance) { } } -function handleTopLevel(bookKeeping) { +function handleTopLevel(bookKeeping: BookKeepingInstance) { let targetInst = bookKeeping.targetInst; // Loop through the hierarchy, in case there's any nested components. @@ -97,7 +101,8 @@ function handleTopLevel(bookKeeping) { let ancestor = targetInst; do { if (!ancestor) { - bookKeeping.ancestors.push(ancestor); + const ancestors = bookKeeping.ancestors; + ((ancestors: any): Array).push(ancestor); break; } const root = findRootContainerNode(ancestor); @@ -111,9 +116,9 @@ function handleTopLevel(bookKeeping) { for (let i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; runExtractedEventsInBatch( - bookKeeping.topLevelType, + ((bookKeeping.topLevelType: any): DOMTopLevelEventType), targetInst, - bookKeeping.nativeEvent, + ((bookKeeping.nativeEvent: any): AnyNativeEvent), getEventTarget(bookKeeping.nativeEvent), bookKeeping.passive, ); From 3bd617618dddd2958a2bdfd2987801c0071a503f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 6 Mar 2019 16:35:13 +0000 Subject: [PATCH 03/19] Add fallback to Map if there is no WeakMap --- packages/react-dom/src/events/ReactBrowserEventEmitter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 83d575534a8b9..051c4e2bbe678 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -85,10 +85,11 @@ import isEventSupported from './isEventSupported'; * React Core . General Purpose Event Plugin System */ -const elementListeningObjects: WeakMap< +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +const elementListeningObjects: WeakMap|Map< Document | Element | Node, ElementListeningObject, -> = new WeakMap(); +> = new PossiblyWeakMap(); // We store legacy events we listen where we don't use // passive/non-passive options on the event listener. From 06f2c3ba319a243770979d17be7e7d903bf4fe8f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 6 Mar 2019 16:57:08 +0000 Subject: [PATCH 04/19] Fix prettier --- .../react-dom/src/events/ReactBrowserEventEmitter.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 051c4e2bbe678..350e78e8e9ba3 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -86,10 +86,12 @@ import isEventSupported from './isEventSupported'; */ const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -const elementListeningObjects: WeakMap|Map< - Document | Element | Node, - ElementListeningObject, -> = new PossiblyWeakMap(); +const elementListeningObjects: + | WeakMap + | Map< + Document | Element | Node, + ElementListeningObject, + > = new PossiblyWeakMap(); // We store legacy events we listen where we don't use // passive/non-passive options on the event listener. From bbae02dea7b2ba52cf7551cd029ce58b8cd08375 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 6 Mar 2019 17:11:49 +0000 Subject: [PATCH 05/19] Update comment --- .../src/events/ReactBrowserEventEmitter.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 350e78e8e9ba3..21d52c6a5a551 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -93,10 +93,15 @@ const elementListeningObjects: ElementListeningObject, > = new PossiblyWeakMap(); -// We store legacy events we listen where we don't use -// passive/non-passive options on the event listener. -// For event listeners that are passive/non-passive, we store -// them in nonLegacy so they do not conflict. +// This object will contain both legacy and non legacy events. +// In the case of legacy events, we register an event listener +// without passing an object with { passive, capture } etc to +// third argument of the addEventListener call (we use a boolean). +// For non legacy events, we register an event listener with +// an object (third argument) with the passive property. We also +// double listen for the event on the element, as we need to check +// both paths (where passive is both true and false) so we can +// handle logic in either case. export type ElementListeningObject = { legacy: Set, nonLegacy: Set, From 68934d852e85891380a1a79d1cc7a65fcd939980 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 08:23:47 +0000 Subject: [PATCH 06/19] Add isLegacy flag to EventPlugins --- packages/events/EventPluginHub.js | 8 ++++++++ packages/events/PluginModuleType.js | 1 + packages/events/ResponderEventPlugin.js | 2 ++ packages/react-dom/src/events/BeforeInputEventPlugin.js | 2 ++ packages/react-dom/src/events/ChangeEventPlugin.js | 2 ++ packages/react-dom/src/events/EnterLeaveEventPlugin.js | 2 ++ .../react-dom/src/events/ReactBrowserEventEmitter.js | 9 ++++----- packages/react-dom/src/events/SelectEventPlugin.js | 2 ++ packages/react-dom/src/events/SimpleEventPlugin.js | 2 ++ .../src/ReactNativeBridgeEventPlugin.js | 2 ++ 10 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index 3a078b19558fa..29f01037df076 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -170,6 +170,14 @@ function extractEvents( // Not every plugin in the ordering may be loaded at runtime. const possiblePlugin: PluginModule = plugins[i]; if (possiblePlugin) { + // Check if the plugin supports legacy or non legacy events + // based on the passive flag being null or a boolean + if ( + (possiblePlugin.isLegacy && passive !== null) || + (!possiblePlugin.isLegacy && passive === null) + ) { + continue; + } const extractedEvents = possiblePlugin.extractEvents( topLevelType, targetInst, diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 9f6ac6ce98c68..8ac25657ee2d9 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -22,6 +22,7 @@ export type PluginName = string; export type PluginModule = { eventTypes: EventTypes, + isLegacy: boolean, extractEvents: ( topLevelType: TopLevelType, targetInst: null | Fiber, diff --git a/packages/events/ResponderEventPlugin.js b/packages/events/ResponderEventPlugin.js index 4db54cae8777d..7f9217c545a11 100644 --- a/packages/events/ResponderEventPlugin.js +++ b/packages/events/ResponderEventPlugin.js @@ -497,6 +497,8 @@ const ResponderEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + /** * We must be resilient to `targetInst` being `null` on `touchMove` or * `touchEnd`. On certain platforms, this means that a native scroll has diff --git a/packages/react-dom/src/events/BeforeInputEventPlugin.js b/packages/react-dom/src/events/BeforeInputEventPlugin.js index 6895e0dc6254d..5f28a6d1c45f3 100644 --- a/packages/react-dom/src/events/BeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/BeforeInputEventPlugin.js @@ -462,6 +462,8 @@ function extractBeforeInputEvent( const BeforeInputEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + extractEvents: function( topLevelType, targetInst, diff --git a/packages/react-dom/src/events/ChangeEventPlugin.js b/packages/react-dom/src/events/ChangeEventPlugin.js index 9e7ba2953edd8..73def78aa9c0d 100644 --- a/packages/react-dom/src/events/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/ChangeEventPlugin.js @@ -258,6 +258,8 @@ function handleControlledInputBlur(node) { const ChangeEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + _isInputEventSupported: isInputEventSupported, extractEvents: function( diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index b608da3870d08..75fb33d209235 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -42,6 +42,8 @@ const eventTypes = { const EnterLeaveEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + /** * For almost every interaction we care about, there will be both a top-level * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 21d52c6a5a551..28c8725034bfb 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -118,12 +118,11 @@ function getListeningSetForElement( element: Document | Element | Node, isLegacy: boolean, ): Set { - if (!elementListeningObjects.has(element)) { - elementListeningObjects.set(element, createElementListeningObject()); + let listeningObject = elementListeningObjects.get(element); + if (listeningObject === undefined) { + listeningObject = createElementListeningObject(); + elementListeningObjects.set(element, listeningObject); } - const listeningObject = ((elementListeningObjects.get( - element, - ): any): ElementListeningObject); const listeningKey = isLegacy ? 'legacy' : 'nonLegacy'; return listeningObject[listeningKey]; } diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index 41fc6a4d74a7c..cb06ff4d4f48e 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -160,6 +160,8 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { const SelectEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + extractEvents: function( topLevelType, targetInst, diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index aa8ffe675a1ad..4792de019f0f3 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -206,6 +206,8 @@ const SimpleEventPlugin: PluginModule & { } = { eventTypes: eventTypes, + isLegacy: true, + isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean { const config = topLevelEventsToDispatchConfig[topLevelType]; return config !== undefined && config.isInteractive === true; diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 237349671f1e9..08015f49f59fd 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -24,6 +24,8 @@ import invariant from 'shared/invariant'; const ReactNativeBridgeEventPlugin = { eventTypes: eventTypes, + isLegacy: true, + /** * @see {EventPluginHub.extractEvents} */ From 37224eac8bb72f98836569fb079be4ec78ffd309 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 16:57:14 +0000 Subject: [PATCH 07/19] Handle fallback for no passive support --- .../src/events/ReactDOMEventListener.js | 50 ++++++++++++------- .../src/events/checkPassiveEvents.js | 26 ++++++++++ 2 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 packages/react-dom/src/events/checkPassiveEvents.js diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index c563332711a6d..9526176661503 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -21,6 +21,7 @@ import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; import {getRawEventName} from './DOMTopLevelEventTypes'; +import {passiveBrowserEventsSupported} from './checkPassiveEvents'; const {isInteractiveTopLevelEventType} = SimpleEventPlugin; @@ -146,19 +147,7 @@ export function trapBubbledEvent( : dispatchEvent; const rawEventName = getRawEventName(topLevelType); - if (isLegacy) { - // Check if interactive and wrap in interactiveUpdates - const listener = dispatch.bind(null, topLevelType, null); - // We don't listen for passive/non-passive - addEventBubbleListener(element, rawEventName, listener, null); - } else { - // Check if interactive and wrap in interactiveUpdates - const passiveListener = dispatch.bind(null, topLevelType, true); - const activeListener = dispatch.bind(null, topLevelType, false); - // We listen to the same event for both passive/non-passive - addEventBubbleListener(element, rawEventName, passiveListener, true); - addEventBubbleListener(element, rawEventName, activeListener, false); - } + trapEvent(element, topLevelType, dispatch, rawEventName, isLegacy); } export function trapCapturedEvent( @@ -171,18 +160,41 @@ export function trapCapturedEvent( : dispatchEvent; const rawEventName = getRawEventName(topLevelType); + trapEvent(element, topLevelType, dispatch, rawEventName, isLegacy); +} + +function trapEvent( + element: Document | Element | Node, + topLevelType: DOMTopLevelEventType, + dispatch: ( + topLevelType: DOMTopLevelEventType, + passive: null | boolean, + nativeEvent: AnyNativeEvent, + ) => void, + rawEventName: string, + isLegacy: boolean, +) { if (isLegacy) { // Check if interactive and wrap in interactiveUpdates const listener = dispatch.bind(null, topLevelType, null); // We don't listen for passive/non-passive addEventCaptureListener(element, rawEventName, listener, null); } else { - // Check if interactive and wrap in interactiveUpdates - const passiveListener = dispatch.bind(null, topLevelType, true); - const activeListener = dispatch.bind(null, topLevelType, false); - // We listen to the same event for both passive/non-passive - addEventCaptureListener(element, rawEventName, passiveListener, true); - addEventCaptureListener(element, rawEventName, activeListener, false); + if (passiveBrowserEventsSupported) { + // Check if interactive and wrap in interactiveUpdates + const activeListener = dispatch.bind(null, topLevelType, false); + const passiveListener = dispatch.bind(null, topLevelType, true); + // We listen to the same event for both passive/non-passive + addEventCaptureListener(element, rawEventName, passiveListener, true); + addEventCaptureListener(element, rawEventName, activeListener, false); + } else { + const fallbackListener = dispatch.bind(null, topLevelType, null); + // We fallback if we can't use passive events to only using active behaviour, + // except we pass through "false" as the passive flag to the dispatch function. + // This ensures that legacy plugins do not incorrectly operate on the fired event + // (they will only operate when "null" is on the passive flag). + addEventCaptureListener(element, rawEventName, fallbackListener, false); + } } } diff --git a/packages/react-dom/src/events/checkPassiveEvents.js b/packages/react-dom/src/events/checkPassiveEvents.js new file mode 100644 index 0000000000000..4039f141ee347 --- /dev/null +++ b/packages/react-dom/src/events/checkPassiveEvents.js @@ -0,0 +1,26 @@ +/** + * 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 let passiveBrowserEventsSupported = false; + +// Check if browser support events with passive listeners +// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support +if (typeof window !== 'undefined') { + try { + const options = { + get passive() { + passiveBrowserEventsSupported = true; + }, + }; + window.addEventListener('test', options, options); + window.removeEventListener('test', options, options); + } catch (e) { + passiveBrowserEventsSupported = false; + } +} From c6838597d3b41e922f81198e727eae67bfc2121a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 17:08:06 +0000 Subject: [PATCH 08/19] Fixed typo + improve code size --- .../src/events/ReactDOMEventListener.js | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 9526176661503..a8f71cc597f6b 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -147,7 +147,14 @@ export function trapBubbledEvent( : dispatchEvent; const rawEventName = getRawEventName(topLevelType); - trapEvent(element, topLevelType, dispatch, rawEventName, isLegacy); + trapEvent( + addEventBubbleListener, + element, + topLevelType, + dispatch, + rawEventName, + isLegacy, + ); } export function trapCapturedEvent( @@ -160,40 +167,64 @@ export function trapCapturedEvent( : dispatchEvent; const rawEventName = getRawEventName(topLevelType); - trapEvent(element, topLevelType, dispatch, rawEventName, isLegacy); + trapEvent( + addEventCaptureListener, + element, + topLevelType, + dispatch, + rawEventName, + isLegacy, + ); +} + +type Dispatcher = ( + topLevelType: DOMTopLevelEventType, + passive: null | boolean, + nativeEvent: AnyNativeEvent, +) => void; + +// A helper function to remove bytes +function bindDispatch( + dispatch: Dispatcher, + topLevelType: DOMTopLevelEventType, + passive: null | boolean, +) { + return dispatch.bind(null, topLevelType, passive); } function trapEvent( + eventListener: ( + element: Document | Element | Node, + eventName: string, + listener: (event: AnyNativeEvent) => void, + passive: boolean | null, + ) => void, element: Document | Element | Node, topLevelType: DOMTopLevelEventType, - dispatch: ( - topLevelType: DOMTopLevelEventType, - passive: null | boolean, - nativeEvent: AnyNativeEvent, - ) => void, + dispatch: Dispatcher, rawEventName: string, isLegacy: boolean, ) { if (isLegacy) { // Check if interactive and wrap in interactiveUpdates - const listener = dispatch.bind(null, topLevelType, null); + const listener = bindDispatch(dispatch, topLevelType, null); // We don't listen for passive/non-passive - addEventCaptureListener(element, rawEventName, listener, null); + eventListener(element, rawEventName, listener, null); } else { if (passiveBrowserEventsSupported) { // Check if interactive and wrap in interactiveUpdates - const activeListener = dispatch.bind(null, topLevelType, false); - const passiveListener = dispatch.bind(null, topLevelType, true); + const activeListener = bindDispatch(dispatch, topLevelType, false); + const passiveListener = bindDispatch(dispatch, topLevelType, true); // We listen to the same event for both passive/non-passive - addEventCaptureListener(element, rawEventName, passiveListener, true); - addEventCaptureListener(element, rawEventName, activeListener, false); + eventListener(element, rawEventName, passiveListener, true); + eventListener(element, rawEventName, activeListener, false); } else { - const fallbackListener = dispatch.bind(null, topLevelType, null); + const fallbackListener = bindDispatch(dispatch, topLevelType, null); // We fallback if we can't use passive events to only using active behaviour, // except we pass through "false" as the passive flag to the dispatch function. // This ensures that legacy plugins do not incorrectly operate on the fired event // (they will only operate when "null" is on the passive flag). - addEventCaptureListener(element, rawEventName, fallbackListener, false); + eventListener(element, rawEventName, fallbackListener, false); } } } From e7e742f6c70b0a121df50c0cb6ac918813478199 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 20:01:01 +0000 Subject: [PATCH 09/19] Changed to enum for passive event types and addressed PR feedback --- packages/events/EventPluginHub.js | 48 ++++++------- packages/events/ListenerTypes.js | 15 ++++ packages/events/PluginModuleType.js | 1 - packages/events/ResponderEventPlugin.js | 2 - .../src/events/BeforeInputEventPlugin.js | 2 - .../react-dom/src/events/ChangeEventPlugin.js | 2 - .../src/events/EnterLeaveEventPlugin.js | 2 - .../react-dom/src/events/EventListener.js | 19 ++++-- .../src/events/ReactBrowserEventEmitter.js | 3 +- .../src/events/ReactDOMEventListener.js | 68 +++++++++++-------- .../react-dom/src/events/SelectEventPlugin.js | 2 - .../react-dom/src/events/SimpleEventPlugin.js | 2 - .../src/events/forks/EventListener-www.js | 29 ++++++-- .../src/test-utils/ReactTestUtils.js | 3 +- .../src/ReactFabricEventEmitter.js | 3 +- .../src/ReactNativeBridgeEventPlugin.js | 2 - .../src/ReactNativeEventEmitter.js | 3 +- 17 files changed, 123 insertions(+), 83 deletions(-) create mode 100644 packages/events/ListenerTypes.js diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index 29f01037df076..67af059f023be 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -27,6 +27,8 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {AnyNativeEvent} from './PluginModuleType'; import type {TopLevelType} from './TopLevelEventTypes'; +import {type ListenerType, PASSIVE_DISABLED} from 'events/ListenerTypes'; + /** * Internal queue of events that have accumulated their dispatches and are * waiting to have their dispatches executed. @@ -163,33 +165,31 @@ function extractEvents( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, - passive: null | boolean, + listenerType: ListenerType, ): Array | ReactSyntheticEvent | null { let events = null; - for (let i = 0; i < plugins.length; i++) { - // Not every plugin in the ordering may be loaded at runtime. - const possiblePlugin: PluginModule = plugins[i]; - if (possiblePlugin) { - // Check if the plugin supports legacy or non legacy events - // based on the passive flag being null or a boolean - if ( - (possiblePlugin.isLegacy && passive !== null) || - (!possiblePlugin.isLegacy && passive === null) - ) { - continue; - } - const extractedEvents = possiblePlugin.extractEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - passive, - ); - if (extractedEvents) { - events = accumulateInto(events, extractedEvents); + // For events that don't use the new passive event type system, + // we continue to use event plugins. This will get updated once + // we add plugins or adapters that make use of the passive event + // system. + if (listenerType === PASSIVE_DISABLED) { + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); + } } } } + return events; } @@ -224,14 +224,14 @@ export function runExtractedEventsInBatch( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, - passive: null | boolean, + listenerType: ListenerType, ) { const events = extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, - passive, + listenerType, ); runEventsInBatch(events); } diff --git a/packages/events/ListenerTypes.js b/packages/events/ListenerTypes.js new file mode 100644 index 0000000000000..9d81692d6421d --- /dev/null +++ b/packages/events/ListenerTypes.js @@ -0,0 +1,15 @@ +/** + * 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 const PASSIVE_DISABLED = 0; +export const PASSIVE_FALLBACK = 1; +export const PASSIVE_TRUE = 2; +export const PASSIVE_FALSE = 3; + +export type ListenerType = 0 | 1 | 2 | 3; diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 8ac25657ee2d9..9f6ac6ce98c68 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -22,7 +22,6 @@ export type PluginName = string; export type PluginModule = { eventTypes: EventTypes, - isLegacy: boolean, extractEvents: ( topLevelType: TopLevelType, targetInst: null | Fiber, diff --git a/packages/events/ResponderEventPlugin.js b/packages/events/ResponderEventPlugin.js index 7f9217c545a11..4db54cae8777d 100644 --- a/packages/events/ResponderEventPlugin.js +++ b/packages/events/ResponderEventPlugin.js @@ -497,8 +497,6 @@ const ResponderEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - /** * We must be resilient to `targetInst` being `null` on `touchMove` or * `touchEnd`. On certain platforms, this means that a native scroll has diff --git a/packages/react-dom/src/events/BeforeInputEventPlugin.js b/packages/react-dom/src/events/BeforeInputEventPlugin.js index 5f28a6d1c45f3..6895e0dc6254d 100644 --- a/packages/react-dom/src/events/BeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/BeforeInputEventPlugin.js @@ -462,8 +462,6 @@ function extractBeforeInputEvent( const BeforeInputEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - extractEvents: function( topLevelType, targetInst, diff --git a/packages/react-dom/src/events/ChangeEventPlugin.js b/packages/react-dom/src/events/ChangeEventPlugin.js index 73def78aa9c0d..9e7ba2953edd8 100644 --- a/packages/react-dom/src/events/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/ChangeEventPlugin.js @@ -258,8 +258,6 @@ function handleControlledInputBlur(node) { const ChangeEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - _isInputEventSupported: isInputEventSupported, extractEvents: function( diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index 75fb33d209235..b608da3870d08 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -42,8 +42,6 @@ const eventTypes = { const EnterLeaveEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - /** * For almost every interaction we care about, there will be both a top-level * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 3cf2a860ac799..29362b1dc65f1 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -7,17 +7,24 @@ * @flow */ +import { + type ListenerType, + PASSIVE_DISABLED, + PASSIVE_FALLBACK, + PASSIVE_TRUE, +} from 'events/ListenerTypes'; + export function addEventBubbleListener( element: Document | Element | Node, eventType: string, listener: Function, - isPassive: boolean | null, + listenerType: ListenerType, ): void { - if (isPassive === null) { + if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { element.addEventListener(eventType, listener, false); } else { element.addEventListener(eventType, listener, { - passive: isPassive, + passive: listenerType === PASSIVE_TRUE, capture: false, }); } @@ -27,13 +34,13 @@ export function addEventCaptureListener( element: Document | Element | Node, eventType: string, listener: Function, - isPassive: boolean | null, + listenerType: ListenerType, ): void { - if (isPassive === null) { + if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { element.addEventListener(eventType, listener, true); } else { element.addEventListener(eventType, listener, { - passive: isPassive, + passive: listenerType === PASSIVE_TRUE, capture: true, }); } diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 28c8725034bfb..8e978de83f6e0 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -123,8 +123,7 @@ function getListeningSetForElement( listeningObject = createElementListeningObject(); elementListeningObjects.set(element, listeningObject); } - const listeningKey = isLegacy ? 'legacy' : 'nonLegacy'; - return listeningObject[listeningKey]; + return isLegacy ? listeningObject.legacy : listeningObject.nonLegacy; } /** diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index a8f71cc597f6b..0f3ce567e4a07 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -15,6 +15,13 @@ import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactWorkTags'; +import { + type ListenerType, + PASSIVE_DISABLED, + PASSIVE_FALLBACK, + PASSIVE_TRUE, + PASSIVE_FALSE, +} from 'events/ListenerTypes'; import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; @@ -33,7 +40,7 @@ type BookKeepingInstance = { nativeEvent: AnyNativeEvent | null, targetInst: Fiber | null, ancestors: Array, - passive: null | boolean, + listenerType: null | ListenerType, }; /** @@ -60,14 +67,14 @@ function getTopLevelCallbackBookKeeping( topLevelType: DOMTopLevelEventType, nativeEvent: AnyNativeEvent, targetInst: Fiber | null, - passive: null | boolean, + listenerType: ListenerType, ): BookKeepingInstance { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; instance.nativeEvent = nativeEvent; instance.targetInst = targetInst; - instance.passive = passive; + instance.listenerType = listenerType; return instance; } return { @@ -75,7 +82,7 @@ function getTopLevelCallbackBookKeeping( nativeEvent, targetInst, ancestors: [], - passive, + listenerType, }; } @@ -86,7 +93,7 @@ function releaseTopLevelCallbackBookKeeping( instance.nativeEvent = null; instance.targetInst = null; instance.ancestors.length = 0; - instance.passive = null; + instance.listenerType = null; if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { callbackBookkeepingPool.push(instance); } @@ -121,7 +128,7 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) { targetInst, ((bookKeeping.nativeEvent: any): AnyNativeEvent), getEventTarget(bookKeeping.nativeEvent), - bookKeeping.passive, + ((bookKeeping.listenerType: any): ListenerType), ); } } @@ -179,7 +186,7 @@ export function trapCapturedEvent( type Dispatcher = ( topLevelType: DOMTopLevelEventType, - passive: null | boolean, + listenerType: ListenerType, nativeEvent: AnyNativeEvent, ) => void; @@ -187,9 +194,9 @@ type Dispatcher = ( function bindDispatch( dispatch: Dispatcher, topLevelType: DOMTopLevelEventType, - passive: null | boolean, + listenerType: ListenerType, ) { - return dispatch.bind(null, topLevelType, passive); + return dispatch.bind(null, topLevelType, listenerType); } function trapEvent( @@ -197,7 +204,7 @@ function trapEvent( element: Document | Element | Node, eventName: string, listener: (event: AnyNativeEvent) => void, - passive: boolean | null, + listenerType: ListenerType, ) => void, element: Document | Element | Node, topLevelType: DOMTopLevelEventType, @@ -207,36 +214,43 @@ function trapEvent( ) { if (isLegacy) { // Check if interactive and wrap in interactiveUpdates - const listener = bindDispatch(dispatch, topLevelType, null); + const listener = bindDispatch(dispatch, topLevelType, PASSIVE_DISABLED); // We don't listen for passive/non-passive - eventListener(element, rawEventName, listener, null); + eventListener(element, rawEventName, listener, PASSIVE_DISABLED); } else { if (passiveBrowserEventsSupported) { // Check if interactive and wrap in interactiveUpdates - const activeListener = bindDispatch(dispatch, topLevelType, false); - const passiveListener = bindDispatch(dispatch, topLevelType, true); + const activeListener = bindDispatch( + dispatch, + topLevelType, + PASSIVE_FALSE, + ); + const passiveListener = bindDispatch( + dispatch, + topLevelType, + PASSIVE_TRUE, + ); // We listen to the same event for both passive/non-passive - eventListener(element, rawEventName, passiveListener, true); - eventListener(element, rawEventName, activeListener, false); + eventListener(element, rawEventName, passiveListener, PASSIVE_FALSE); + eventListener(element, rawEventName, activeListener, PASSIVE_TRUE); } else { - const fallbackListener = bindDispatch(dispatch, topLevelType, null); - // We fallback if we can't use passive events to only using active behaviour, - // except we pass through "false" as the passive flag to the dispatch function. - // This ensures that legacy plugins do not incorrectly operate on the fired event - // (they will only operate when "null" is on the passive flag). - eventListener(element, rawEventName, fallbackListener, false); + const fallbackListener = bindDispatch( + dispatch, + topLevelType, + PASSIVE_FALLBACK, + ); + eventListener(element, rawEventName, fallbackListener, PASSIVE_FALLBACK); } } } -function dispatchInteractiveEvent(topLevelType, isPassiveEvent, nativeEvent) { - interactiveUpdates(dispatchEvent, topLevelType, isPassiveEvent, nativeEvent); +function dispatchInteractiveEvent(topLevelType, listenerType, nativeEvent) { + interactiveUpdates(dispatchEvent, topLevelType, listenerType, nativeEvent); } export function dispatchEvent( topLevelType: DOMTopLevelEventType, - // passive will be `null` for legacy events - passive: null | boolean, + listenerType: ListenerType, nativeEvent: AnyNativeEvent, ): void { if (!_enabled) { @@ -261,7 +275,7 @@ export function dispatchEvent( topLevelType, nativeEvent, targetInst, - passive, + listenerType, ); try { diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index cb06ff4d4f48e..41fc6a4d74a7c 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -160,8 +160,6 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { const SelectEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - extractEvents: function( topLevelType, targetInst, diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 4792de019f0f3..aa8ffe675a1ad 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -206,8 +206,6 @@ const SimpleEventPlugin: PluginModule & { } = { eventTypes: eventTypes, - isLegacy: true, - isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean { const config = topLevelEventsToDispatchConfig[topLevelType]; return config !== undefined && config.isInteractive === true; diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 06f4f113d6ce0..da2b44cc63f03 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -7,6 +7,13 @@ * @flow */ +import { + type ListenerType, + PASSIVE_DISABLED, + PASSIVE_FALLBACK, + PASSIVE_TRUE, +} from 'events/ListenerTypes'; + const EventListenerWWW = require('EventListener'); import typeof * as EventListenerType from '../EventListener'; @@ -16,12 +23,17 @@ export function addEventBubbleListener( element: Element, eventType: string, listener: Function, - isPassive: boolean | null, + listenerType: ListenerType, ): void { - if (isPassive === null) { + if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { EventListenerWWW.listen(element, eventType, listener); } else { - EventListenerWWW.listen(element, eventType, listener, isPassive); + EventListenerWWW.listen( + element, + eventType, + listener, + listenerType === PASSIVE_TRUE, + ); } } @@ -29,12 +41,17 @@ export function addEventCaptureListener( element: Element, eventType: string, listener: Function, - isPassive: boolean | null, + listenerType: ListenerType, ): void { - if (isPassive === null) { + if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { EventListenerWWW.capture(element, eventType, listener); } else { - EventListenerWWW.capture(element, eventType, listener, isPassive); + EventListenerWWW.capture( + element, + eventType, + listener, + listenerType === PASSIVE_TRUE, + ); } } diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index abd4c554d6c87..9686ec7442d42 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -21,6 +21,7 @@ import lowPriorityWarning from 'shared/lowPriorityWarning'; import warningWithoutStack from 'shared/warningWithoutStack'; import {ELEMENT_NODE} from '../shared/HTMLNodeType'; import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes'; +import {PASSIVE_DISABLED} from 'events/ListenerTypes'; // for .act's return value type Thenable = { @@ -63,7 +64,7 @@ let hasWarnedAboutDeprecatedMockComponent = false; */ function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { fakeNativeEvent.target = node; - dispatchEvent(topLevelType, null, fakeNativeEvent); + dispatchEvent(topLevelType, PASSIVE_DISABLED, fakeNativeEvent); } /** diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index 0d9c067f61985..10b4de512d003 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -15,6 +15,7 @@ import {batchedUpdates} from 'events/ReactGenericBatching'; import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {TopLevelType} from 'events/TopLevelEventTypes'; +import {PASSIVE_DISABLED} from 'events/ListenerTypes'; export {getListener, registrationNameModules as registrationNames}; @@ -30,7 +31,7 @@ export function dispatchEvent( targetFiber, nativeEvent, nativeEvent.target, - null, + PASSIVE_DISABLED, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 08015f49f59fd..237349671f1e9 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -24,8 +24,6 @@ import invariant from 'shared/invariant'; const ReactNativeBridgeEventPlugin = { eventTypes: eventTypes, - isLegacy: true, - /** * @see {EventPluginHub.extractEvents} */ diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index 97c7299f0ef4b..f54ce8c12f436 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -16,6 +16,7 @@ import {getInstanceFromNode} from './ReactNativeComponentTree'; import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {TopLevelType} from 'events/TopLevelEventTypes'; +import {PASSIVE_DISABLED} from 'events/ListenerTypes'; export {getListener, registrationNameModules as registrationNames}; @@ -100,7 +101,7 @@ function _receiveRootNodeIDEvent( inst, nativeEvent, nativeEvent.target, - null, + PASSIVE_DISABLED, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's From ccf388433b8ec457d18b71d5116ee9987d85f1c5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 20:07:54 +0000 Subject: [PATCH 10/19] Added comment to describe intent --- packages/react-dom/src/events/ReactDOMEventListener.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 0f3ce567e4a07..c3b60a5d60cf9 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -230,7 +230,14 @@ function trapEvent( topLevelType, PASSIVE_TRUE, ); - // We listen to the same event for both passive/non-passive + // We listen to the same event for both passive true/passive false. + // We do this so future event experiments can handle conditional logic. + // For example, we might want to support some derivative of both + // onMouseMovePassive and onMouseMoveActive, where the underlying logic + // is forked conditionally, depending on the event handler being + // passive or not. Furthermore, given we generally always listen to + // events on the root, we have have to anticipate that this might + // occur and listen to both ahead of time. eventListener(element, rawEventName, passiveListener, PASSIVE_FALSE); eventListener(element, rawEventName, activeListener, PASSIVE_TRUE); } else { From c9fc1780c256252894e303372c6324e87e71906b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 22:40:00 +0000 Subject: [PATCH 11/19] use canUseDOM --- packages/react-dom/src/events/checkPassiveEvents.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/events/checkPassiveEvents.js b/packages/react-dom/src/events/checkPassiveEvents.js index 4039f141ee347..8e061749e2d74 100644 --- a/packages/react-dom/src/events/checkPassiveEvents.js +++ b/packages/react-dom/src/events/checkPassiveEvents.js @@ -7,11 +7,13 @@ * @flow */ +import {canUseDOM} from 'shared/ExecutionEnvironment'; + export let passiveBrowserEventsSupported = false; // Check if browser support events with passive listeners // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support -if (typeof window !== 'undefined') { +if (canUseDOM) { try { const options = { get passive() { From 5d5b9725f8e4c1bd3673846c4376e61d7bd83ec1 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 13 Mar 2019 23:02:54 +0000 Subject: [PATCH 12/19] Cleaned up some logic and reduces more bytes --- packages/events/EventPluginHub.js | 37 +++++++------------ .../src/events/ReactDOMEventListener.js | 20 ++++++---- .../src/ReactFabricEventEmitter.js | 2 - .../src/ReactNativeEventEmitter.js | 2 - 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index 67af059f023be..da20a885add65 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -27,8 +27,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {AnyNativeEvent} from './PluginModuleType'; import type {TopLevelType} from './TopLevelEventTypes'; -import {type ListenerType, PASSIVE_DISABLED} from 'events/ListenerTypes'; - /** * Internal queue of events that have accumulated their dispatches and are * waiting to have their dispatches executed. @@ -165,31 +163,24 @@ function extractEvents( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, - listenerType: ListenerType, ): Array | ReactSyntheticEvent | null { let events = null; - // For events that don't use the new passive event type system, - // we continue to use event plugins. This will get updated once - // we add plugins or adapters that make use of the passive event - // system. - if (listenerType === PASSIVE_DISABLED) { - for (let i = 0; i < plugins.length; i++) { - // Not every plugin in the ordering may be loaded at runtime. - const possiblePlugin: PluginModule = plugins[i]; - if (possiblePlugin) { - const extractedEvents = possiblePlugin.extractEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - ); - if (extractedEvents) { - events = accumulateInto(events, extractedEvents); - } + + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); } } } - return events; } @@ -224,14 +215,12 @@ export function runExtractedEventsInBatch( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, - listenerType: ListenerType, ) { const events = extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, - listenerType, ); runEventsInBatch(events); } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index c3b60a5d60cf9..f4434ee94b97d 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -123,13 +123,19 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) { for (let i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; - runExtractedEventsInBatch( - ((bookKeeping.topLevelType: any): DOMTopLevelEventType), - targetInst, - ((bookKeeping.nativeEvent: any): AnyNativeEvent), - getEventTarget(bookKeeping.nativeEvent), - ((bookKeeping.listenerType: any): ListenerType), - ); + // For events that don't use the new passive event type system, + // we use the current event plugin hub for extracting and + // dispatching events. For future experimental APIs, we'll + // likely use an alternative system without the abstraction + // costs of a full plugin even system. + if (bookKeeping.listenerType === PASSIVE_DISABLED) { + runExtractedEventsInBatch( + ((bookKeeping.topLevelType: any): DOMTopLevelEventType), + targetInst, + ((bookKeeping.nativeEvent: any): AnyNativeEvent), + getEventTarget(bookKeeping.nativeEvent), + ); + } } } diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index 10b4de512d003..7e016f8ce8c51 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -15,7 +15,6 @@ import {batchedUpdates} from 'events/ReactGenericBatching'; import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {TopLevelType} from 'events/TopLevelEventTypes'; -import {PASSIVE_DISABLED} from 'events/ListenerTypes'; export {getListener, registrationNameModules as registrationNames}; @@ -31,7 +30,6 @@ export function dispatchEvent( targetFiber, nativeEvent, nativeEvent.target, - PASSIVE_DISABLED, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index f54ce8c12f436..a5ec6e298f13d 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -16,7 +16,6 @@ import {getInstanceFromNode} from './ReactNativeComponentTree'; import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {TopLevelType} from 'events/TopLevelEventTypes'; -import {PASSIVE_DISABLED} from 'events/ListenerTypes'; export {getListener, registrationNameModules as registrationNames}; @@ -101,7 +100,6 @@ function _receiveRootNodeIDEvent( inst, nativeEvent, nativeEvent.target, - PASSIVE_DISABLED, ); }); // React Native doesn't use ReactControlledComponent but if it did, here's From e186b095958d6753639af97f6efb8da23d678c0c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 00:12:46 +0000 Subject: [PATCH 13/19] Fix typo --- packages/react-dom/src/events/ReactDOMEventListener.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index f4434ee94b97d..5d58db5f07282 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -244,8 +244,8 @@ function trapEvent( // passive or not. Furthermore, given we generally always listen to // events on the root, we have have to anticipate that this might // occur and listen to both ahead of time. - eventListener(element, rawEventName, passiveListener, PASSIVE_FALSE); - eventListener(element, rawEventName, activeListener, PASSIVE_TRUE); + eventListener(element, rawEventName, passiveListener, PASSIVE_TRUE); + eventListener(element, rawEventName, activeListener, PASSIVE_FALSE); } else { const fallbackListener = bindDispatch( dispatch, From 1dff1fceb1eb1a4ba62a683ab377e44ac183dfe3 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 11:54:19 +0000 Subject: [PATCH 14/19] Address nits --- packages/events/EventPluginHub.js | 1 - packages/events/PluginModuleType.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index da20a885add65..e639110713481 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -165,7 +165,6 @@ function extractEvents( nativeEventTarget: EventTarget, ): Array | ReactSyntheticEvent | null { let events = null; - for (let i = 0; i < plugins.length; i++) { // Not every plugin in the ordering may be loaded at runtime. const possiblePlugin: PluginModule = plugins[i]; diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 9f6ac6ce98c68..cd7a07661ab4e 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -27,7 +27,6 @@ export type PluginModule = { targetInst: null | Fiber, nativeTarget: NativeEvent, nativeEventTarget: EventTarget, - passive?: null | boolean, ) => ?ReactSyntheticEvent, tapMoveThreshold?: number, }; From 8f79ee88769d992a2f535052f1f08ea714bd5fe7 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 19:30:21 +0000 Subject: [PATCH 15/19] Addressed feedback and took a different approach with flags --- packages/events/EventSystemFlags.js | 16 ++ packages/events/ListenerTypes.js | 15 -- .../react-dom/src/client/ReactDOMComponent.js | 46 ++--- .../react-dom/src/events/EventListener.js | 19 +- .../src/events/ReactBrowserEventEmitter.js | 123 +++++------- .../src/events/ReactDOMEventListener.js | 175 +++++++----------- .../react-dom/src/events/SelectEventPlugin.js | 2 +- .../src/events/forks/EventListener-www.js | 33 +--- .../src/test-utils/ReactTestUtils.js | 4 +- 9 files changed, 167 insertions(+), 266 deletions(-) create mode 100644 packages/events/EventSystemFlags.js delete mode 100644 packages/events/ListenerTypes.js diff --git a/packages/events/EventSystemFlags.js b/packages/events/EventSystemFlags.js new file mode 100644 index 0000000000000..be5e3544a2ea4 --- /dev/null +++ b/packages/events/EventSystemFlags.js @@ -0,0 +1,16 @@ +/** + * 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; diff --git a/packages/events/ListenerTypes.js b/packages/events/ListenerTypes.js deleted file mode 100644 index 9d81692d6421d..0000000000000 --- a/packages/events/ListenerTypes.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * 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 const PASSIVE_DISABLED = 0; -export const PASSIVE_FALLBACK = 1; -export const PASSIVE_TRUE = 2; -export const PASSIVE_FALSE = 3; - -export type ListenerType = 0 | 1 | 2 | 3; diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 4a4e444a58bc5..9c21c5b79aae0 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -263,7 +263,7 @@ function ensureListeningTo( const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; - listenTo(registrationName, doc, true /* isLegacy */); + listenTo(registrationName, doc); } function getOwnerDocumentFromRootContainer( @@ -497,41 +497,41 @@ export function setInitialProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement, true); + trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'video': case 'audio': // Create listener for each media event for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement, true); + trapBubbledEvent(mediaEventTypes[i], domElement); } props = rawProps; break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement, true); + trapBubbledEvent(TOP_ERROR, domElement); props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement, true); - trapBubbledEvent(TOP_LOAD, domElement, true); + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement, true); - trapBubbledEvent(TOP_SUBMIT, domElement, true); + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement, true); + trapBubbledEvent(TOP_TOGGLE, domElement); props = rawProps; break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); props = ReactDOMInputGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -543,7 +543,7 @@ export function setInitialProperties( case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); props = ReactDOMSelectGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -551,7 +551,7 @@ export function setInitialProperties( case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); props = ReactDOMTextareaGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -891,34 +891,34 @@ export function diffHydratedProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement, true); + trapBubbledEvent(TOP_LOAD, domElement); break; case 'video': case 'audio': // Create listener for each media event for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement, true); + trapBubbledEvent(mediaEventTypes[i], domElement); } break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement, true); + trapBubbledEvent(TOP_ERROR, domElement); break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement, true); - trapBubbledEvent(TOP_LOAD, domElement, true); + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); break; case 'form': - trapBubbledEvent(TOP_RESET, domElement, true); - trapBubbledEvent(TOP_SUBMIT, domElement, true); + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement, true); + trapBubbledEvent(TOP_TOGGLE, domElement); break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -928,14 +928,14 @@ export function diffHydratedProperties( break; case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement, true); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 29362b1dc65f1..944246327ebb7 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -7,24 +7,17 @@ * @flow */ -import { - type ListenerType, - PASSIVE_DISABLED, - PASSIVE_FALLBACK, - PASSIVE_TRUE, -} from 'events/ListenerTypes'; - export function addEventBubbleListener( element: Document | Element | Node, eventType: string, listener: Function, - listenerType: ListenerType, + passive?: boolean, ): void { - if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { + if (passive === undefined) { element.addEventListener(eventType, listener, false); } else { element.addEventListener(eventType, listener, { - passive: listenerType === PASSIVE_TRUE, + passive, capture: false, }); } @@ -34,13 +27,13 @@ export function addEventCaptureListener( element: Document | Element | Node, eventType: string, listener: Function, - listenerType: ListenerType, + passive?: boolean, ): void { - if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { + if (passive === undefined) { element.addEventListener(eventType, listener, true); } else { element.addEventListener(eventType, listener, { - passive: listenerType === PASSIVE_TRUE, + passive, capture: true, }); } diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 8e978de83f6e0..2f9f29fcd3097 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -86,44 +86,22 @@ import isEventSupported from './isEventSupported'; */ const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -const elementListeningObjects: +const elementListeningSets: | WeakMap | Map< Document | Element | Node, - ElementListeningObject, + Set, > = new PossiblyWeakMap(); -// This object will contain both legacy and non legacy events. -// In the case of legacy events, we register an event listener -// without passing an object with { passive, capture } etc to -// third argument of the addEventListener call (we use a boolean). -// For non legacy events, we register an event listener with -// an object (third argument) with the passive property. We also -// double listen for the event on the element, as we need to check -// both paths (where passive is both true and false) so we can -// handle logic in either case. -export type ElementListeningObject = { - legacy: Set, - nonLegacy: Set, -}; - -function createElementListeningObject(): ElementListeningObject { - return { - legacy: new Set(), - nonLegacy: new Set(), - }; -} - function getListeningSetForElement( element: Document | Element | Node, - isLegacy: boolean, ): Set { - let listeningObject = elementListeningObjects.get(element); - if (listeningObject === undefined) { - listeningObject = createElementListeningObject(); - elementListeningObjects.set(element, listeningObject); + let listeningSet = elementListeningSets.get(element); + if (listeningSet === undefined) { + listeningSet = new Set(); + elementListeningSets.set(element, listeningSet); } - return isLegacy ? listeningObject.legacy : listeningObject.nonLegacy; + return listeningSet; } /** @@ -150,68 +128,57 @@ function getListeningSetForElement( export function listenTo( registrationName: string, mountAt: Document | Element | Node, - isLegacy: boolean, ): void { - const listeningSet = getListeningSetForElement(mountAt, isLegacy); + const listeningSet = getListeningSetForElement(mountAt); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; - listenToDependency(dependency, listeningSet, mountAt, isLegacy); - } -} - -function listenToDependency( - dependency: DOMTopLevelEventType, - listeningSet: Set, - mountAt: Document | Element | Node, - isLegacy: boolean, -): void { - if (!listeningSet.has(dependency)) { - switch (dependency) { - case TOP_SCROLL: - trapCapturedEvent(TOP_SCROLL, mountAt, isLegacy); - break; - case TOP_FOCUS: - case TOP_BLUR: - trapCapturedEvent(TOP_FOCUS, mountAt, isLegacy); - trapCapturedEvent(TOP_BLUR, mountAt, isLegacy); - // We set the flag for a single dependency later in this function, - // but this ensures we mark both as attached rather than just one. - listeningSet.add(TOP_BLUR); - listeningSet.add(TOP_FOCUS); - break; - case TOP_CANCEL: - case TOP_CLOSE: - if (isEventSupported(getRawEventName(dependency))) { - trapCapturedEvent(dependency, mountAt, isLegacy); - } - break; - case TOP_INVALID: - case TOP_SUBMIT: - case TOP_RESET: - // We listen to them on the target DOM elements. - // Some of them bubble so we don't want them to fire twice. - break; - default: - // By default, listen on the top level to all non-media events. - // Media events don't bubble so adding the listener wouldn't do anything. - const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; - if (!isMediaEvent) { - trapBubbledEvent(dependency, mountAt, isLegacy); - } - break; + if (!listeningSet.has(dependency)) { + switch (dependency) { + case TOP_SCROLL: + trapCapturedEvent(TOP_SCROLL, mountAt); + break; + case TOP_FOCUS: + case TOP_BLUR: + trapCapturedEvent(TOP_FOCUS, mountAt); + trapCapturedEvent(TOP_BLUR, mountAt); + // We set the flag for a single dependency later in this function, + // but this ensures we mark both as attached rather than just one. + listeningSet.add(TOP_BLUR); + listeningSet.add(TOP_FOCUS); + break; + case TOP_CANCEL: + case TOP_CLOSE: + if (isEventSupported(getRawEventName(dependency))) { + trapCapturedEvent(dependency, mountAt); + } + break; + case TOP_INVALID: + case TOP_SUBMIT: + case TOP_RESET: + // We listen to them on the target DOM elements. + // Some of them bubble so we don't want them to fire twice. + break; + default: + // By default, listen on the top level to all non-media events. + // Media events don't bubble so adding the listener wouldn't do anything. + const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; + if (!isMediaEvent) { + trapBubbledEvent(dependency, mountAt); + } + break; + } + listeningSet.add(dependency); } - listeningSet.add(dependency); } } export function isListeningToAllDependencies( registrationName: string, mountAt: Document | Element, - isLegacy: boolean, ): boolean { - const listeningSet = getListeningSetForElement(mountAt, isLegacy); + const listeningSet = getListeningSetForElement(mountAt); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 5d58db5f07282..de2217d441f3a 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -16,12 +16,13 @@ import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactWorkTags'; import { - type ListenerType, - PASSIVE_DISABLED, - PASSIVE_FALLBACK, - PASSIVE_TRUE, - PASSIVE_FALSE, -} from 'events/ListenerTypes'; + type EventSystemFlags, + PLUGIN_EVENT_SYSTEM, + RESPONDER_EVENT_SYSTEM, + IS_PASSIVE, + IS_ACTIVE, + PASSIVE_NOT_SUPPORTED, +} from 'events/EventSystemFlags'; import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; @@ -40,7 +41,7 @@ type BookKeepingInstance = { nativeEvent: AnyNativeEvent | null, targetInst: Fiber | null, ancestors: Array, - listenerType: null | ListenerType, + eventSystemFlags: EventSystemFlags, }; /** @@ -67,14 +68,14 @@ function getTopLevelCallbackBookKeeping( topLevelType: DOMTopLevelEventType, nativeEvent: AnyNativeEvent, targetInst: Fiber | null, - listenerType: ListenerType, + eventSystemFlags: EventSystemFlags, ): BookKeepingInstance { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; instance.nativeEvent = nativeEvent; instance.targetInst = targetInst; - instance.listenerType = listenerType; + instance.eventSystemFlags = eventSystemFlags; return instance; } return { @@ -82,7 +83,7 @@ function getTopLevelCallbackBookKeeping( nativeEvent, targetInst, ancestors: [], - listenerType, + eventSystemFlags, }; } @@ -93,7 +94,7 @@ function releaseTopLevelCallbackBookKeeping( instance.nativeEvent = null; instance.targetInst = null; instance.ancestors.length = 0; - instance.listenerType = null; + instance.eventSystemFlags = 0; if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { callbackBookkeepingPool.push(instance); } @@ -123,18 +124,16 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) { for (let i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; - // For events that don't use the new passive event type system, - // we use the current event plugin hub for extracting and - // dispatching events. For future experimental APIs, we'll - // likely use an alternative system without the abstraction - // costs of a full plugin even system. - if (bookKeeping.listenerType === PASSIVE_DISABLED) { + if (bookKeeping.eventSystemFlags === PLUGIN_EVENT_SYSTEM) { runExtractedEventsInBatch( ((bookKeeping.topLevelType: any): DOMTopLevelEventType), targetInst, ((bookKeeping.nativeEvent: any): AnyNativeEvent), getEventTarget(bookKeeping.nativeEvent), ); + } else { + // RESPONDER_EVENT_SYSTEM + // TODO: Add implementation } } } @@ -153,117 +152,83 @@ export function isEnabled() { export function trapBubbledEvent( topLevelType: DOMTopLevelEventType, element: Document | Element | Node, - isLegacy: boolean, ): void { - const dispatch = isInteractiveTopLevelEventType(topLevelType) - ? dispatchInteractiveEvent - : dispatchEvent; - const rawEventName = getRawEventName(topLevelType); - - trapEvent( - addEventBubbleListener, - element, - topLevelType, - dispatch, - rawEventName, - isLegacy, - ); + trapEventForPluginEventSystem(element, topLevelType, false); } export function trapCapturedEvent( topLevelType: DOMTopLevelEventType, element: Document | Element | Node, - isLegacy: boolean, +): void { + trapEventForPluginEventSystem(element, topLevelType, true); +} + +export function trapEventForResponderEventSystem( + element: Document | Element | Node, + topLevelType: DOMTopLevelEventType, + capture: boolean, + passive: boolean, ): void { const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; const rawEventName = getRawEventName(topLevelType); + let eventFlags = RESPONDER_EVENT_SYSTEM; - trapEvent( - addEventCaptureListener, - element, - topLevelType, - dispatch, - rawEventName, - isLegacy, - ); -} - -type Dispatcher = ( - topLevelType: DOMTopLevelEventType, - listenerType: ListenerType, - nativeEvent: AnyNativeEvent, -) => void; - -// A helper function to remove bytes -function bindDispatch( - dispatch: Dispatcher, - topLevelType: DOMTopLevelEventType, - listenerType: ListenerType, -) { - return dispatch.bind(null, topLevelType, listenerType); + // If passive option is not supported, then the event will be + // active and not passive, but we flag it as using not being + // supported too. This way the responder event plugins know, + // and can provide polyfills if needed. + if (passive) { + if (passiveBrowserEventsSupported) { + eventFlags |= IS_ACTIVE; + eventFlags |= PASSIVE_NOT_SUPPORTED; + passive = false; + } else { + eventFlags |= IS_PASSIVE; + } + } else { + eventFlags |= IS_ACTIVE; + } + // Check if interactive and wrap in interactiveUpdates + const listener = dispatch.bind(null, topLevelType, eventFlags); + if (capture) { + addEventCaptureListener(element, rawEventName, listener, passive); + } else { + addEventBubbleListener(element, rawEventName, listener, passive); + } } -function trapEvent( - eventListener: ( - element: Document | Element | Node, - eventName: string, - listener: (event: AnyNativeEvent) => void, - listenerType: ListenerType, - ) => void, +function trapEventForPluginEventSystem( element: Document | Element | Node, topLevelType: DOMTopLevelEventType, - dispatch: Dispatcher, - rawEventName: string, - isLegacy: boolean, -) { - if (isLegacy) { - // Check if interactive and wrap in interactiveUpdates - const listener = bindDispatch(dispatch, topLevelType, PASSIVE_DISABLED); - // We don't listen for passive/non-passive - eventListener(element, rawEventName, listener, PASSIVE_DISABLED); + capture: boolean, +): void { + const dispatch = isInteractiveTopLevelEventType(topLevelType) + ? dispatchInteractiveEvent + : dispatchEvent; + const rawEventName = getRawEventName(topLevelType); + // Check if interactive and wrap in interactiveUpdates + const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM); + if (capture) { + addEventCaptureListener(element, rawEventName, listener); } else { - if (passiveBrowserEventsSupported) { - // Check if interactive and wrap in interactiveUpdates - const activeListener = bindDispatch( - dispatch, - topLevelType, - PASSIVE_FALSE, - ); - const passiveListener = bindDispatch( - dispatch, - topLevelType, - PASSIVE_TRUE, - ); - // We listen to the same event for both passive true/passive false. - // We do this so future event experiments can handle conditional logic. - // For example, we might want to support some derivative of both - // onMouseMovePassive and onMouseMoveActive, where the underlying logic - // is forked conditionally, depending on the event handler being - // passive or not. Furthermore, given we generally always listen to - // events on the root, we have have to anticipate that this might - // occur and listen to both ahead of time. - eventListener(element, rawEventName, passiveListener, PASSIVE_TRUE); - eventListener(element, rawEventName, activeListener, PASSIVE_FALSE); - } else { - const fallbackListener = bindDispatch( - dispatch, - topLevelType, - PASSIVE_FALLBACK, - ); - eventListener(element, rawEventName, fallbackListener, PASSIVE_FALLBACK); - } + addEventBubbleListener(element, rawEventName, listener); } } -function dispatchInteractiveEvent(topLevelType, listenerType, nativeEvent) { - interactiveUpdates(dispatchEvent, topLevelType, listenerType, nativeEvent); +function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) { + interactiveUpdates( + dispatchEvent, + topLevelType, + eventSystemFlags, + nativeEvent, + ); } export function dispatchEvent( topLevelType: DOMTopLevelEventType, - listenerType: ListenerType, + eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, ): void { if (!_enabled) { @@ -288,7 +253,7 @@ export function dispatchEvent( topLevelType, nativeEvent, targetInst, - listenerType, + eventSystemFlags, ); try { diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index 41fc6a4d74a7c..9d1b7f16dda73 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -169,7 +169,7 @@ const SelectEventPlugin = { const doc = getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. - if (!doc || !isListeningToAllDependencies('onSelect', doc, true)) { + if (!doc || !isListeningToAllDependencies('onSelect', doc)) { return null; } diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index da2b44cc63f03..ed3f124b6ffdb 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -7,13 +7,6 @@ * @flow */ -import { - type ListenerType, - PASSIVE_DISABLED, - PASSIVE_FALLBACK, - PASSIVE_TRUE, -} from 'events/ListenerTypes'; - const EventListenerWWW = require('EventListener'); import typeof * as EventListenerType from '../EventListener'; @@ -23,36 +16,18 @@ export function addEventBubbleListener( element: Element, eventType: string, listener: Function, - listenerType: ListenerType, + passive?: boolean, ): void { - if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { - EventListenerWWW.listen(element, eventType, listener); - } else { - EventListenerWWW.listen( - element, - eventType, - listener, - listenerType === PASSIVE_TRUE, - ); - } + EventListenerWWW.listen(element, eventType, listener, passive); } export function addEventCaptureListener( element: Element, eventType: string, listener: Function, - listenerType: ListenerType, + passive?: boolean, ): void { - if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) { - EventListenerWWW.capture(element, eventType, listener); - } else { - EventListenerWWW.capture( - element, - eventType, - listener, - listenerType === PASSIVE_TRUE, - ); - } + EventListenerWWW.capture(element, eventType, listener, passive); } // Flow magic to verify the exports of this file match the original version. diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 9686ec7442d42..06c39357fb29e 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -21,7 +21,7 @@ import lowPriorityWarning from 'shared/lowPriorityWarning'; import warningWithoutStack from 'shared/warningWithoutStack'; import {ELEMENT_NODE} from '../shared/HTMLNodeType'; import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes'; -import {PASSIVE_DISABLED} from 'events/ListenerTypes'; +import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags'; // for .act's return value type Thenable = { @@ -64,7 +64,7 @@ let hasWarnedAboutDeprecatedMockComponent = false; */ function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { fakeNativeEvent.target = node; - dispatchEvent(topLevelType, PASSIVE_DISABLED, fakeNativeEvent); + dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, fakeNativeEvent); } /** From 2acacf8a93276c7a903aed53efffc2830c897c80 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 19:32:53 +0000 Subject: [PATCH 16/19] Wrap function in flags --- .../src/events/ReactDOMEventListener.js | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index de2217d441f3a..e24d00f3d4ed8 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -31,6 +31,8 @@ import SimpleEventPlugin from './SimpleEventPlugin'; import {getRawEventName} from './DOMTopLevelEventTypes'; import {passiveBrowserEventsSupported} from './checkPassiveEvents'; +import {enableEventAPI} from 'shared/ReactFeatureFlags'; + const {isInteractiveTopLevelEventType} = SimpleEventPlugin; const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; @@ -169,33 +171,35 @@ export function trapEventForResponderEventSystem( capture: boolean, passive: boolean, ): void { - const dispatch = isInteractiveTopLevelEventType(topLevelType) - ? dispatchInteractiveEvent - : dispatchEvent; - const rawEventName = getRawEventName(topLevelType); - let eventFlags = RESPONDER_EVENT_SYSTEM; + if (enableEventAPI) { + const dispatch = isInteractiveTopLevelEventType(topLevelType) + ? dispatchInteractiveEvent + : dispatchEvent; + const rawEventName = getRawEventName(topLevelType); + let eventFlags = RESPONDER_EVENT_SYSTEM; - // If passive option is not supported, then the event will be - // active and not passive, but we flag it as using not being - // supported too. This way the responder event plugins know, - // and can provide polyfills if needed. - if (passive) { - if (passiveBrowserEventsSupported) { + // If passive option is not supported, then the event will be + // active and not passive, but we flag it as using not being + // supported too. This way the responder event plugins know, + // and can provide polyfills if needed. + if (passive) { + if (passiveBrowserEventsSupported) { + eventFlags |= IS_ACTIVE; + eventFlags |= PASSIVE_NOT_SUPPORTED; + passive = false; + } else { + eventFlags |= IS_PASSIVE; + } + } else { eventFlags |= IS_ACTIVE; - eventFlags |= PASSIVE_NOT_SUPPORTED; - passive = false; + } + // Check if interactive and wrap in interactiveUpdates + const listener = dispatch.bind(null, topLevelType, eventFlags); + if (capture) { + addEventCaptureListener(element, rawEventName, listener, passive); } else { - eventFlags |= IS_PASSIVE; + addEventBubbleListener(element, rawEventName, listener, passive); } - } else { - eventFlags |= IS_ACTIVE; - } - // Check if interactive and wrap in interactiveUpdates - const listener = dispatch.bind(null, topLevelType, eventFlags); - if (capture) { - addEventCaptureListener(element, rawEventName, listener, passive); - } else { - addEventBubbleListener(element, rawEventName, listener, passive); } } From 3b6725584867d84e7b98a6289414e2e08fd9d717 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 19:34:21 +0000 Subject: [PATCH 17/19] Remove more code that is not needed anymore --- .../__tests__/ReactBrowserEventEmitter-test.internal.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 023a4435513c5..2a6b526218c48 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -331,15 +331,15 @@ describe('ReactBrowserEventEmitter', () => { it('should listen to events only once', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1); }); it('should work with event plugins without dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( 'click', @@ -349,7 +349,7 @@ describe('ReactBrowserEventEmitter', () => { it('should work with event plugins with dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document, true); + ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); const setEventListeners = []; const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs(); From 5b044cf4bcf5ba48e9e939c248d4a346e2f50bc6 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 14 Mar 2019 19:36:05 +0000 Subject: [PATCH 18/19] Adds flag to another location --- packages/react-dom/src/events/checkPassiveEvents.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/events/checkPassiveEvents.js b/packages/react-dom/src/events/checkPassiveEvents.js index 8e061749e2d74..ad175b3a05bd5 100644 --- a/packages/react-dom/src/events/checkPassiveEvents.js +++ b/packages/react-dom/src/events/checkPassiveEvents.js @@ -8,12 +8,13 @@ */ import {canUseDOM} from 'shared/ExecutionEnvironment'; +import {enableEventAPI} from 'shared/ReactFeatureFlags'; export let passiveBrowserEventsSupported = false; // Check if browser support events with passive listeners // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support -if (canUseDOM) { +if (enableEventAPI && canUseDOM) { try { const options = { get passive() { From 8b34efad7e476d5c7d5cc8ca4919372f924d7737 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 15 Mar 2019 09:28:15 +0000 Subject: [PATCH 19/19] Address feedback on addEventListener --- .../react-dom/src/events/EventListener.js | 29 +++++++------------ .../src/events/ReactDOMEventListener.js | 15 ++++++---- .../src/events/forks/EventListener-www.js | 23 ++++++++++++--- scripts/flow/environment.js | 10 ++----- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 944246327ebb7..a44d160e7970f 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -11,30 +11,23 @@ export function addEventBubbleListener( element: Document | Element | Node, eventType: string, listener: Function, - passive?: boolean, ): void { - if (passive === undefined) { - element.addEventListener(eventType, listener, false); - } else { - element.addEventListener(eventType, listener, { - passive, - capture: false, - }); - } + element.addEventListener(eventType, listener, false); } export function addEventCaptureListener( element: Document | Element | Node, eventType: string, listener: Function, - passive?: boolean, ): void { - if (passive === undefined) { - element.addEventListener(eventType, listener, true); - } else { - element.addEventListener(eventType, listener, { - passive, - capture: true, - }); - } + element.addEventListener(eventType, listener, true); +} + +export function addEventListener( + element: Document | Element | Node, + eventType: string, + listener: Function, + options: {passive: boolean}, +): void { + element.addEventListener(eventType, listener, (options: any)); } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index e24d00f3d4ed8..32da3c6106147 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -24,7 +24,11 @@ import { PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; -import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; +import { + addEventBubbleListener, + addEventCaptureListener, + addEventListener, +} from './EventListener'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; @@ -195,11 +199,10 @@ export function trapEventForResponderEventSystem( } // Check if interactive and wrap in interactiveUpdates const listener = dispatch.bind(null, topLevelType, eventFlags); - if (capture) { - addEventCaptureListener(element, rawEventName, listener, passive); - } else { - addEventBubbleListener(element, rawEventName, listener, passive); - } + addEventListener(element, rawEventName, listener, { + capture, + passive, + }); } } diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index ed3f124b6ffdb..d217a52ce6309 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -12,22 +12,37 @@ const EventListenerWWW = require('EventListener'); import typeof * as EventListenerType from '../EventListener'; import typeof * as EventListenerShimType from './EventListener-www'; +const NORMAL_PRIORITY = 0; + export function addEventBubbleListener( element: Element, eventType: string, listener: Function, - passive?: boolean, ): void { - EventListenerWWW.listen(element, eventType, listener, passive); + EventListenerWWW.listen(element, eventType, listener); } export function addEventCaptureListener( element: Element, eventType: string, listener: Function, - passive?: boolean, ): void { - EventListenerWWW.capture(element, eventType, listener, passive); + EventListenerWWW.capture(element, eventType, listener); +} + +export function addEventListener( + element: Element, + eventType: string, + listener: Function, + options: {passive: boolean}, +): void { + EventListenerWWW.listen( + element, + eventType, + listener, + NORMAL_PRIORITY, + options, + ); } // Flow magic to verify the exports of this file match the original version. diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 407b995b07629..153e6b37a8c34 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -35,13 +35,9 @@ declare module 'EventListener' { target: Element, type: string, callback: Function, - passive?: boolean, - ) => mixed, - capture: ( - target: Element, - type: string, - callback: Function, - passive?: boolean, + priority?: number, + options?: {passive: boolean}, ) => mixed, + capture: (target: Element, type: string, callback: Function) => mixed, }; }