diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
index 4d2ba7b69f..410e1006ed 100644
--- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
@@ -349,13 +349,10 @@ describe('ReactDOMEventListener', () => {
}),
);
// As of the modern event system refactor, we now support
- // this on
. The reason for this, is because we now
- // attach all media events to the "root" or "portal" in the
- // capture phase, rather than the bubble phase. This allows
- // us to assign less event listeners to individual elements,
- // which also nicely allows us to support more without needing
- // to add more individual code paths to support various
- // events that do not bubble.
+ // this on
. The reason for this, is because we allow
+ // events to be attached to nodes regardless of if they
+ // necessary support them. This is a strange test, as this
+ // would never occur from normal browser behavior.
expect(handleImgLoadStart).toHaveBeenCalledTimes(1);
videoRef.current.dispatchEvent(
@@ -374,7 +371,9 @@ describe('ReactDOMEventListener', () => {
document.body.appendChild(container);
const videoRef = React.createRef();
- const handleVideoPlay = jest.fn(); // We'll test this one.
+ // We'll test this event alone.
+ const handleVideoPlay = jest.fn();
+ const handleVideoPlayDelegated = jest.fn();
const mediaEvents = {
onAbort() {},
onCanPlay() {},
@@ -401,10 +400,20 @@ describe('ReactDOMEventListener', () => {
onWaiting() {},
};
- const originalAddEventListener = document.addEventListener;
+ const originalDocAddEventListener = document.addEventListener;
+ const originalRootAddEventListener = container.addEventListener;
document.addEventListener = function(type) {
throw new Error(
- `Did not expect to add a top-level listener for the "${type}" event.`,
+ `Did not expect to add a document-level listener for the "${type}" event.`,
+ );
+ };
+ container.addEventListener = function(type) {
+ if (type === 'mouseout' || type === 'mouseover') {
+ // We currently listen to it unconditionally.
+ return;
+ }
+ throw new Error(
+ `Did not expect to add a root-level listener for the "${type}" event.`,
);
};
@@ -412,12 +421,11 @@ describe('ReactDOMEventListener', () => {
// We expect that mounting this tree will
// *not* attach handlers for any top-level events.
ReactDOM.render(
-
+
,
container,
);
@@ -429,8 +437,12 @@ describe('ReactDOMEventListener', () => {
}),
);
expect(handleVideoPlay).toHaveBeenCalledTimes(1);
+ // Unlike browsers, we delegate media events.
+ // (This doesn't make a lot of sense but it would be a breaking change not to.)
+ expect(handleVideoPlayDelegated).toHaveBeenCalledTimes(1);
} finally {
- document.addEventListener = originalAddEventListener;
+ document.addEventListener = originalDocAddEventListener;
+ container.addEventListener = originalRootAddEventListener;
document.body.removeChild(container);
}
});
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index acd4c4a4d9..bf3222582c 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -85,8 +85,18 @@ import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
} from 'shared/ReactFeatureFlags';
-import {listenToReactEvent} from '../events/DOMModernPluginEventSystem';
+import {
+ listenToReactEvent,
+ mediaEventTypes,
+ listenToNonDelegatedEvent,
+} from '../events/DOMModernPluginEventSystem';
import {getEventListenerMap} from './ReactDOMComponentTree';
+import {
+ TOP_LOAD,
+ TOP_ERROR,
+ TOP_TOGGLE,
+ TOP_INVALID,
+} from '../events/DOMTopLevelEventTypes';
let didWarnInvalidHydration = false;
let didWarnScriptTags = false;
@@ -266,6 +276,7 @@ if (__DEV__) {
export function ensureListeningTo(
rootContainerInstance: Element | Node,
reactPropEvent: string,
+ targetElement: Element | null,
): void {
// If we have a comment node, then use the parent node,
// which should be an element.
@@ -282,7 +293,11 @@ export function ensureListeningTo(
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React.',
);
- listenToReactEvent(reactPropEvent, ((rootContainerElement: any): Element));
+ listenToReactEvent(
+ reactPropEvent,
+ ((rootContainerElement: any): Element),
+ targetElement,
+ );
}
function getOwnerDocumentFromRootContainer(
@@ -364,7 +379,7 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
@@ -527,32 +542,50 @@ export function setInitialProperties(
case 'iframe':
case 'object':
case 'embed':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the load event.
+ listenToNonDelegatedEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'video':
case 'audio':
+ // We listen to these events in case to ensure emulated bubble
+ // listeners still fire for all the media events.
+ for (let i = 0; i < mediaEventTypes.length; i++) {
+ listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
+ }
props = rawProps;
break;
case 'source':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the error event.
+ listenToNonDelegatedEvent(TOP_ERROR, domElement);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
- props = rawProps;
- break;
- case 'form':
+ // We listen to these events in case to ensure emulated bubble
+ // listeners still fire for error and load events.
+ listenToNonDelegatedEvent(TOP_ERROR, domElement);
+ listenToNonDelegatedEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'details':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the toggle event.
+ listenToNonDelegatedEvent(TOP_TOGGLE, domElement);
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -561,16 +594,22 @@ export function setInitialProperties(
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
default:
props = rawProps;
@@ -790,7 +829,7 @@ export function diffProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ ensureListeningTo(rootContainerElement, propKey, domElement);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
@@ -900,26 +939,68 @@ export function diffHydratedProperties(
// TODO: Make sure that we check isMounted before firing any of these events.
switch (tag) {
+ case 'iframe':
+ case 'object':
+ case 'embed':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the load event.
+ listenToNonDelegatedEvent(TOP_LOAD, domElement);
+ break;
+ case 'video':
+ case 'audio':
+ // We listen to these events in case to ensure emulated bubble
+ // listeners still fire for all the media events.
+ for (let i = 0; i < mediaEventTypes.length; i++) {
+ listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
+ }
+ break;
+ case 'source':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the error event.
+ listenToNonDelegatedEvent(TOP_ERROR, domElement);
+ break;
+ case 'img':
+ case 'image':
+ case 'link':
+ // We listen to these events in case to ensure emulated bubble
+ // listeners still fire for error and load events.
+ listenToNonDelegatedEvent(TOP_ERROR, domElement);
+ listenToNonDelegatedEvent(TOP_LOAD, domElement);
+ break;
+ case 'details':
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the toggle event.
+ listenToNonDelegatedEvent(TOP_TOGGLE, domElement);
+ break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
+ // We listen to this event in case to ensure emulated bubble
+ // listeners still fire for the invalid event.
+ listenToNonDelegatedEvent(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');
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
}
@@ -986,7 +1067,7 @@ export function diffHydratedProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey);
+ ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (
__DEV__ &&
diff --git a/packages/react-dom/src/client/ReactDOMEventHandle.js b/packages/react-dom/src/client/ReactDOMEventHandle.js
index 24d6b73e95..84f6a2ecf2 100644
--- a/packages/react-dom/src/client/ReactDOMEventHandle.js
+++ b/packages/react-dom/src/client/ReactDOMEventHandle.js
@@ -19,7 +19,6 @@ import {
getClosestInstanceFromNode,
getEventHandlerListeners,
setEventHandlerListeners,
- getEventListenerMap,
getFiberFromScopeInstance,
} from './ReactDOMComponentTree';
import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
@@ -87,6 +86,7 @@ function registerEventOnNearestTargetContainer(
isPassiveListener: boolean | void,
listenerPriority: EventPriority | void,
isCapturePhaseListener: boolean,
+ targetElement: Element | null,
): void {
// If it is, find the nearest root or portal and make it
// our event handle target container.
@@ -101,13 +101,11 @@ function registerEventOnNearestTargetContainer(
if (targetContainer.nodeType === COMMENT_NODE) {
targetContainer = ((targetContainer.parentNode: any): Element);
}
- const listenerMap = getEventListenerMap(targetContainer);
listenToNativeEvent(
topLevelType,
- targetContainer,
- listenerMap,
- PLUGIN_EVENT_SYSTEM,
isCapturePhaseListener,
+ targetContainer,
+ targetElement,
isPassiveListener,
listenerPriority,
);
@@ -138,6 +136,7 @@ function registerReactDOMEvent(
isPassiveListener,
listenerPriority,
isCapturePhaseListener,
+ targetElement,
);
} else if (enableScopeAPI && isReactScope(target)) {
const scopeTarget = ((target: any): ReactScopeInstance);
@@ -152,18 +151,20 @@ function registerReactDOMEvent(
isPassiveListener,
listenerPriority,
isCapturePhaseListener,
+ null,
);
} else if (isValidEventTarget(target)) {
const eventTarget = ((target: any): EventTarget);
- const listenerMap = getEventListenerMap(eventTarget);
+ // These are valid event targets, but they are also
+ // non-managed React nodes.
listenToNativeEvent(
topLevelType,
- eventTarget,
- listenerMap,
- PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
isCapturePhaseListener,
+ eventTarget,
+ null,
isPassiveListener,
listenerPriority,
+ PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
);
} else {
invariant(
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 3aaf0b577c..61b7ba6c86 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -1111,7 +1111,7 @@ export function makeOpaqueHydratingObject(
}
export function preparePortalMount(portalInstance: Instance): void {
- listenToReactEvent('onMouseEnter', portalInstance);
+ listenToReactEvent('onMouseEnter', portalInstance, null);
}
export function prepareScopeUpdate(
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index 31a869ba2d..d9e446749c 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -145,7 +145,7 @@ function createRootImpl(
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
containerNodeType !== DOCUMENT_NODE
) {
- ensureListeningTo(container, 'onMouseEnter');
+ ensureListeningTo(container, 'onMouseEnter', null);
}
if (mutableSources) {
diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
index 81ff02d5d3..732840b508 100644
--- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js
+++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
@@ -8,23 +8,24 @@
*/
import type {TopLevelType, DOMTopLevelEventType} from './TopLevelEventTypes';
-import type {EventSystemFlags} from './EventSystemFlags';
+import {
+ type EventSystemFlags,
+ SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE,
+ IS_LEGACY_FB_SUPPORT_MODE,
+ SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS,
+} from './EventSystemFlags';
import type {AnyNativeEvent} from './PluginModuleType';
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
-import type {
- ElementListenerMap,
- ElementListenerMapEntry,
-} from '../client/ReactDOMComponentTree';
+import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
import type {EventPriority} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {registrationNameDependencies} from './EventRegistry';
import {
PLUGIN_EVENT_SYSTEM,
- LEGACY_FB_SUPPORT,
- IS_REPLAYED,
IS_CAPTURE_PHASE,
IS_EVENT_HANDLE_NON_MANAGED_NODE,
+ IS_NON_DELEGATED,
} from './EventSystemFlags';
import {
@@ -67,7 +68,6 @@ import {
TOP_PLAYING,
TOP_CLICK,
TOP_SELECTION_CHANGE,
- TOP_AFTER_BLUR,
getRawEventName,
} from './DOMTopLevelEventTypes';
import {
@@ -150,8 +150,7 @@ function extractEvents(
targetContainer,
);
const shouldProcessPolyfillPlugins =
- (eventSystemFlags & IS_CAPTURE_PHASE) === 0 ||
- capturePhaseEvents.has(topLevelType);
+ (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
// We don't process these events unless we are in the
// event's native "bubble" phase, which means that we're
// not in the capture phase. That's because we emulate
@@ -209,13 +208,8 @@ function extractEvents(
}
}
-export const capturePhaseEvents: Set
= new Set([
- TOP_SCROLL,
- TOP_LOAD,
- TOP_ABORT,
- TOP_CANCEL,
- TOP_CLOSE,
- TOP_INVALID,
+// List of events that need to be individually attached to media elements.
+export const mediaEventTypes = [
TOP_ABORT,
TOP_CAN_PLAY,
TOP_CAN_PLAY_THROUGH,
@@ -239,11 +233,23 @@ export const capturePhaseEvents: Set = new Set([
TOP_TIME_UPDATE,
TOP_VOLUME_CHANGE,
TOP_WAITING,
-]);
+];
-if (enableCreateEventHandleAPI) {
- capturePhaseEvents.add(TOP_AFTER_BLUR);
-}
+// We should not delegate these events to the container, but rather
+// set them on the actual target element itself. This is primarily
+// because these events do not consistently bubble in the DOM.
+export const nonDelegatedEvents: Set = new Set([
+ TOP_SCROLL,
+ TOP_LOAD,
+ TOP_CANCEL,
+ TOP_CLOSE,
+ TOP_INVALID,
+ // In order to reduce bytes, we insert the above array of media events
+ // into this Set. Note: some events like "load" and "error" aren't
+ // exclusively media events, but rather than duplicate them, we just
+ // take them from the media events array.
+ ...mediaEventTypes,
+]);
function executeDispatch(
event: ReactSyntheticEvent,
@@ -327,22 +333,53 @@ function shouldUpgradeListener(
);
}
+export function listenToNonDelegatedEvent(
+ topLevelType: DOMTopLevelEventType,
+ targetElement: Element,
+): void {
+ const isCapturePhaseListener = false;
+ const listenerMap = getEventListenerMap(targetElement);
+ const listenerMapKey = getListenerMapKey(
+ topLevelType,
+ isCapturePhaseListener,
+ );
+ const listenerEntry = ((listenerMap.get(
+ listenerMapKey,
+ ): any): ElementListenerMapEntry | void);
+ if (listenerEntry === undefined) {
+ const listener = addTrappedEventListener(
+ targetElement,
+ topLevelType,
+ PLUGIN_EVENT_SYSTEM | IS_NON_DELEGATED,
+ isCapturePhaseListener,
+ );
+ listenerMap.set(listenerMapKey, {passive: false, listener});
+ }
+}
+
export function listenToNativeEvent(
topLevelType: DOMTopLevelEventType,
- target: EventTarget,
- listenerMap: ElementListenerMap,
- eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
+ rootContainerElement: EventTarget,
+ targetElement: Element | null,
isPassiveListener?: boolean,
- priority?: EventPriority,
+ listenerPriority?: EventPriority,
+ eventSystemFlags?: EventSystemFlags = PLUGIN_EVENT_SYSTEM,
): void {
+ let target = rootContainerElement;
// TOP_SELECTION_CHANGE needs to be attached to the document
// otherwise it won't capture incoming events that are only
// triggered on the document directly.
if (topLevelType === TOP_SELECTION_CHANGE) {
- target = (target: any).ownerDocument || target;
- listenerMap = getEventListenerMap(target);
+ target = (rootContainerElement: any).ownerDocument;
}
+ // If the event can be delegated, we can register it to the root container.
+ // Otherwise, we should register the event to the target element.
+ if (targetElement !== null && nonDelegatedEvents.has(topLevelType)) {
+ eventSystemFlags |= IS_NON_DELEGATED;
+ target = targetElement;
+ }
+ const listenerMap = getEventListenerMap(target);
const listenerMapKey = getListenerMapKey(
topLevelType,
isCapturePhaseListener,
@@ -375,50 +412,51 @@ export function listenToNativeEvent(
isCapturePhaseListener,
false,
isPassiveListener,
- priority,
+ listenerPriority,
);
listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
}
}
-function isCaptureRegistrationName(registrationName: string): boolean {
- const len = registrationName.length;
- return registrationName.substr(len - 7) === 'Capture';
-}
-
export function listenToReactEvent(
- reactPropEvent: string,
+ reactEvent: string,
rootContainerElement: Element,
+ targetElement: Element | null,
): void {
- const listenerMap = getEventListenerMap(rootContainerElement);
- // For optimization, let's check if we have the registration name
- // on the rootContainerElement.
- if (listenerMap.has(reactPropEvent)) {
- return;
- }
- // Add the registration name to the map, so we can avoid processing
- // this React prop event again.
- listenerMap.set(reactPropEvent, null);
- const dependencies = registrationNameDependencies[reactPropEvent];
+ const dependencies = registrationNameDependencies[reactEvent];
const dependenciesLength = dependencies.length;
// If the dependencies length is 1, that means we're not using a polyfill
- // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin and
- // SelectEventPlugin. SimpleEventPlugin always only has a single dependency.
- // Given this, we know that we never need to apply capture phase event
- // listeners to anything other than the SimpleEventPlugin.
- const registrationCapturePhase =
- isCaptureRegistrationName(reactPropEvent) && dependenciesLength === 1;
+ // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
+ // and SelectEventPlugin. We always use the native bubble event phase for
+ // these plugins and emulate two phase event dispatching. SimpleEventPlugin
+ // always only has a single dependency and SimpleEventPlugin events also
+ // use either the native capture event phase or bubble event phase, there
+ // is no emulation (except for focus/blur, but that will be removed soon).
+ const isPolyfillEventPlugin = dependenciesLength !== 1;
- for (let i = 0; i < dependenciesLength; i++) {
- const dependency = dependencies[i];
- const capture =
- capturePhaseEvents.has(dependency) || registrationCapturePhase;
+ if (isPolyfillEventPlugin) {
+ const listenerMap = getEventListenerMap(rootContainerElement);
+ // For optimization, we register plugins on the listener map, so we
+ // don't need to check each of their dependencies each time.
+ if (!listenerMap.has(reactEvent)) {
+ listenerMap.set(reactEvent, null);
+ for (let i = 0; i < dependenciesLength; i++) {
+ listenToNativeEvent(
+ dependencies[i],
+ false,
+ rootContainerElement,
+ targetElement,
+ );
+ }
+ }
+ } else {
+ // Check if the react event ends in "Capture"
+ const isCapturePhaseListener = reactEvent.substr(-7) === 'Capture';
listenToNativeEvent(
- dependency,
+ dependencies[0],
+ isCapturePhaseListener,
rootContainerElement,
- listenerMap,
- PLUGIN_EVENT_SYSTEM,
- capture,
+ targetElement,
);
}
}
@@ -509,13 +547,10 @@ function addTrappedEventListener(
return unsubscribeListener;
}
-function willDeferLaterForLegacyFBSupport(
+function deferClickToDocumentForLegacyFBSupport(
topLevelType: DOMTopLevelEventType,
targetContainer: EventTarget,
-): boolean {
- if (topLevelType !== TOP_CLICK) {
- return false;
- }
+): void {
// We defer all click events with legacy FB support mode on.
// This means we add a one time event listener to trigger
// after the FB delegated listeners fire.
@@ -523,11 +558,10 @@ function willDeferLaterForLegacyFBSupport(
addTrappedEventListener(
targetContainer,
topLevelType,
- PLUGIN_EVENT_SYSTEM | LEGACY_FB_SUPPORT,
+ PLUGIN_EVENT_SYSTEM | IS_LEGACY_FB_SUPPORT_MODE,
false,
isDeferredListenerForLegacyFBSupport,
);
- return true;
}
function isMatchingRootContainer(
@@ -549,10 +583,10 @@ export function dispatchEventForPluginEventSystem(
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
- if (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) {
- // For TargetEvent nodes (i.e. document, window)
- ancestorInst = null;
- } else {
+ if (
+ (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
+ (eventSystemFlags & IS_NON_DELEGATED) === 0
+ ) {
const targetContainerNode = ((targetContainer: any): Node);
// If we are using the legacy FB support flag, we
@@ -560,17 +594,15 @@ export function dispatchEventForPluginEventSystem(
// time event listener so we can defer the event.
if (
enableLegacyFBSupport &&
- // We do not want to defer if the event system has already been
- // set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
- // we call willDeferLaterForLegacyFBSupport, thus not bailing out
- // will result in endless cycles like an infinite loop.
- (eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
- // We also don't want to defer during event replaying.
- (eventSystemFlags & IS_REPLAYED) === 0 &&
- // We don't apply this during capture phase.
- (eventSystemFlags & IS_CAPTURE_PHASE) === 0 &&
- willDeferLaterForLegacyFBSupport(topLevelType, targetContainer)
+ // If our event flags match the required flags for entering
+ // FB legacy mode and we are prcocessing the "click" event,
+ // then we can defer the event to the "document", to allow
+ // for legacy FB support, where the expected behavior was to
+ // match React < 16 behavior of delegated clicks to the doc.
+ topLevelType === TOP_CLICK &&
+ (eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
) {
+ deferClickToDocumentForLegacyFBSupport(topLevelType, targetContainer);
return;
}
if (targetInst !== null) {
@@ -691,11 +723,6 @@ export function accumulateSinglePhaseListeners(
let instance = targetFiber;
let lastHostComponent = null;
const targetType = event.type;
- // shouldEmulateTwoPhase is temporary till we can polyfill focus/blur to
- // focusin/focusout.
- const shouldEmulateTwoPhase = capturePhaseEvents.has(
- ((targetType: any): DOMTopLevelEventType),
- );
// Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
@@ -721,17 +748,10 @@ export function accumulateSinglePhaseListeners(
listeners.push(
createDispatchListener(instance, callback, currentTarget),
);
- } else if (!isCapturePhaseListener) {
- const entry = createDispatchListener(
- instance,
- callback,
- currentTarget,
+ } else if (!isCapturePhaseListener && !inCapturePhase) {
+ listeners.push(
+ createDispatchListener(instance, callback, currentTarget),
);
- if (shouldEmulateTwoPhase) {
- listeners.unshift(entry);
- } else if (!inCapturePhase) {
- listeners.push(entry);
- }
}
}
}
@@ -746,19 +766,12 @@ export function accumulateSinglePhaseListeners(
);
}
}
- if (bubbled !== null) {
+ if (bubbled !== null && !inCapturePhase) {
const bubbleListener = getListener(instance, bubbled);
if (bubbleListener != null) {
- const entry = createDispatchListener(
- instance,
- bubbleListener,
- currentTarget,
+ listeners.push(
+ createDispatchListener(instance, bubbleListener, currentTarget),
);
- if (shouldEmulateTwoPhase) {
- listeners.unshift(entry);
- } else if (!inCapturePhase) {
- listeners.push(entry);
- }
}
}
} else if (
@@ -786,17 +799,10 @@ export function accumulateSinglePhaseListeners(
listeners.push(
createDispatchListener(instance, callback, lastCurrentTarget),
);
- } else if (!isCapturePhaseListener) {
- const entry = createDispatchListener(
- instance,
- callback,
- lastCurrentTarget,
+ } else if (!isCapturePhaseListener && !inCapturePhase) {
+ listeners.push(
+ createDispatchListener(instance, callback, lastCurrentTarget),
);
- if (shouldEmulateTwoPhase) {
- listeners.unshift(entry);
- } else if (!inCapturePhase) {
- listeners.push(entry);
- }
}
}
}
diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js
index 140deafc0b..e8bdc7eacc 100644
--- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js
+++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js
@@ -153,35 +153,6 @@ export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur');
export const TOP_FOCUS_IN = unsafeCastStringToDOMTopLevelType('focusin');
export const TOP_FOCUS_OUT = unsafeCastStringToDOMTopLevelType('focusout');
-// List of events that need to be individually attached to media elements.
-// Note that events in this list will *not* be listened to at the top level
-// unless they're explicitly listed in `ReactBrowserEventEmitter.listenTo`.
-export const mediaEventTypes = [
- 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 getRawEventName(topLevelType: DOMTopLevelEventType): string {
return unsafeCastDOMTopLevelTypeToString(topLevelType);
}
diff --git a/packages/react-dom/src/events/EventSystemFlags.js b/packages/react-dom/src/events/EventSystemFlags.js
index 38cb9196b5..a6b34ab3b3 100644
--- a/packages/react-dom/src/events/EventSystemFlags.js
+++ b/packages/react-dom/src/events/EventSystemFlags.js
@@ -12,9 +12,21 @@ export type EventSystemFlags = number;
export const PLUGIN_EVENT_SYSTEM = 1;
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1 << 2;
-export const IS_CAPTURE_PHASE = 1 << 3;
-export const IS_PASSIVE = 1 << 4;
-export const PASSIVE_NOT_SUPPORTED = 1 << 5;
+export const IS_NON_DELEGATED = 1 << 3;
+export const IS_CAPTURE_PHASE = 1 << 4;
+export const IS_PASSIVE = 1 << 5;
export const IS_REPLAYED = 1 << 6;
-export const IS_FIRST_ANCESTOR = 1 << 7;
-export const LEGACY_FB_SUPPORT = 1 << 8;
+export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 7;
+// This is used by React Flare
+export const PASSIVE_NOT_SUPPORTED = 1 << 8;
+
+export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
+ IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE;
+
+// We do not want to defer if the event system has already been
+// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
+// we call willDeferLaterForLegacyFBSupport, thus not bailing out
+// will result in endless cycles like an infinite loop.
+// We also don't want to defer during event replaying.
+export const SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS =
+ IS_EVENT_HANDLE_NON_MANAGED_NODE | IS_NON_DELEGATED | IS_CAPTURE_PHASE;
diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js
index fad2d1d56c..ace0595305 100644
--- a/packages/react-dom/src/events/ReactDOMEventListener.js
+++ b/packages/react-dom/src/events/ReactDOMEventListener.js
@@ -33,7 +33,7 @@ import {
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {
type EventSystemFlags,
- LEGACY_FB_SUPPORT,
+ IS_LEGACY_FB_SUPPORT_MODE,
PLUGIN_EVENT_SYSTEM,
RESPONDER_EVENT_SYSTEM,
} from './EventSystemFlags';
@@ -132,9 +132,9 @@ function dispatchDiscreteEvent(
) {
if (
!enableLegacyFBSupport ||
- // If we have Legacy FB support, it means we've already
+ // If we are in Legacy FB support mode, it means we've already
// flushed for this event and we don't need to do it again.
- (eventSystemFlags & LEGACY_FB_SUPPORT) === 0
+ (eventSystemFlags & IS_LEGACY_FB_SUPPORT_MODE) === 0
) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
}
diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js
index a486cf4e34..f94f5d61e7 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -108,8 +108,6 @@ import {
TOP_KEY_UP,
TOP_INPUT,
TOP_TEXT_INPUT,
- TOP_CLOSE,
- TOP_CANCEL,
TOP_COPY,
TOP_CUT,
TOP_PASTE,
@@ -129,11 +127,8 @@ import {
TOP_FOCUS_IN,
TOP_FOCUS_OUT,
} from './DOMTopLevelEventTypes';
-import {IS_REPLAYED, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags';
-import {
- listenToNativeEvent,
- capturePhaseEvents,
-} from './DOMModernPluginEventSystem';
+import {IS_REPLAYED} from './EventSystemFlags';
+import {listenToNativeEvent} from './DOMModernPluginEventSystem';
import {addResponderEventSystemEvent} from './DeprecatedDOMEventResponderSystem';
type QueuedReplayableEvent = {|
@@ -198,8 +193,6 @@ const discreteReplayableEvents = [
TOP_KEY_UP,
TOP_INPUT,
TOP_TEXT_INPUT,
- TOP_CLOSE,
- TOP_CANCEL,
TOP_COPY,
TOP_CUT,
TOP_PASTE,
@@ -232,16 +225,8 @@ export function isReplayableDiscreteEvent(
function trapReplayableEventForContainer(
topLevelType: DOMTopLevelEventType,
container: Container,
- listenerMap: ElementListenerMap,
) {
- const capture = capturePhaseEvents.has(topLevelType);
- listenToNativeEvent(
- topLevelType,
- ((container: any): Element),
- listenerMap,
- PLUGIN_EVENT_SYSTEM,
- capture,
- );
+ listenToNativeEvent(topLevelType, false, ((container: any): Element), null);
}
function trapReplayableEventForDocument(
@@ -273,23 +258,14 @@ export function eagerlyTrapReplayableEvents(
document: Document,
) {
const listenerMapForDoc = getEventListenerMap(document);
- const listenerMapForContainer = getEventListenerMap(container);
// Discrete
discreteReplayableEvents.forEach(topLevelType => {
- trapReplayableEventForContainer(
- topLevelType,
- container,
- listenerMapForContainer,
- );
+ trapReplayableEventForContainer(topLevelType, container);
trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
});
// Continuous
continuousReplayableEvents.forEach(topLevelType => {
- trapReplayableEventForContainer(
- topLevelType,
- container,
- listenerMapForContainer,
- );
+ trapReplayableEventForContainer(topLevelType, container);
trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
});
}