@samitouri / QOS-React-2 / commits / 356c17108f

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>

Dominic Gannaway committed Jul 21, 2020 at 22:40 UTC 356c17108f4e132371450338fa86e195f5e0acf4
10 files changed +277 -218
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+25 -13
@@ -349,13 +349,10 @@ describe('ReactDOMEventListener', () => {
349 }),
350 );
351 // As of the modern event system refactor, we now support
352 - // this on <img>. The reason for this, is because we now
353 - // attach all media events to the "root" or "portal" in the
354 - // capture phase, rather than the bubble phase. This allows
355 - // us to assign less event listeners to individual elements,
356 - // which also nicely allows us to support more without needing
357 - // to add more individual code paths to support various
358 - // events that do not bubble.
352 + // this on <img>. The reason for this, is because we allow
353 + // events to be attached to nodes regardless of if they
354 + // necessary support them. This is a strange test, as this
355 + // would never occur from normal browser behavior.
356 expect(handleImgLoadStart).toHaveBeenCalledTimes(1);
357
358 videoRef.current.dispatchEvent(
@@ -374,7 +371,9 @@ describe('ReactDOMEventListener', () => {
371 document.body.appendChild(container);
372
373 const videoRef = React.createRef();
377 - const handleVideoPlay = jest.fn(); // We'll test this one.
374 + // We'll test this event alone.
375 + const handleVideoPlay = jest.fn();
376 + const handleVideoPlayDelegated = jest.fn();
377 const mediaEvents = {
378 onAbort() {},
379 onCanPlay() {},
@@ -401,10 +400,20 @@ describe('ReactDOMEventListener', () => {
400 onWaiting() {},
401 };
402
404 - const originalAddEventListener = document.addEventListener;
403 + const originalDocAddEventListener = document.addEventListener;
404 + const originalRootAddEventListener = container.addEventListener;
405 document.addEventListener = function(type) {
406 throw new Error(
407 - `Did not expect to add a top-level listener for the "${type}" event.`,
407 + `Did not expect to add a document-level listener for the "${type}" event.`,
408 + );
409 + };
410 + container.addEventListener = function(type) {
411 + if (type === 'mouseout' || type === 'mouseover') {
412 + // We currently listen to it unconditionally.
413 + return;
414 + }
415 + throw new Error(
416 + `Did not expect to add a root-level listener for the "${type}" event.`,
417 );
418 };
419
@@ -412,12 +421,11 @@ describe('ReactDOMEventListener', () => {
421 // We expect that mounting this tree will
422 // *not* attach handlers for any top-level events.
423 ReactDOM.render(
415 - <div>
424 + <div onPlay={handleVideoPlayDelegated}>
425 <video ref={videoRef} {...mediaEvents} onPlay={handleVideoPlay} />
426 <audio {...mediaEvents}>
427 <source {...mediaEvents} />
428 </audio>
420 - <form onReset={() => {}} onSubmit={() => {}} />
429 </div>,
430 container,
431 );
@@ -429,8 +437,12 @@ describe('ReactDOMEventListener', () => {
437 }),
438 );
439 expect(handleVideoPlay).toHaveBeenCalledTimes(1);
440 + // Unlike browsers, we delegate media events.
441 + // (This doesn't make a lot of sense but it would be a breaking change not to.)
442 + expect(handleVideoPlayDelegated).toHaveBeenCalledTimes(1);
443 } finally {
433 - document.addEventListener = originalAddEventListener;
444 + document.addEventListener = originalDocAddEventListener;
445 + container.addEventListener = originalRootAddEventListener;
446 document.body.removeChild(container);
447 }
448 });
packages/react-dom/src/client/ReactDOMComponent.js
+95 -14
@@ -85,8 +85,18 @@ import {
85 enableDeprecatedFlareAPI,
86 enableTrustedTypesIntegration,
87 } from 'shared/ReactFeatureFlags';
88 -import {listenToReactEvent} from '../events/DOMModernPluginEventSystem';
88 +import {
89 + listenToReactEvent,
90 + mediaEventTypes,
91 + listenToNonDelegatedEvent,
92 +} from '../events/DOMModernPluginEventSystem';
93 import {getEventListenerMap} from './ReactDOMComponentTree';
94 +import {
95 + TOP_LOAD,
96 + TOP_ERROR,
97 + TOP_TOGGLE,
98 + TOP_INVALID,
99 +} from '../events/DOMTopLevelEventTypes';
100
101 let didWarnInvalidHydration = false;
102 let didWarnScriptTags = false;
@@ -266,6 +276,7 @@ if (__DEV__) {
276 export function ensureListeningTo(
277 rootContainerInstance: Element | Node,
278 reactPropEvent: string,
279 + targetElement: Element | null,
280 ): void {
281 // If we have a comment node, then use the parent node,
282 // which should be an element.
@@ -282,7 +293,11 @@ export function ensureListeningTo(
293 'ensureListeningTo(): received a container that was not an element node. ' +
294 'This is likely a bug in React.',
295 );
285 - listenToReactEvent(reactPropEvent, ((rootContainerElement: any): Element));
296 + listenToReactEvent(
297 + reactPropEvent,
298 + ((rootContainerElement: any): Element),
299 + targetElement,
300 + );
301 }
302
303 function getOwnerDocumentFromRootContainer(
@@ -364,7 +379,7 @@ function setInitialDOMProperties(
379 if (__DEV__ && typeof nextProp !== 'function') {
380 warnForInvalidEventListener(propKey, nextProp);
381 }
367 - ensureListeningTo(rootContainerElement, propKey);
382 + ensureListeningTo(rootContainerElement, propKey, domElement);
383 }
384 } else if (nextProp != null) {
385 setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
@@ -527,32 +542,50 @@ export function setInitialProperties(
542 case 'iframe':
543 case 'object':
544 case 'embed':
545 + // We listen to this event in case to ensure emulated bubble
546 + // listeners still fire for the load event.
547 + listenToNonDelegatedEvent(TOP_LOAD, domElement);
548 props = rawProps;
549 break;
550 case 'video':
551 case 'audio':
552 + // We listen to these events in case to ensure emulated bubble
553 + // listeners still fire for all the media events.
554 + for (let i = 0; i < mediaEventTypes.length; i++) {
555 + listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
556 + }
557 props = rawProps;
558 break;
559 case 'source':
560 + // We listen to this event in case to ensure emulated bubble
561 + // listeners still fire for the error event.
562 + listenToNonDelegatedEvent(TOP_ERROR, domElement);
563 props = rawProps;
564 break;
565 case 'img':
566 case 'image':
567 case 'link':
542 - props = rawProps;
543 - break;
544 - case 'form':
568 + // We listen to these events in case to ensure emulated bubble
569 + // listeners still fire for error and load events.
570 + listenToNonDelegatedEvent(TOP_ERROR, domElement);
571 + listenToNonDelegatedEvent(TOP_LOAD, domElement);
572 props = rawProps;
573 break;
574 case 'details':
575 + // We listen to this event in case to ensure emulated bubble
576 + // listeners still fire for the toggle event.
577 + listenToNonDelegatedEvent(TOP_TOGGLE, domElement);
578 props = rawProps;
579 break;
580 case 'input':
581 ReactDOMInputInitWrapperState(domElement, rawProps);
582 props = ReactDOMInputGetHostProps(domElement, rawProps);
583 + // We listen to this event in case to ensure emulated bubble
584 + // listeners still fire for the invalid event.
585 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
586 // For controlled components we always need to ensure we're listening
587 // to onChange. Even if there is no listener.
555 - ensureListeningTo(rootContainerElement, 'onChange');
588 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
589 break;
590 case 'option':
591 ReactDOMOptionValidateProps(domElement, rawProps);
@@ -561,16 +594,22 @@ export function setInitialProperties(
594 case 'select':
595 ReactDOMSelectInitWrapperState(domElement, rawProps);
596 props = ReactDOMSelectGetHostProps(domElement, rawProps);
597 + // We listen to this event in case to ensure emulated bubble
598 + // listeners still fire for the invalid event.
599 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
600 // For controlled components we always need to ensure we're listening
601 // to onChange. Even if there is no listener.
566 - ensureListeningTo(rootContainerElement, 'onChange');
602 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
603 break;
604 case 'textarea':
605 ReactDOMTextareaInitWrapperState(domElement, rawProps);
606 props = ReactDOMTextareaGetHostProps(domElement, rawProps);
607 + // We listen to this event in case to ensure emulated bubble
608 + // listeners still fire for the invalid event.
609 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
610 // For controlled components we always need to ensure we're listening
611 // to onChange. Even if there is no listener.
573 - ensureListeningTo(rootContainerElement, 'onChange');
612 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
613 break;
614 default:
615 props = rawProps;
@@ -790,7 +829,7 @@ export function diffProperties(
829 if (__DEV__ && typeof nextProp !== 'function') {
830 warnForInvalidEventListener(propKey, nextProp);
831 }
793 - ensureListeningTo(rootContainerElement, propKey);
832 + ensureListeningTo(rootContainerElement, propKey, domElement);
833 }
834 if (!updatePayload && lastProp !== nextProp) {
835 // This is a special case. If any listener updates we need to ensure
@@ -900,26 +939,68 @@ export function diffHydratedProperties(
939
940 // TODO: Make sure that we check isMounted before firing any of these events.
941 switch (tag) {
942 + case 'iframe':
943 + case 'object':
944 + case 'embed':
945 + // We listen to this event in case to ensure emulated bubble
946 + // listeners still fire for the load event.
947 + listenToNonDelegatedEvent(TOP_LOAD, domElement);
948 + break;
949 + case 'video':
950 + case 'audio':
951 + // We listen to these events in case to ensure emulated bubble
952 + // listeners still fire for all the media events.
953 + for (let i = 0; i < mediaEventTypes.length; i++) {
954 + listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
955 + }
956 + break;
957 + case 'source':
958 + // We listen to this event in case to ensure emulated bubble
959 + // listeners still fire for the error event.
960 + listenToNonDelegatedEvent(TOP_ERROR, domElement);
961 + break;
962 + case 'img':
963 + case 'image':
964 + case 'link':
965 + // We listen to these events in case to ensure emulated bubble
966 + // listeners still fire for error and load events.
967 + listenToNonDelegatedEvent(TOP_ERROR, domElement);
968 + listenToNonDelegatedEvent(TOP_LOAD, domElement);
969 + break;
970 + case 'details':
971 + // We listen to this event in case to ensure emulated bubble
972 + // listeners still fire for the toggle event.
973 + listenToNonDelegatedEvent(TOP_TOGGLE, domElement);
974 + break;
975 case 'input':
976 ReactDOMInputInitWrapperState(domElement, rawProps);
977 + // We listen to this event in case to ensure emulated bubble
978 + // listeners still fire for the invalid event.
979 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
980 // For controlled components we always need to ensure we're listening
981 // to onChange. Even if there is no listener.
907 - ensureListeningTo(rootContainerElement, 'onChange');
982 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
983 break;
984 case 'option':
985 ReactDOMOptionValidateProps(domElement, rawProps);
986 break;
987 case 'select':
988 ReactDOMSelectInitWrapperState(domElement, rawProps);
989 + // We listen to this event in case to ensure emulated bubble
990 + // listeners still fire for the invalid event.
991 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
992 // For controlled components we always need to ensure we're listening
993 // to onChange. Even if there is no listener.
916 - ensureListeningTo(rootContainerElement, 'onChange');
994 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
995 break;
996 case 'textarea':
997 ReactDOMTextareaInitWrapperState(domElement, rawProps);
998 + // We listen to this event in case to ensure emulated bubble
999 + // listeners still fire for the invalid event.
1000 + listenToNonDelegatedEvent(TOP_INVALID, domElement);
1001 // For controlled components we always need to ensure we're listening
1002 // to onChange. Even if there is no listener.
922 - ensureListeningTo(rootContainerElement, 'onChange');
1003 + ensureListeningTo(rootContainerElement, 'onChange', domElement);
1004 break;
1005 }
1006
@@ -986,7 +1067,7 @@ export function diffHydratedProperties(
1067 if (__DEV__ && typeof nextProp !== 'function') {
1068 warnForInvalidEventListener(propKey, nextProp);
1069 }
989 - ensureListeningTo(rootContainerElement, propKey);
1070 + ensureListeningTo(rootContainerElement, propKey, domElement);
1071 }
1072 } else if (
1073 __DEV__ &&
packages/react-dom/src/client/ReactDOMEventHandle.js
+10 -9
@@ -19,7 +19,6 @@ import {
19 getClosestInstanceFromNode,
20 getEventHandlerListeners,
21 setEventHandlerListeners,
22 - getEventListenerMap,
22 getFiberFromScopeInstance,
23 } from './ReactDOMComponentTree';
24 import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
@@ -87,6 +86,7 @@ function registerEventOnNearestTargetContainer(
86 isPassiveListener: boolean | void,
87 listenerPriority: EventPriority | void,
88 isCapturePhaseListener: boolean,
89 + targetElement: Element | null,
90 ): void {
91 // If it is, find the nearest root or portal and make it
92 // our event handle target container.
@@ -101,13 +101,11 @@ function registerEventOnNearestTargetContainer(
101 if (targetContainer.nodeType === COMMENT_NODE) {
102 targetContainer = ((targetContainer.parentNode: any): Element);
103 }
104 - const listenerMap = getEventListenerMap(targetContainer);
104 listenToNativeEvent(
105 topLevelType,
107 - targetContainer,
108 - listenerMap,
109 - PLUGIN_EVENT_SYSTEM,
106 isCapturePhaseListener,
107 + targetContainer,
108 + targetElement,
109 isPassiveListener,
110 listenerPriority,
111 );
@@ -138,6 +136,7 @@ function registerReactDOMEvent(
136 isPassiveListener,
137 listenerPriority,
138 isCapturePhaseListener,
139 + targetElement,
140 );
141 } else if (enableScopeAPI && isReactScope(target)) {
142 const scopeTarget = ((target: any): ReactScopeInstance);
@@ -152,18 +151,20 @@ function registerReactDOMEvent(
151 isPassiveListener,
152 listenerPriority,
153 isCapturePhaseListener,
154 + null,
155 );
156 } else if (isValidEventTarget(target)) {
157 const eventTarget = ((target: any): EventTarget);
158 - const listenerMap = getEventListenerMap(eventTarget);
158 + // These are valid event targets, but they are also
159 + // non-managed React nodes.
160 listenToNativeEvent(
161 topLevelType,
161 - eventTarget,
162 - listenerMap,
163 - PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
162 isCapturePhaseListener,
163 + eventTarget,
164 + null,
165 isPassiveListener,
166 listenerPriority,
167 + PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
168 );
169 } else {
170 invariant(
packages/react-dom/src/client/ReactDOMHostConfig.js
+1 -1
@@ -1111,7 +1111,7 @@ export function makeOpaqueHydratingObject(
1111 }
1112
1113 export function preparePortalMount(portalInstance: Instance): void {
1114 - listenToReactEvent('onMouseEnter', portalInstance);
1114 + listenToReactEvent('onMouseEnter', portalInstance, null);
1115 }
1116
1117 export function prepareScopeUpdate(
packages/react-dom/src/client/ReactDOMRoot.js
+1 -1
@@ -145,7 +145,7 @@ function createRootImpl(
145 containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
146 containerNodeType !== DOCUMENT_NODE
147 ) {
148 - ensureListeningTo(container, 'onMouseEnter');
148 + ensureListeningTo(container, 'onMouseEnter', null);
149 }
150
151 if (mutableSources) {
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+120 -114
@@ -8,23 +8,24 @@
8 */
9
10 import type {TopLevelType, DOMTopLevelEventType} from './TopLevelEventTypes';
11 -import type {EventSystemFlags} from './EventSystemFlags';
11 +import {
12 + type EventSystemFlags,
13 + SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE,
14 + IS_LEGACY_FB_SUPPORT_MODE,
15 + SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS,
16 +} from './EventSystemFlags';
17 import type {AnyNativeEvent} from './PluginModuleType';
18 import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
14 -import type {
15 - ElementListenerMap,
16 - ElementListenerMapEntry,
17 -} from '../client/ReactDOMComponentTree';
19 +import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
20 import type {EventPriority} from 'shared/ReactTypes';
21 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
22
23 import {registrationNameDependencies} from './EventRegistry';
24 import {
25 PLUGIN_EVENT_SYSTEM,
24 - LEGACY_FB_SUPPORT,
25 - IS_REPLAYED,
26 IS_CAPTURE_PHASE,
27 IS_EVENT_HANDLE_NON_MANAGED_NODE,
28 + IS_NON_DELEGATED,
29 } from './EventSystemFlags';
30
31 import {
@@ -67,7 +68,6 @@ import {
68 TOP_PLAYING,
69 TOP_CLICK,
70 TOP_SELECTION_CHANGE,
70 - TOP_AFTER_BLUR,
71 getRawEventName,
72 } from './DOMTopLevelEventTypes';
73 import {
@@ -150,8 +150,7 @@ function extractEvents(
150 targetContainer,
151 );
152 const shouldProcessPolyfillPlugins =
153 - (eventSystemFlags & IS_CAPTURE_PHASE) === 0 ||
154 - capturePhaseEvents.has(topLevelType);
153 + (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
154 // We don't process these events unless we are in the
155 // event's native "bubble" phase, which means that we're
156 // not in the capture phase. That's because we emulate
@@ -209,13 +208,8 @@ function extractEvents(
208 }
209 }
210
212 -export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
213 - TOP_SCROLL,
214 - TOP_LOAD,
215 - TOP_ABORT,
216 - TOP_CANCEL,
217 - TOP_CLOSE,
218 - TOP_INVALID,
211 +// List of events that need to be individually attached to media elements.
212 +export const mediaEventTypes = [
213 TOP_ABORT,
214 TOP_CAN_PLAY,
215 TOP_CAN_PLAY_THROUGH,
@@ -239,11 +233,23 @@ export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
233 TOP_TIME_UPDATE,
234 TOP_VOLUME_CHANGE,
235 TOP_WAITING,
242 -]);
236 +];
237
244 -if (enableCreateEventHandleAPI) {
245 - capturePhaseEvents.add(TOP_AFTER_BLUR);
246 -}
238 +// We should not delegate these events to the container, but rather
239 +// set them on the actual target element itself. This is primarily
240 +// because these events do not consistently bubble in the DOM.
241 +export const nonDelegatedEvents: Set<DOMTopLevelEventType> = new Set([
242 + TOP_SCROLL,
243 + TOP_LOAD,
244 + TOP_CANCEL,
245 + TOP_CLOSE,
246 + TOP_INVALID,
247 + // In order to reduce bytes, we insert the above array of media events
248 + // into this Set. Note: some events like "load" and "error" aren't
249 + // exclusively media events, but rather than duplicate them, we just
250 + // take them from the media events array.
251 + ...mediaEventTypes,
252 +]);
253
254 function executeDispatch(
255 event: ReactSyntheticEvent,
@@ -327,22 +333,53 @@ function shouldUpgradeListener(
333 );
334 }
335
336 +export function listenToNonDelegatedEvent(
337 + topLevelType: DOMTopLevelEventType,
338 + targetElement: Element,
339 +): void {
340 + const isCapturePhaseListener = false;
341 + const listenerMap = getEventListenerMap(targetElement);
342 + const listenerMapKey = getListenerMapKey(
343 + topLevelType,
344 + isCapturePhaseListener,
345 + );
346 + const listenerEntry = ((listenerMap.get(
347 + listenerMapKey,
348 + ): any): ElementListenerMapEntry | void);
349 + if (listenerEntry === undefined) {
350 + const listener = addTrappedEventListener(
351 + targetElement,
352 + topLevelType,
353 + PLUGIN_EVENT_SYSTEM | IS_NON_DELEGATED,
354 + isCapturePhaseListener,
355 + );
356 + listenerMap.set(listenerMapKey, {passive: false, listener});
357 + }
358 +}
359 +
360 export function listenToNativeEvent(
361 topLevelType: DOMTopLevelEventType,
332 - target: EventTarget,
333 - listenerMap: ElementListenerMap,
334 - eventSystemFlags: EventSystemFlags,
362 isCapturePhaseListener: boolean,
363 + rootContainerElement: EventTarget,
364 + targetElement: Element | null,
365 isPassiveListener?: boolean,
337 - priority?: EventPriority,
366 + listenerPriority?: EventPriority,
367 + eventSystemFlags?: EventSystemFlags = PLUGIN_EVENT_SYSTEM,
368 ): void {
369 + let target = rootContainerElement;
370 // TOP_SELECTION_CHANGE needs to be attached to the document
371 // otherwise it won't capture incoming events that are only
372 // triggered on the document directly.
373 if (topLevelType === TOP_SELECTION_CHANGE) {
343 - target = (target: any).ownerDocument || target;
344 - listenerMap = getEventListenerMap(target);
374 + target = (rootContainerElement: any).ownerDocument;
375 + }
376 + // If the event can be delegated, we can register it to the root container.
377 + // Otherwise, we should register the event to the target element.
378 + if (targetElement !== null && nonDelegatedEvents.has(topLevelType)) {
379 + eventSystemFlags |= IS_NON_DELEGATED;
380 + target = targetElement;
381 }
382 + const listenerMap = getEventListenerMap(target);
383 const listenerMapKey = getListenerMapKey(
384 topLevelType,
385 isCapturePhaseListener,
@@ -375,50 +412,51 @@ export function listenToNativeEvent(
412 isCapturePhaseListener,
413 false,
414 isPassiveListener,
378 - priority,
415 + listenerPriority,
416 );
417 listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
418 }
419 }
420
384 -function isCaptureRegistrationName(registrationName: string): boolean {
385 - const len = registrationName.length;
386 - return registrationName.substr(len - 7) === 'Capture';
387 -}
388 -
421 export function listenToReactEvent(
390 - reactPropEvent: string,
422 + reactEvent: string,
423 rootContainerElement: Element,
424 + targetElement: Element | null,
425 ): void {
393 - const listenerMap = getEventListenerMap(rootContainerElement);
394 - // For optimization, let's check if we have the registration name
395 - // on the rootContainerElement.
396 - if (listenerMap.has(reactPropEvent)) {
397 - return;
398 - }
399 - // Add the registration name to the map, so we can avoid processing
400 - // this React prop event again.
401 - listenerMap.set(reactPropEvent, null);
402 - const dependencies = registrationNameDependencies[reactPropEvent];
426 + const dependencies = registrationNameDependencies[reactEvent];
427 const dependenciesLength = dependencies.length;
428 // If the dependencies length is 1, that means we're not using a polyfill
405 - // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin and
406 - // SelectEventPlugin. SimpleEventPlugin always only has a single dependency.
407 - // Given this, we know that we never need to apply capture phase event
408 - // listeners to anything other than the SimpleEventPlugin.
409 - const registrationCapturePhase =
410 - isCaptureRegistrationName(reactPropEvent) && dependenciesLength === 1;
429 + // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
430 + // and SelectEventPlugin. We always use the native bubble event phase for
431 + // these plugins and emulate two phase event dispatching. SimpleEventPlugin
432 + // always only has a single dependency and SimpleEventPlugin events also
433 + // use either the native capture event phase or bubble event phase, there
434 + // is no emulation (except for focus/blur, but that will be removed soon).
435 + const isPolyfillEventPlugin = dependenciesLength !== 1;
436
412 - for (let i = 0; i < dependenciesLength; i++) {
413 - const dependency = dependencies[i];
414 - const capture =
415 - capturePhaseEvents.has(dependency) || registrationCapturePhase;
437 + if (isPolyfillEventPlugin) {
438 + const listenerMap = getEventListenerMap(rootContainerElement);
439 + // For optimization, we register plugins on the listener map, so we
440 + // don't need to check each of their dependencies each time.
441 + if (!listenerMap.has(reactEvent)) {
442 + listenerMap.set(reactEvent, null);
443 + for (let i = 0; i < dependenciesLength; i++) {
444 + listenToNativeEvent(
445 + dependencies[i],
446 + false,
447 + rootContainerElement,
448 + targetElement,
449 + );
450 + }
451 + }
452 + } else {
453 + // Check if the react event ends in "Capture"
454 + const isCapturePhaseListener = reactEvent.substr(-7) === 'Capture';
455 listenToNativeEvent(
417 - dependency,
456 + dependencies[0],
457 + isCapturePhaseListener,
458 rootContainerElement,
419 - listenerMap,
420 - PLUGIN_EVENT_SYSTEM,
421 - capture,
459 + targetElement,
460 );
461 }
462 }
@@ -509,13 +547,10 @@ function addTrappedEventListener(
547 return unsubscribeListener;
548 }
549
512 -function willDeferLaterForLegacyFBSupport(
550 +function deferClickToDocumentForLegacyFBSupport(
551 topLevelType: DOMTopLevelEventType,
552 targetContainer: EventTarget,
515 -): boolean {
516 - if (topLevelType !== TOP_CLICK) {
517 - return false;
518 - }
553 +): void {
554 // We defer all click events with legacy FB support mode on.
555 // This means we add a one time event listener to trigger
556 // after the FB delegated listeners fire.
@@ -523,11 +558,10 @@ function willDeferLaterForLegacyFBSupport(
558 addTrappedEventListener(
559 targetContainer,
560 topLevelType,
526 - PLUGIN_EVENT_SYSTEM | LEGACY_FB_SUPPORT,
561 + PLUGIN_EVENT_SYSTEM | IS_LEGACY_FB_SUPPORT_MODE,
562 false,
563 isDeferredListenerForLegacyFBSupport,
564 );
530 - return true;
565 }
566
567 function isMatchingRootContainer(
@@ -549,10 +583,10 @@ export function dispatchEventForPluginEventSystem(
583 targetContainer: EventTarget,
584 ): void {
585 let ancestorInst = targetInst;
552 - if (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) {
553 - // For TargetEvent nodes (i.e. document, window)
554 - ancestorInst = null;
555 - } else {
586 + if (
587 + (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
588 + (eventSystemFlags & IS_NON_DELEGATED) === 0
589 + ) {
590 const targetContainerNode = ((targetContainer: any): Node);
591
592 // If we are using the legacy FB support flag, we
@@ -560,17 +594,15 @@ export function dispatchEventForPluginEventSystem(
594 // time event listener so we can defer the event.
595 if (
596 enableLegacyFBSupport &&
563 - // We do not want to defer if the event system has already been
564 - // set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
565 - // we call willDeferLaterForLegacyFBSupport, thus not bailing out
566 - // will result in endless cycles like an infinite loop.
567 - (eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
568 - // We also don't want to defer during event replaying.
569 - (eventSystemFlags & IS_REPLAYED) === 0 &&
570 - // We don't apply this during capture phase.
571 - (eventSystemFlags & IS_CAPTURE_PHASE) === 0 &&
572 - willDeferLaterForLegacyFBSupport(topLevelType, targetContainer)
597 + // If our event flags match the required flags for entering
598 + // FB legacy mode and we are prcocessing the "click" event,
599 + // then we can defer the event to the "document", to allow
600 + // for legacy FB support, where the expected behavior was to
601 + // match React < 16 behavior of delegated clicks to the doc.
602 + topLevelType === TOP_CLICK &&
603 + (eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
604 ) {
605 + deferClickToDocumentForLegacyFBSupport(topLevelType, targetContainer);
606 return;
607 }
608 if (targetInst !== null) {
@@ -691,11 +723,6 @@ export function accumulateSinglePhaseListeners(
723 let instance = targetFiber;
724 let lastHostComponent = null;
725 const targetType = event.type;
694 - // shouldEmulateTwoPhase is temporary till we can polyfill focus/blur to
695 - // focusin/focusout.
696 - const shouldEmulateTwoPhase = capturePhaseEvents.has(
697 - ((targetType: any): DOMTopLevelEventType),
698 - );
726
727 // Accumulate all instances and listeners via the target -> root path.
728 while (instance !== null) {
@@ -721,17 +748,10 @@ export function accumulateSinglePhaseListeners(
748 listeners.push(
749 createDispatchListener(instance, callback, currentTarget),
750 );
724 - } else if (!isCapturePhaseListener) {
725 - const entry = createDispatchListener(
726 - instance,
727 - callback,
728 - currentTarget,
751 + } else if (!isCapturePhaseListener && !inCapturePhase) {
752 + listeners.push(
753 + createDispatchListener(instance, callback, currentTarget),
754 );
730 - if (shouldEmulateTwoPhase) {
731 - listeners.unshift(entry);
732 - } else if (!inCapturePhase) {
733 - listeners.push(entry);
734 - }
755 }
756 }
757 }
@@ -746,19 +766,12 @@ export function accumulateSinglePhaseListeners(
766 );
767 }
768 }
749 - if (bubbled !== null) {
769 + if (bubbled !== null && !inCapturePhase) {
770 const bubbleListener = getListener(instance, bubbled);
771 if (bubbleListener != null) {
752 - const entry = createDispatchListener(
753 - instance,
754 - bubbleListener,
755 - currentTarget,
772 + listeners.push(
773 + createDispatchListener(instance, bubbleListener, currentTarget),
774 );
757 - if (shouldEmulateTwoPhase) {
758 - listeners.unshift(entry);
759 - } else if (!inCapturePhase) {
760 - listeners.push(entry);
761 - }
775 }
776 }
777 } else if (
@@ -786,17 +799,10 @@ export function accumulateSinglePhaseListeners(
799 listeners.push(
800 createDispatchListener(instance, callback, lastCurrentTarget),
801 );
789 - } else if (!isCapturePhaseListener) {
790 - const entry = createDispatchListener(
791 - instance,
792 - callback,
793 - lastCurrentTarget,
802 + } else if (!isCapturePhaseListener && !inCapturePhase) {
803 + listeners.push(
804 + createDispatchListener(instance, callback, lastCurrentTarget),
805 );
795 - if (shouldEmulateTwoPhase) {
796 - listeners.unshift(entry);
797 - } else if (!inCapturePhase) {
798 - listeners.push(entry);
799 - }
806 }
807 }
808 }
packages/react-dom/src/events/DOMTopLevelEventTypes.js
-29
@@ -153,35 +153,6 @@ export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur');
153 export const TOP_FOCUS_IN = unsafeCastStringToDOMTopLevelType('focusin');
154 export const TOP_FOCUS_OUT = unsafeCastStringToDOMTopLevelType('focusout');
155
156 -// List of events that need to be individually attached to media elements.
157 -// Note that events in this list will *not* be listened to at the top level
158 -// unless they're explicitly listed in `ReactBrowserEventEmitter.listenTo`.
159 -export const mediaEventTypes = [
160 - TOP_ABORT,
161 - TOP_CAN_PLAY,
162 - TOP_CAN_PLAY_THROUGH,
163 - TOP_DURATION_CHANGE,
164 - TOP_EMPTIED,
165 - TOP_ENCRYPTED,
166 - TOP_ENDED,
167 - TOP_ERROR,
168 - TOP_LOADED_DATA,
169 - TOP_LOADED_METADATA,
170 - TOP_LOAD_START,
171 - TOP_PAUSE,
172 - TOP_PLAY,
173 - TOP_PLAYING,
174 - TOP_PROGRESS,
175 - TOP_RATE_CHANGE,
176 - TOP_SEEKED,
177 - TOP_SEEKING,
178 - TOP_STALLED,
179 - TOP_SUSPEND,
180 - TOP_TIME_UPDATE,
181 - TOP_VOLUME_CHANGE,
182 - TOP_WAITING,
183 -];
184 -
156 export function getRawEventName(topLevelType: DOMTopLevelEventType): string {
157 return unsafeCastDOMTopLevelTypeToString(topLevelType);
158 }
packages/react-dom/src/events/EventSystemFlags.js
+17 -5
@@ -12,9 +12,21 @@ export type EventSystemFlags = number;
12 export const PLUGIN_EVENT_SYSTEM = 1;
13 export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14 export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1 << 2;
15 -export const IS_CAPTURE_PHASE = 1 << 3;
16 -export const IS_PASSIVE = 1 << 4;
17 -export const PASSIVE_NOT_SUPPORTED = 1 << 5;
15 +export const IS_NON_DELEGATED = 1 << 3;
16 +export const IS_CAPTURE_PHASE = 1 << 4;
17 +export const IS_PASSIVE = 1 << 5;
18 export const IS_REPLAYED = 1 << 6;
19 -export const IS_FIRST_ANCESTOR = 1 << 7;
20 -export const LEGACY_FB_SUPPORT = 1 << 8;
19 +export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 7;
20 +// This is used by React Flare
21 +export const PASSIVE_NOT_SUPPORTED = 1 << 8;
22 +
23 +export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
24 + IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE;
25 +
26 +// We do not want to defer if the event system has already been
27 +// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
28 +// we call willDeferLaterForLegacyFBSupport, thus not bailing out
29 +// will result in endless cycles like an infinite loop.
30 +// We also don't want to defer during event replaying.
31 +export const SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS =
32 + IS_EVENT_HANDLE_NON_MANAGED_NODE | IS_NON_DELEGATED | IS_CAPTURE_PHASE;
packages/react-dom/src/events/ReactDOMEventListener.js
+3 -3
@@ -33,7 +33,7 @@ import {
33 import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
34 import {
35 type EventSystemFlags,
36 - LEGACY_FB_SUPPORT,
36 + IS_LEGACY_FB_SUPPORT_MODE,
37 PLUGIN_EVENT_SYSTEM,
38 RESPONDER_EVENT_SYSTEM,
39 } from './EventSystemFlags';
@@ -132,9 +132,9 @@ function dispatchDiscreteEvent(
132 ) {
133 if (
134 !enableLegacyFBSupport ||
135 - // If we have Legacy FB support, it means we've already
135 + // If we are in Legacy FB support mode, it means we've already
136 // flushed for this event and we don't need to do it again.
137 - (eventSystemFlags & LEGACY_FB_SUPPORT) === 0
137 + (eventSystemFlags & IS_LEGACY_FB_SUPPORT_MODE) === 0
138 ) {
139 flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
140 }
packages/react-dom/src/events/ReactDOMEventReplaying.js
+5 -29
@@ -108,8 +108,6 @@ import {
108 TOP_KEY_UP,
109 TOP_INPUT,
110 TOP_TEXT_INPUT,
111 - TOP_CLOSE,
112 - TOP_CANCEL,
111 TOP_COPY,
112 TOP_CUT,
113 TOP_PASTE,
@@ -129,11 +127,8 @@ import {
127 TOP_FOCUS_IN,
128 TOP_FOCUS_OUT,
129 } from './DOMTopLevelEventTypes';
132 -import {IS_REPLAYED, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags';
133 -import {
134 - listenToNativeEvent,
135 - capturePhaseEvents,
136 -} from './DOMModernPluginEventSystem';
130 +import {IS_REPLAYED} from './EventSystemFlags';
131 +import {listenToNativeEvent} from './DOMModernPluginEventSystem';
132 import {addResponderEventSystemEvent} from './DeprecatedDOMEventResponderSystem';
133
134 type QueuedReplayableEvent = {|
@@ -198,8 +193,6 @@ const discreteReplayableEvents = [
193 TOP_KEY_UP,
194 TOP_INPUT,
195 TOP_TEXT_INPUT,
201 - TOP_CLOSE,
202 - TOP_CANCEL,
196 TOP_COPY,
197 TOP_CUT,
198 TOP_PASTE,
@@ -232,16 +225,8 @@ export function isReplayableDiscreteEvent(
225 function trapReplayableEventForContainer(
226 topLevelType: DOMTopLevelEventType,
227 container: Container,
235 - listenerMap: ElementListenerMap,
228 ) {
237 - const capture = capturePhaseEvents.has(topLevelType);
238 - listenToNativeEvent(
239 - topLevelType,
240 - ((container: any): Element),
241 - listenerMap,
242 - PLUGIN_EVENT_SYSTEM,
243 - capture,
244 - );
229 + listenToNativeEvent(topLevelType, false, ((container: any): Element), null);
230 }
231
232 function trapReplayableEventForDocument(
@@ -273,23 +258,14 @@ export function eagerlyTrapReplayableEvents(
258 document: Document,
259 ) {
260 const listenerMapForDoc = getEventListenerMap(document);
276 - const listenerMapForContainer = getEventListenerMap(container);
261 // Discrete
262 discreteReplayableEvents.forEach(topLevelType => {
279 - trapReplayableEventForContainer(
280 - topLevelType,
281 - container,
282 - listenerMapForContainer,
283 - );
263 + trapReplayableEventForContainer(topLevelType, container);
264 trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
265 });
266 // Continuous
267 continuousReplayableEvents.forEach(topLevelType => {
288 - trapReplayableEventForContainer(
289 - topLevelType,
290 - container,
291 - listenerMapForContainer,
292 - );
268 + trapReplayableEventForContainer(topLevelType, container);
269 trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
270 });
271 }