diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 114565d342..0b377d16ec 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -14,7 +14,8 @@ let EventPluginRegistry; let React; let ReactDOM; let ReactDOMComponentTree; -let ReactBrowserEventEmitter; +let DOMEventPluginSystem; +let ReactDOMEventListener; let ReactTestUtils; let idCallOrder; @@ -52,18 +53,21 @@ function registerSimpleTestHandler() { return getListener(CHILD, ON_CLICK_KEY); } +// We should probably remove this file at some point, it's just full of +// internal API usage. ReactBrowserEventEmitter was refactored out in +// #18056 too. The majority of this code lives in DOMEventPluginSystem. describe('ReactBrowserEventEmitter', () => { beforeEach(() => { jest.resetModules(); LISTENER.mockClear(); - // TODO: can we express this test with only public API? EventPluginGetListener = require('legacy-events/getListener').default; EventPluginRegistry = require('legacy-events/EventPluginRegistry'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); - ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter'); + DOMEventPluginSystem = require('../events/DOMEventPluginSystem'); + ReactDOMEventListener = require('../events/ReactDOMEventListener'); ReactTestUtils = require('react-dom/test-utils'); container = document.createElement('div'); @@ -177,12 +181,12 @@ describe('ReactBrowserEventEmitter', () => { expect(LISTENER).toHaveBeenCalledTimes(1); }); - it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => { + it('should not invoke handlers if ReactDOMEventListener is disabled', () => { registerSimpleTestHandler(); - ReactBrowserEventEmitter.setEnabled(false); + ReactDOMEventListener.setEnabled(false); CHILD.click(); expect(LISTENER).toHaveBeenCalledTimes(0); - ReactBrowserEventEmitter.setEnabled(true); + ReactDOMEventListener.setEnabled(true); CHILD.click(); expect(LISTENER).toHaveBeenCalledTimes(1); }); @@ -346,15 +350,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); + DOMEventPluginSystem.listenToEvent(ON_CLICK_KEY, document); + DOMEventPluginSystem.listenToEvent(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); + DOMEventPluginSystem.listenToEvent(ON_CLICK_KEY, document); expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( 'click', @@ -364,7 +368,7 @@ describe('ReactBrowserEventEmitter', () => { it('should work with event plugins with dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); + DOMEventPluginSystem.listenToEvent(ON_CHANGE_KEY, document); 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 e5111ae603..aa01964f9b 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -57,14 +57,11 @@ import { TOP_SUBMIT, TOP_TOGGLE, } from '../events/DOMTopLevelEventTypes'; -import { - listenTo, - trapBubbledEvent, - getListenerMapForElement, -} from '../events/ReactBrowserEventEmitter'; +import {getListenerMapForElement} from '../events/DOMEventListenerMap'; import { addResponderEventSystemEvent, removeActiveResponderEventSystemEvent, + trapBubbledEvent, } from '../events/ReactDOMEventListener.js'; import {mediaEventTypes} from '../events/DOMTopLevelEventTypes'; import { @@ -90,6 +87,7 @@ import { enableDeprecatedFlareAPI, enableTrustedTypesIntegration, } from 'shared/ReactFeatureFlags'; +import {listenToEvent} from '../events/DOMEventPluginSystem'; let didWarnInvalidHydration = false; let didWarnShadyDOM = false; @@ -274,7 +272,7 @@ function ensureListeningTo( const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; - listenTo(registrationName, doc); + listenToEvent(registrationName, doc); } function getOwnerDocumentFromRootContainer( diff --git a/packages/react-dom/src/client/ReactDOMFB.js b/packages/react-dom/src/client/ReactDOMFB.js index 5acb45d81e..02403919e7 100644 --- a/packages/react-dom/src/client/ReactDOMFB.js +++ b/packages/react-dom/src/client/ReactDOMFB.js @@ -10,7 +10,7 @@ import {addUserTimingListener} from 'shared/ReactFeatureFlags'; import ReactDOM from './ReactDOM'; -import {isEnabled} from '../events/ReactBrowserEventEmitter'; +import {isEnabled} from '../events/ReactDOMEventListener'; import {getClosestInstanceFromNode} from './ReactDOMComponentTree'; if (__EXPERIMENTAL__) { diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 2e90978b31..0cf8db9da9 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -34,7 +34,7 @@ import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting'; import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, -} from '../events/ReactBrowserEventEmitter'; +} from '../events/ReactDOMEventListener'; import {getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, diff --git a/packages/react-dom/src/events/DOMEventListenerMap.js b/packages/react-dom/src/events/DOMEventListenerMap.js new file mode 100644 index 0000000000..b951eed1b6 --- /dev/null +++ b/packages/react-dom/src/events/DOMEventListenerMap.js @@ -0,0 +1,31 @@ +/** + * 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 type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; + +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +// prettier-ignore +const elementListenerMap: + // $FlowFixMe Work around Flow bug + | WeakMap + | Map< + Document | Element | Node, + Map void)>, + > = new PossiblyWeakMap(); + +export function getListenerMapForElement( + element: Document | Element | Node, +): Map void)> { + let listenerMap = elementListenerMap.get(element); + if (listenerMap === undefined) { + listenerMap = new Map(); + elementListenerMap.set(element, listenerMap); + } + return listenerMap; +} diff --git a/packages/react-dom/src/events/DOMEventPluginSystem.js b/packages/react-dom/src/events/DOMEventPluginSystem.js index 40190f30e3..ad8b38f794 100644 --- a/packages/react-dom/src/events/DOMEventPluginSystem.js +++ b/packages/react-dom/src/events/DOMEventPluginSystem.js @@ -21,9 +21,81 @@ import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; import {runEventsInBatch} from 'legacy-events/EventBatching'; import {plugins} from 'legacy-events/EventPluginRegistry'; import accumulateInto from 'legacy-events/accumulateInto'; +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import {trapCapturedEvent, trapBubbledEvent} from './ReactDOMEventListener'; +import {getListenerMapForElement} from './DOMEventListenerMap'; +import isEventSupported from './isEventSupported'; +import { + TOP_BLUR, + TOP_CANCEL, + TOP_CLOSE, + TOP_FOCUS, + TOP_INVALID, + TOP_RESET, + TOP_SCROLL, + TOP_SUBMIT, + getRawEventName, + mediaEventTypes, +} from './DOMTopLevelEventTypes'; + +/** + * Summary of `DOMEventPluginSystem` event handling: + * + * - Top-level delegation is used to trap most native browser events. This + * may only occur in the main thread and is the responsibility of + * ReactDOMEventListener, which is injected and can therefore support + * pluggable event sources. This is the only work that occurs in the main + * thread. + * + * - We normalize and de-duplicate events to account for browser quirks. This + * may be done in the worker thread. + * + * - Forward these native events (with the associated top-level type used to + * trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want + * to extract any synthetic events. + * + * - The `EventPluginRegistry` will then process each event by annotating them with + * "dispatches", a sequence of listeners and IDs that care about that event. + * + * - The `EventPluginRegistry` then dispatches the events. + * + * Overview of React and the event system: + * + * +------------+ . + * | DOM | . + * +------------+ . + * | . + * v . + * +------------+ . + * | ReactEvent | . + * | Listener | . + * +------------+ . +-----------+ + * | . +--------+|SimpleEvent| + * | . | |Plugin | + * +-----|------+ . v +-----------+ + * | | | . +--------------+ +------------+ + * | +-----------.--->|PluginRegistry| | Event | + * | | . | | +-----------+ | Propagators| + * | ReactEvent | . | | |TapEvent | |------------| + * | Emitter | . | |<---+|Plugin | |other plugin| + * | | . | | +-----------+ | utilities | + * | +-----------.--->| | +------------+ + * | | | . +--------------+ + * +-----|------+ . ^ +-----------+ + * | . | |Enter/Leave| + * + . +-------+|Plugin | + * +-------------+ . +-----------+ + * | application | . + * |-------------| . + * | | . + * | | . + * +-------------+ . + * . + * React Core . General Purpose Event Plugin System + */ const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; const callbackBookkeepingPool = []; @@ -213,3 +285,97 @@ export function dispatchEventForPluginEventSystem( releaseTopLevelCallbackBookKeeping(bookKeeping); } } + +/** + * We listen for bubbled touch events on the document object. + * + * Firefox v8.01 (and possibly others) exhibited strange behavior when + * mounting `onmousemove` events at some node that was not the document + * element. The symptoms were that if your mouse is not moving over something + * contained within that mount point (for example on the background) the + * top-level listeners for `onmousemove` won't be called. However, if you + * register the `mousemove` on the document object, then it will of course + * catch all `mousemove`s. This along with iOS quirks, justifies restricting + * top-level listeners to the document object only, at least for these + * movement types of events and possibly all events. + * + * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html + * + * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but + * they bubble to document. + * + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @param {object} mountAt Container where to mount the listener + */ +export function listenToEvent( + registrationName: string, + mountAt: Document | Element | Node, +): void { + const listeningSet = getListenerMapForElement(mountAt); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + listenToTopLevelEvent(dependency, mountAt, listeningSet); + } +} + +export function listenToTopLevelEvent( + topLevelType: DOMTopLevelEventType, + mountAt: Document | Element | Node, + listenerMap: Map void)>, +): void { + if (!listenerMap.has(topLevelType)) { + switch (topLevelType) { + 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. + listenerMap.set(TOP_BLUR, null); + listenerMap.set(TOP_FOCUS, null); + break; + case TOP_CANCEL: + case TOP_CLOSE: + if (isEventSupported(getRawEventName(topLevelType))) { + trapCapturedEvent(topLevelType, 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(topLevelType) !== -1; + if (!isMediaEvent) { + trapBubbledEvent(topLevelType, mountAt); + } + break; + } + listenerMap.set(topLevelType, null); + } +} + +export function isListeningToAllDependencies( + registrationName: string, + mountAt: Document | Element, +): boolean { + const listenerMap = getListenerMapForElement(mountAt); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + if (!listenerMap.has(dependency)) { + return false; + } + } + return true; +} diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js deleted file mode 100644 index 3cc7aae2f2..0000000000 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ /dev/null @@ -1,203 +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 - */ - -import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; -import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; -import { - TOP_BLUR, - TOP_CANCEL, - TOP_CLOSE, - TOP_FOCUS, - TOP_INVALID, - TOP_RESET, - TOP_SCROLL, - TOP_SUBMIT, - getRawEventName, - mediaEventTypes, -} from './DOMTopLevelEventTypes'; -import { - setEnabled, - isEnabled, - trapBubbledEvent, - trapCapturedEvent, -} from './ReactDOMEventListener'; -import isEventSupported from './isEventSupported'; - -/** - * Summary of `ReactBrowserEventEmitter` event handling: - * - * - Top-level delegation is used to trap most native browser events. This - * may only occur in the main thread and is the responsibility of - * ReactDOMEventListener, which is injected and can therefore support - * pluggable event sources. This is the only work that occurs in the main - * thread. - * - * - We normalize and de-duplicate events to account for browser quirks. This - * may be done in the worker thread. - * - * - Forward these native events (with the associated top-level type used to - * trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want - * to extract any synthetic events. - * - * - The `EventPluginRegistry` will then process each event by annotating them with - * "dispatches", a sequence of listeners and IDs that care about that event. - * - * - The `EventPluginRegistry` then dispatches the events. - * - * Overview of React and the event system: - * - * +------------+ . - * | DOM | . - * +------------+ . - * | . - * v . - * +------------+ . - * | ReactEvent | . - * | Listener | . - * +------------+ . +-----------+ - * | . +--------+|SimpleEvent| - * | . | |Plugin | - * +-----|------+ . v +-----------+ - * | | | . +--------------+ +------------+ - * | +-----------.--->|PluginRegistry| | Event | - * | | . | | +-----------+ | Propagators| - * | ReactEvent | . | | |TapEvent | |------------| - * | Emitter | . | |<---+|Plugin | |other plugin| - * | | . | | +-----------+ | utilities | - * | +-----------.--->| | +------------+ - * | | | . +--------------+ - * +-----|------+ . ^ +-----------+ - * | . | |Enter/Leave| - * + . +-------+|Plugin | - * +-------------+ . +-----------+ - * | application | . - * |-------------| . - * | | . - * | | . - * +-------------+ . - * . - * React Core . General Purpose Event Plugin System - */ - -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -// prettier-ignore -const elementListenerMap: - // $FlowFixMe Work around Flow bug - | WeakMap - | Map< - Document | Element | Node, - Map void)>, - > = new PossiblyWeakMap(); - -export function getListenerMapForElement( - element: Document | Element | Node, -): Map void)> { - let listenerMap = elementListenerMap.get(element); - if (listenerMap === undefined) { - listenerMap = new Map(); - elementListenerMap.set(element, listenerMap); - } - return listenerMap; -} - -/** - * We listen for bubbled touch events on the document object. - * - * Firefox v8.01 (and possibly others) exhibited strange behavior when - * mounting `onmousemove` events at some node that was not the document - * element. The symptoms were that if your mouse is not moving over something - * contained within that mount point (for example on the background) the - * top-level listeners for `onmousemove` won't be called. However, if you - * register the `mousemove` on the document object, then it will of course - * catch all `mousemove`s. This along with iOS quirks, justifies restricting - * top-level listeners to the document object only, at least for these - * movement types of events and possibly all events. - * - * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - * - * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but - * they bubble to document. - * - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {object} mountAt Container where to mount the listener - */ -export function listenTo( - registrationName: string, - mountAt: Document | Element | Node, -): void { - const listeningSet = getListenerMapForElement(mountAt); - const dependencies = registrationNameDependencies[registrationName]; - - for (let i = 0; i < dependencies.length; i++) { - const dependency = dependencies[i]; - listenToTopLevel(dependency, mountAt, listeningSet); - } -} - -export function listenToTopLevel( - topLevelType: DOMTopLevelEventType, - mountAt: Document | Element | Node, - listenerMap: Map void)>, -): void { - if (!listenerMap.has(topLevelType)) { - switch (topLevelType) { - 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. - listenerMap.set(TOP_BLUR, null); - listenerMap.set(TOP_FOCUS, null); - break; - case TOP_CANCEL: - case TOP_CLOSE: - if (isEventSupported(getRawEventName(topLevelType))) { - trapCapturedEvent(topLevelType, 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(topLevelType) !== -1; - if (!isMediaEvent) { - trapBubbledEvent(topLevelType, mountAt); - } - break; - } - listenerMap.set(topLevelType, null); - } -} - -export function isListeningToAllDependencies( - registrationName: string, - mountAt: Document | Element, -): boolean { - const listenerMap = getListenerMapForElement(mountAt); - const dependencies = registrationNameDependencies[registrationName]; - - for (let i = 0; i < dependencies.length; i++) { - const dependency = dependencies[i]; - if (!listenerMap.has(dependency)) { - return false; - } - } - return true; -} - -export {setEnabled, isEnabled, trapBubbledEvent, trapCapturedEvent}; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index b0c999caf4..36a78b5d0f 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -32,10 +32,7 @@ import { attemptToDispatchEvent, addResponderEventSystemEvent, } from './ReactDOMEventListener'; -import { - getListenerMapForElement, - listenToTopLevel, -} from './ReactBrowserEventEmitter'; +import {getListenerMapForElement} from './DOMEventListenerMap'; import { getInstanceFromNode, getClosestInstanceFromNode, @@ -120,6 +117,7 @@ import { TOP_BLUR, } from './DOMTopLevelEventTypes'; import {IS_REPLAYED} from 'legacy-events/EventSystemFlags'; +import {listenToTopLevelEvent} from './DOMEventPluginSystem'; type QueuedReplayableEvent = {| blockedOn: null | Container | SuspenseInstance, @@ -217,7 +215,7 @@ function trapReplayableEvent( document: Document, listenerMap: Map void)>, ) { - listenToTopLevel(topLevelType, document, listenerMap); + listenToTopLevelEvent(topLevelType, document, listenerMap); if (enableDeprecatedFlareAPI) { // Trap events for the responder system. const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType); diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index b358e896ea..2c3da14bbe 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -22,11 +22,11 @@ import { TOP_MOUSE_UP, TOP_SELECTION_CHANGE, } from './DOMTopLevelEventTypes'; -import {isListeningToAllDependencies} from './ReactBrowserEventEmitter'; import getActiveElement from '../client/getActiveElement'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; +import {isListeningToAllDependencies} from './DOMEventPluginSystem'; const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11;