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
}