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/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/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 50f21ccf47d57..9c21c5b79aae0 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -253,7 +253,10 @@ 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; diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 67b9bcadd868b..a44d160e7970f 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -8,7 +8,7 @@ */ export function addEventBubbleListener( - element: Document | Element, + element: Document | Element | Node, eventType: string, listener: Function, ): void { @@ -16,9 +16,18 @@ export function addEventBubbleListener( } export function addEventCaptureListener( - element: Document | Element, + element: Document | Element | Node, eventType: string, listener: Function, ): void { 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/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index ea02bdc4a6aa2..2f9f29fcd3097 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,23 @@ import isEventSupported from './isEventSupported'; * React Core . General Purpose Event Plugin System */ -const alreadyListeningTo = {}; -let reactTopListenersCounter = 0; +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +const elementListeningSets: + | WeakMap + | Map< + Document | Element | Node, + Set, + > = new PossiblyWeakMap(); -/** - * To ensure no conflicts with other potential React instances on the page - */ -const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2); - -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 getListeningSetForElement( + element: Document | Element | Node, +): Set { + let listeningSet = elementListeningSets.get(element); + if (listeningSet === undefined) { + listeningSet = new Set(); + elementListeningSets.set(element, listeningSet); } - return alreadyListeningTo[mountAt[topListenersIDKey]]; + return listeningSet; } /** @@ -125,14 +127,14 @@ function getListeningForDocument(mountAt: any) { */ export function listenTo( registrationName: string, - mountAt: Document | Element, -) { - const isListening = getListeningForDocument(mountAt); + mountAt: Document | Element | Node, +): void { + const listeningSet = getListeningSetForElement(mountAt); 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)) { switch (dependency) { case TOP_SCROLL: trapCapturedEvent(TOP_SCROLL, mountAt); @@ -143,8 +145,8 @@ export function listenTo( 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; + listeningSet.add(TOP_BLUR); + listeningSet.add(TOP_FOCUS); break; case TOP_CANCEL: case TOP_CLOSE: @@ -167,7 +169,7 @@ export function listenTo( } break; } - isListening[dependency] = true; + listeningSet.add(dependency); } } } @@ -175,12 +177,13 @@ export function listenTo( export function isListeningToAllDependencies( registrationName: string, mountAt: Document | Element, -) { - const isListening = getListeningForDocument(mountAt); +): boolean { + const listeningSet = getListeningSetForElement(mountAt); 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..32da3c6106147 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -15,18 +15,41 @@ import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactWorkTags'; +import { + type EventSystemFlags, + PLUGIN_EVENT_SYSTEM, + RESPONDER_EVENT_SYSTEM, + IS_PASSIVE, + IS_ACTIVE, + 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'; import {getRawEventName} from './DOMTopLevelEventTypes'; +import {passiveBrowserEventsSupported} from './checkPassiveEvents'; + +import {enableEventAPI} from 'shared/ReactFeatureFlags'; const {isInteractiveTopLevelEventType} = SimpleEventPlugin; const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; const callbackBookkeepingPool = []; +type BookKeepingInstance = { + topLevelType: DOMTopLevelEventType | null, + nativeEvent: AnyNativeEvent | null, + targetInst: Fiber | null, + ancestors: Array, + eventSystemFlags: EventSystemFlags, +}; + /** * 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,20 +71,17 @@ function findRootContainerNode(inst) { // Used to store ancestor hierarchy in top level callback function getTopLevelCallbackBookKeeping( - topLevelType, - nativeEvent, - targetInst, -): { - topLevelType: ?DOMTopLevelEventType, - nativeEvent: ?AnyNativeEvent, + topLevelType: DOMTopLevelEventType, + nativeEvent: AnyNativeEvent, targetInst: Fiber | null, - ancestors: Array, -} { + eventSystemFlags: EventSystemFlags, +): BookKeepingInstance { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; instance.nativeEvent = nativeEvent; instance.targetInst = targetInst; + instance.eventSystemFlags = eventSystemFlags; return instance; } return { @@ -69,20 +89,24 @@ function getTopLevelCallbackBookKeeping( nativeEvent, targetInst, ancestors: [], + eventSystemFlags, }; } -function releaseTopLevelCallbackBookKeeping(instance) { +function releaseTopLevelCallbackBookKeeping( + instance: BookKeepingInstance, +): void { instance.topLevelType = null; instance.nativeEvent = null; instance.targetInst = null; instance.ancestors.length = 0; + instance.eventSystemFlags = 0; if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { callbackBookkeepingPool.push(instance); } } -function handleTopLevel(bookKeeping) { +function handleTopLevel(bookKeeping: BookKeepingInstance) { let targetInst = bookKeeping.targetInst; // Loop through the hierarchy, in case there's any nested components. @@ -92,7 +116,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); @@ -105,12 +130,17 @@ function handleTopLevel(bookKeeping) { for (let i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; - runExtractedEventsInBatch( - bookKeeping.topLevelType, - targetInst, - bookKeeping.nativeEvent, - getEventTarget(bookKeeping.nativeEvent), - ); + 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 + } } } @@ -125,70 +155,89 @@ 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; - } - const dispatch = isInteractiveTopLevelEventType(topLevelType) - ? dispatchInteractiveEvent - : dispatchEvent; - - addEventBubbleListener( - element, - getRawEventName(topLevelType), - // Check if interactive and wrap in interactiveUpdates - dispatch.bind(null, topLevelType), - ); + element: Document | Element | Node, +): void { + trapEventForPluginEventSystem(element, topLevelType, 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, +): void { + trapEventForPluginEventSystem(element, topLevelType, true); +} + +export function trapEventForResponderEventSystem( + element: Document | Element | Node, + topLevelType: DOMTopLevelEventType, + capture: boolean, + passive: boolean, +): void { + 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) { + 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); + addEventListener(element, rawEventName, listener, { + capture, + passive, + }); } +} + +function trapEventForPluginEventSystem( + element: Document | Element | Node, + topLevelType: DOMTopLevelEventType, + capture: boolean, +): void { const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; - - addEventCaptureListener( - element, - getRawEventName(topLevelType), - // Check if interactive and wrap in interactiveUpdates - dispatch.bind(null, topLevelType), - ); + 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 { + addEventBubbleListener(element, rawEventName, listener); + } } -function dispatchInteractiveEvent(topLevelType, nativeEvent) { - interactiveUpdates(dispatchEvent, topLevelType, nativeEvent); +function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) { + interactiveUpdates( + dispatchEvent, + topLevelType, + eventSystemFlags, + nativeEvent, + ); } export function dispatchEvent( topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, -) { +): void { if (!_enabled) { return; } @@ -211,6 +260,7 @@ export function dispatchEvent( topLevelType, nativeEvent, targetInst, + eventSystemFlags, ); try { diff --git a/packages/react-dom/src/events/checkPassiveEvents.js b/packages/react-dom/src/events/checkPassiveEvents.js new file mode 100644 index 0000000000000..ad175b3a05bd5 --- /dev/null +++ b/packages/react-dom/src/events/checkPassiveEvents.js @@ -0,0 +1,29 @@ +/** + * 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 + */ + +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 (enableEventAPI && canUseDOM) { + try { + const options = { + get passive() { + passiveBrowserEventsSupported = true; + }, + }; + window.addEventListener('test', options, options); + window.removeEventListener('test', options, options); + } catch (e) { + passiveBrowserEventsSupported = false; + } +} diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 4f70d179810d4..d217a52ce6309 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -12,6 +12,8 @@ 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, @@ -28,6 +30,21 @@ export function addEventCaptureListener( 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. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 9793b4c6e3c59..06c39357fb29e 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 {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags'; // for .act's return value type Thenable = { @@ -63,7 +64,7 @@ let hasWarnedAboutDeprecatedMockComponent = false; */ function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { fakeNativeEvent.target = node; - dispatchEvent(topLevelType, fakeNativeEvent); + dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, fakeNativeEvent); } /** diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 390f46af4ea4c..fdd42867b31d3 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -2523,9 +2523,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 @@ -2545,7 +2550,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..153e6b37a8c34 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -31,7 +31,13 @@ declare module 'ReactFiberErrorDialog' { // EventListener www fork declare module 'EventListener' { declare module.exports: { - listen: (target: Element, type: string, callback: Function) => mixed, + listen: ( + target: Element, + type: string, + callback: Function, + priority?: number, + options?: {passive: boolean}, + ) => mixed, capture: (target: Element, type: string, callback: Function) => mixed, }; }