Remove capturePhaseEvents and separate events by bubbling (#19278)

* Remove capturePhaseEvents and separate events by bubbling

WIP

Refine all logic

Revise types

Fix

Fix conflicts

Fix flags

Fix

Fix

Fix test

Revise

Cleanup

Refine

Deal with replaying

Fix

* Add non delegated listeners unconditionally

* Add media events

* Fix a previously ignored test

* Address feedback

Co-authored-by: Dan Abramov <dan.abramov@me.com>
This commit is contained in:
Dominic Gannaway
2020-07-21 22:40:50 +01:00
committed by GitHub
co-authored by Dan Abramov
parent 1dcee86565
commit 356c17108f
10 changed files with 277 additions and 218 deletions
@@ -349,13 +349,10 @@ describe('ReactDOMEventListener', () => {
}),
);
// As of the modern event system refactor, we now support
// this on <img>. 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 <img>. 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(
<div>
<div onPlay={handleVideoPlayDelegated}>
<video ref={videoRef} {...mediaEvents} onPlay={handleVideoPlay} />
<audio {...mediaEvents}>
<source {...mediaEvents} />
</audio>
<form onReset={() => {}} onSubmit={() => {}} />
</div>,
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);
}
});
+95 -14
View File
@@ -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__ &&
+10 -9
View File
@@ -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(
+1 -1
View File
@@ -1111,7 +1111,7 @@ export function makeOpaqueHydratingObject(
}
export function preparePortalMount(portalInstance: Instance): void {
listenToReactEvent('onMouseEnter', portalInstance);
listenToReactEvent('onMouseEnter', portalInstance, null);
}
export function prepareScopeUpdate(
+1 -1
View File
@@ -145,7 +145,7 @@ function createRootImpl(
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
containerNodeType !== DOCUMENT_NODE
) {
ensureListeningTo(container, 'onMouseEnter');
ensureListeningTo(container, 'onMouseEnter', null);
}
if (mutableSources) {
+120 -114
View File
@@ -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<DOMTopLevelEventType> = 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<DOMTopLevelEventType> = 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<DOMTopLevelEventType> = 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);
}
}
}
}
-29
View File
@@ -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);
}
+17 -5
View File
@@ -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;
+3 -3
View File
@@ -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);
}
+5 -29
View File
@@ -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);
});
}