mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
co-authored by
Dan Abramov
parent
1dcee86565
commit
356c17108f
+25
-13
@@ -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
@@ -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
@@ -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
@@ -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
@@ -145,7 +145,7 @@ function createRootImpl(
|
||||
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
|
||||
containerNodeType !== DOCUMENT_NODE
|
||||
) {
|
||||
ensureListeningTo(container, 'onMouseEnter');
|
||||
ensureListeningTo(container, 'onMouseEnter', null);
|
||||
}
|
||||
|
||||
if (mutableSources) {
|
||||
|
||||
+120
-114
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user