From 62861bbcc752c63728592fdc45078854f9aae161 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 2 Mar 2020 10:59:07 +0000 Subject: [PATCH] More event system cleanup and scaffolding (#18179) --- packages/legacy-events/PluginModuleType.js | 2 +- .../react-dom/src/client/ReactDOMComponent.js | 140 ++++++++++++----- .../src/events/DOMEventListenerMap.js | 18 +++ .../src/events/DOMLegacyEventPluginSystem.js | 40 +++-- .../src/events/DOMModernPluginEventSystem.js | 127 +++++++++++++++ .../src/events/ReactDOMEventListener.js | 144 ++++++++++-------- .../src/events/ReactDOMEventReplaying.js | 42 ++++- .../react-dom/src/events/SelectEventPlugin.js | 2 +- .../src/events/forks/EventListener-www.js | 12 +- scripts/error-codes/codes.json | 3 +- 10 files changed, 396 insertions(+), 134 deletions(-) create mode 100644 packages/react-dom/src/events/DOMModernPluginEventSystem.js diff --git a/packages/legacy-events/PluginModuleType.js b/packages/legacy-events/PluginModuleType.js index df353f2f3cc6b..988fdd3296c4d 100644 --- a/packages/legacy-events/PluginModuleType.js +++ b/packages/legacy-events/PluginModuleType.js @@ -29,7 +29,7 @@ export type PluginModule = { nativeTarget: NativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, - container?: Document | Element | Node, + container?: Document | Element, ) => ?ReactSyntheticEvent, tapMoveThreshold?: number, }; diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index cdf953207d0d1..5cc6981d82aad 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -10,6 +10,7 @@ import {registrationNameModules} from 'legacy-events/EventPluginRegistry'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import endsWith from 'shared/endsWith'; +import invariant from 'shared/invariant'; import {setListenToResponderEventTypes} from '../events/DeprecatedDOMEventResponderSystem'; import { @@ -59,7 +60,6 @@ import {getListenerMapForElement} from '../events/DOMEventListenerMap'; import { addResponderEventSystemEvent, removeActiveResponderEventSystemEvent, - trapBubbledEvent, } from '../events/ReactDOMEventListener.js'; import {mediaEventTypes} from '../events/DOMTopLevelEventTypes'; import { @@ -74,7 +74,12 @@ import { shouldRemoveAttribute, } from '../shared/DOMProperty'; import assertValidProps from '../shared/assertValidProps'; -import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; +import { + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, + ELEMENT_NODE, + COMMENT_NODE, +} from '../shared/HTMLNodeType'; import isCustomComponent from '../shared/isCustomComponent'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -84,8 +89,13 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO import { enableDeprecatedFlareAPI, enableTrustedTypesIntegration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; -import {legacyListenToEvent} from '../events/DOMLegacyEventPluginSystem'; +import { + legacyListenToEvent, + legacyTrapBubbledEvent, +} from '../events/DOMLegacyEventPluginSystem'; +import {listenToEvent} from '../events/DOMModernPluginEventSystem'; let didWarnInvalidHydration = false; let didWarnScriptTags = false; @@ -260,16 +270,36 @@ if (__DEV__) { } function ensureListeningTo( - rootContainerElement: Element | Node, + rootContainerInstance: Element | Node, registrationName: string, ): void { - const isDocumentOrFragment = - rootContainerElement.nodeType === DOCUMENT_NODE || - rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; - const doc = isDocumentOrFragment - ? rootContainerElement - : rootContainerElement.ownerDocument; - legacyListenToEvent(registrationName, doc); + if (enableModernEventSystem) { + // If we have a comment node, then use the parent node, + // which should be an element. + const rootContainerElement = + rootContainerInstance.nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + // Containers can only ever be element nodes. We do not + // want to register events to document fragments or documents + // with the modern plugin event system. + invariant( + rootContainerElement != null && + rootContainerElement.nodeType === ELEMENT_NODE, + 'ensureListeningTo(): received a container that was not an element node. ' + + 'This is likely a bug in React.', + ); + listenToEvent(registrationName, ((rootContainerElement: any): Element)); + } else { + // Legacy plugin event system path + const isDocumentOrFragment = + rootContainerInstance.nodeType === DOCUMENT_NODE || + rootContainerInstance.nodeType === DOCUMENT_FRAGMENT_NODE; + const doc = isDocumentOrFragment + ? rootContainerInstance + : rootContainerInstance.ownerDocument; + legacyListenToEvent(registrationName, ((doc: any): Document)); + } } function getOwnerDocumentFromRootContainer( @@ -514,41 +544,55 @@ export function setInitialProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + legacyTrapBubbledEvent(mediaEventTypes[i], domElement); + } } props = rawProps; break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + } props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_RESET, domElement); + legacyTrapBubbledEvent(TOP_SUBMIT, domElement); + } props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_TOGGLE, domElement); + } props = rawProps; break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); props = ReactDOMInputGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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'); @@ -560,7 +604,9 @@ export function setInitialProperties( case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); props = ReactDOMSelectGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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'); @@ -568,7 +614,9 @@ export function setInitialProperties( case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); props = ReactDOMTextareaGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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'); @@ -898,34 +946,48 @@ export function diffHydratedProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + legacyTrapBubbledEvent(mediaEventTypes[i], domElement); + } } break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + } break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_RESET, domElement); + legacyTrapBubbledEvent(TOP_SUBMIT, domElement); + } break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_TOGGLE, domElement); + } break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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'); @@ -935,14 +997,18 @@ export function diffHydratedProperties( break; case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(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/DOMEventListenerMap.js b/packages/react-dom/src/events/DOMEventListenerMap.js index b951eed1b61a1..f087a793d904c 100644 --- a/packages/react-dom/src/events/DOMEventListenerMap.js +++ b/packages/react-dom/src/events/DOMEventListenerMap.js @@ -9,6 +9,8 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; + const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; // prettier-ignore const elementListenerMap: @@ -29,3 +31,19 @@ export function getListenerMapForElement( } return listenerMap; } + +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/DOMLegacyEventPluginSystem.js b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js index 2897d3b42fef6..d7881ac6903a8 100644 --- a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js +++ b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js @@ -25,7 +25,6 @@ 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 { @@ -40,6 +39,7 @@ import { getRawEventName, mediaEventTypes, } from './DOMTopLevelEventTypes'; +import {trapEventForPluginEventSystem} from './ReactDOMEventListener'; /** * Summary of `DOMEventPluginSystem` event handling: @@ -309,7 +309,7 @@ export function dispatchEventForLegacyPluginEventSystem( */ export function legacyListenToEvent( registrationName: string, - mountAt: Document | Element | Node, + mountAt: Document | Element, ): void { const listenerMap = getListenerMapForElement(mountAt); const dependencies = registrationNameDependencies[registrationName]; @@ -322,18 +322,18 @@ export function legacyListenToEvent( export function legacyListenToTopLevelEvent( topLevelType: DOMTopLevelEventType, - mountAt: Document | Element | Node, + mountAt: Document | Element, listenerMap: Map void)>, ): void { if (!listenerMap.has(topLevelType)) { switch (topLevelType) { case TOP_SCROLL: - trapCapturedEvent(TOP_SCROLL, mountAt); + legacyTrapCapturedEvent(TOP_SCROLL, mountAt); break; case TOP_FOCUS: case TOP_BLUR: - trapCapturedEvent(TOP_FOCUS, mountAt); - trapCapturedEvent(TOP_BLUR, mountAt); + legacyTrapCapturedEvent(TOP_FOCUS, mountAt); + legacyTrapCapturedEvent(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); @@ -342,7 +342,7 @@ export function legacyListenToTopLevelEvent( case TOP_CANCEL: case TOP_CLOSE: if (isEventSupported(getRawEventName(topLevelType))) { - trapCapturedEvent(topLevelType, mountAt); + legacyTrapCapturedEvent(topLevelType, mountAt); } break; case TOP_INVALID: @@ -356,7 +356,7 @@ export function legacyListenToTopLevelEvent( // Media events don't bubble so adding the listener wouldn't do anything. const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1; if (!isMediaEvent) { - trapBubbledEvent(topLevelType, mountAt); + legacyTrapBubbledEvent(topLevelType, mountAt); } break; } @@ -364,18 +364,16 @@ export function legacyListenToTopLevelEvent( } } -export function isListeningToAllDependencies( - registrationName: string, - mountAt: Document | Element, -): boolean { - const listenerMap = getListenerMapForElement(mountAt); - const dependencies = registrationNameDependencies[registrationName]; +export function legacyTrapBubbledEvent( + topLevelType: DOMTopLevelEventType, + element: Document | Element, +): void { + trapEventForPluginEventSystem(element, topLevelType, false); +} - for (let i = 0; i < dependencies.length; i++) { - const dependency = dependencies[i]; - if (!listenerMap.has(dependency)) { - return false; - } - } - return true; +export function legacyTrapCapturedEvent( + topLevelType: DOMTopLevelEventType, + element: Document | Element, +): void { + trapEventForPluginEventSystem(element, topLevelType, true); } diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js new file mode 100644 index 0000000000000..aeeba5933d6a6 --- /dev/null +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -0,0 +1,127 @@ +/** + * 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 {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; + +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; + +import {trapEventForPluginEventSystem} from './ReactDOMEventListener'; +import {getListenerMapForElement} from './DOMEventListenerMap'; +import { + TOP_FOCUS, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_INVALID, + TOP_BLUR, + TOP_SCROLL, + TOP_CLOSE, + TOP_RESET, + TOP_SUBMIT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_WAITING, + TOP_VOLUME_CHANGE, + TOP_TIME_UPDATE, + TOP_SUSPEND, + TOP_STALLED, + TOP_SEEKING, + TOP_SEEKED, + TOP_PLAY, + TOP_PAUSE, + TOP_LOAD_START, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_RATE_CHANGE, + TOP_PROGRESS, + TOP_PLAYING, +} from './DOMTopLevelEventTypes'; + +const capturePhaseEvents = new Set([ + TOP_FOCUS, + TOP_BLUR, + TOP_SCROLL, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_CLOSE, + TOP_INVALID, + TOP_RESET, + TOP_SUBMIT, + TOP_ABORT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_LOAD_START, + TOP_PAUSE, + TOP_PLAY, + TOP_PLAYING, + TOP_PROGRESS, + TOP_RATE_CHANGE, + TOP_SEEKED, + TOP_SEEKING, + TOP_STALLED, + TOP_SUSPEND, + TOP_TIME_UPDATE, + TOP_VOLUME_CHANGE, + TOP_WAITING, +]); + +export function listenToTopLevelEvent( + topLevelType: DOMTopLevelEventType, + rootContainerElement: Element, + listenerMap: Map void)>, +): void { + if (!listenerMap.has(topLevelType)) { + const isCapturePhase = capturePhaseEvents.has(topLevelType); + trapEventForPluginEventSystem( + rootContainerElement, + topLevelType, + isCapturePhase, + ); + listenerMap.set(topLevelType, null); + } +} + +export function listenToEvent( + registrationName: string, + rootContainerElement: Element, +): void { + const listenerMap = getListenerMapForElement(rootContainerElement); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + listenToTopLevelEvent(dependency, rootContainerElement, listenerMap); + } +} + +export function dispatchEventForPluginEventSystem( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, + rootContainer: Document | Element, +): void { + // TODO +} diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index c63dc0ee648dd..e6913dbeed5b2 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -53,7 +53,10 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import {getRawEventName} from './DOMTopLevelEventTypes'; import {passiveBrowserEventsSupported} from './checkPassiveEvents'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; +import { + enableDeprecatedFlareAPI, + enableModernEventSystem, +} from 'shared/ReactFeatureFlags'; import { UserBlockingEvent, ContinuousEvent, @@ -61,6 +64,7 @@ import { } from 'shared/ReactTypes'; import {getEventPriorityForPluginSystem} from './DOMEventProperties'; import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem'; +import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem'; const { unstable_UserBlockingPriority: UserBlockingPriority, @@ -78,20 +82,6 @@ export function isEnabled() { return _enabled; } -export function trapBubbledEvent( - topLevelType: DOMTopLevelEventType, - element: Document | Element | Node, -): void { - trapEventForPluginEventSystem(element, topLevelType, false); -} - -export function trapCapturedEvent( - topLevelType: DOMTopLevelEventType, - element: Document | Element | Node, -): void { - trapEventForPluginEventSystem(element, topLevelType, true); -} - export function addResponderEventSystemEvent( document: Document, topLevelType: string, @@ -149,39 +139,31 @@ export function removeActiveResponderEventSystemEvent( } } -function trapEventForPluginEventSystem( - container: Document | Element | Node, +export function trapEventForPluginEventSystem( + container: Document | Element, topLevelType: DOMTopLevelEventType, capture: boolean, ): void { let listener; + let listenerWrapper; switch (getEventPriorityForPluginSystem(topLevelType)) { case DiscreteEvent: - listener = dispatchDiscreteEvent.bind( - null, - topLevelType, - PLUGIN_EVENT_SYSTEM, - container, - ); + listenerWrapper = dispatchDiscreteEvent; break; case UserBlockingEvent: - listener = dispatchUserBlockingUpdate.bind( - null, - topLevelType, - PLUGIN_EVENT_SYSTEM, - container, - ); + listenerWrapper = dispatchUserBlockingUpdate; break; case ContinuousEvent: default: - listener = dispatchEvent.bind( - null, - topLevelType, - PLUGIN_EVENT_SYSTEM, - container, - ); + listenerWrapper = dispatchEvent; break; } + listener = listenerWrapper.bind( + null, + topLevelType, + PLUGIN_EVENT_SYSTEM, + container, + ); const rawEventName = getRawEventName(topLevelType); if (capture) { @@ -228,7 +210,7 @@ function dispatchUserBlockingUpdate( export function dispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): void { if (!_enabled) { @@ -293,12 +275,22 @@ export function dispatchEvent( // in case the event system needs to trace it. if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -311,12 +303,22 @@ export function dispatchEvent( ); } } else { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } } @@ -324,7 +326,7 @@ export function dispatchEvent( export function attemptToDispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): null | Container | SuspenseInstance { // TODO: Warn if _enabled is false. @@ -372,12 +374,22 @@ export function attemptToDispatchEvent( if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -390,12 +402,22 @@ export function attemptToDispatchEvent( ); } } else { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } // We're not blocked on anything. return null; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 096a87aedae4d..a72fcdd2c5cde 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -16,6 +16,7 @@ import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; import { enableDeprecatedFlareAPI, enableSelectiveHydration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; import { unstable_runWithPriority as runWithPriority, @@ -118,13 +119,14 @@ import { } from './DOMTopLevelEventTypes'; import {IS_REPLAYED} from 'legacy-events/EventSystemFlags'; import {legacyListenToTopLevelEvent} from './DOMLegacyEventPluginSystem'; +import {listenToTopLevelEvent} from './DOMModernPluginEventSystem'; type QueuedReplayableEvent = {| blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, - container: Document | Element | Node, + container: Document | Element, |}; let hasScheduledReplayAttempt = false; @@ -211,12 +213,22 @@ export function isReplayableDiscreteEvent( return discreteReplayableEvents.indexOf(eventType) > -1; } +function trapReplayableEventForContainer( + topLevelType: DOMTopLevelEventType, + container: Container, + listenerMap: Map void)>, +) { + listenToTopLevelEvent(topLevelType, ((container: any): Element), listenerMap); +} + function trapReplayableEventForDocument( topLevelType: DOMTopLevelEventType, document: Document, listenerMap: Map void)>, ) { - legacyListenToTopLevelEvent(topLevelType, document, listenerMap); + if (!enableModernEventSystem) { + legacyListenToTopLevelEvent(topLevelType, document, listenerMap); + } if (enableDeprecatedFlareAPI) { // Trap events for the responder system. const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType); @@ -241,12 +253,30 @@ export function eagerlyTrapReplayableEvents( document: Document, ) { const listenerMapForDoc = getListenerMapForElement(document); + let listenerMapForContainer; + if (enableModernEventSystem) { + listenerMapForContainer = getListenerMapForElement(container); + } // Discrete discreteReplayableEvents.forEach(topLevelType => { + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); // Continuous continuousReplayableEvents.forEach(topLevelType => { + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); } @@ -255,7 +285,7 @@ function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): QueuedReplayableEvent { return { @@ -271,7 +301,7 @@ export function queueDiscreteEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): void { const queuedEvent = createQueuedReplayableEvent( @@ -346,7 +376,7 @@ function accumulateOrCreateContinuousQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): QueuedReplayableEvent { if ( @@ -381,7 +411,7 @@ export function queueIfContinuousEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, - container: Document | Element | Node, + container: Document | Element, nativeEvent: AnyNativeEvent, ): boolean { // These set relatedTarget to null because the replayed event will be treated as if we diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index a370b225f28d2..2cb52b4dfedbf 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -26,7 +26,7 @@ import getActiveElement from '../client/getActiveElement'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; -import {isListeningToAllDependencies} from './DOMLegacyEventPluginSystem'; +import {isListeningToAllDependencies} from './DOMEventListenerMap'; const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11; diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 99b337f0e7805..b76b9d2e390c1 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -16,16 +16,16 @@ export function addEventBubbleListener( element: Element, eventType: string, listener: Function, -): void { - EventListenerWWW.listen(element, eventType, listener); +) { + return EventListenerWWW.listen(element, eventType, listener); } export function addEventCaptureListener( element: Element, eventType: string, listener: Function, -): void { - EventListenerWWW.capture(element, eventType, listener); +) { + return EventListenerWWW.capture(element, eventType, listener); } export function addEventCaptureListenerWithPassiveFlag( @@ -33,8 +33,8 @@ export function addEventCaptureListenerWithPassiveFlag( eventType: string, listener: Function, passive: boolean, -): void { - EventListenerWWW.captureWithPassiveFlag( +) { + return EventListenerWWW.captureWithPassiveFlag( element, eventType, listener, diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index dad5a7be3c7d2..9e85caa7c7475 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -345,5 +345,6 @@ "344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.", "345": "Root did not complete. This is a bug in React.", "346": "An event responder context was used outside of an event cycle.", - "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead." + "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead.", + "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React." }