Modern Event System: make on*Capture events use capture phase (#19221)
Dominic Gannaway committed
Jul 8, 2020 at 17:10 UTC
f5ea39c18269921107ea70353d86958064889bea
12 files changed
+336
-167
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+3
-8
@@ -2714,28 +2714,23 @@ describe('ReactDOMComponent', () => {
2714
2715
innerRef.current.click();
2716
2717
- // The order we receive here is not ideal since it is expected that the
2718
- // capture listener fire before all bubble listeners. Other React apps
2719
- // might depend on this.
2720
- //
2721
- // @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
2717
if (ReactFeatureFlags.enableLegacyFBSupport) {
2718
// The order will change here, as the legacy FB support adds
2719
// the event listener onto the document after the one above has.
2720
expect(eventOrder).toEqual([
2721
'document capture',
2727
- 'document bubble',
2722
+ 'outer capture',
2723
'inner capture',
2724
+ 'document bubble',
2725
'inner bubble',
2730
- 'outer capture',
2726
'outer bubble',
2727
]);
2728
} else {
2729
expect(eventOrder).toEqual([
2730
'document capture',
2731
+ 'outer capture',
2732
'inner capture',
2733
'inner bubble',
2738
- 'outer capture',
2734
'outer bubble',
2735
'document bubble',
2736
]);
packages/react-dom/src/client/ReactDOMComponent.js
+1
@@ -13,6 +13,7 @@ import {
13
registrationNameDependencies,
14
possibleRegistrationNames,
15
} from '../events/EventRegistry';
16
+
17
import {canUseDOM} from 'shared/ExecutionEnvironment';
18
import invariant from 'shared/invariant';
19
import {
packages/react-dom/src/client/ReactDOMEventHandle.js
+3
-2
@@ -26,7 +26,6 @@ import {ELEMENT_NODE} from '../shared/HTMLNodeType';
26
import {
27
listenToTopLevelEvent,
28
addEventTypeToDispatchConfig,
29
- capturePhaseEvents,
29
} from '../events/DOMModernPluginEventSystem';
30
31
import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
@@ -87,6 +86,7 @@ function registerEventOnNearestTargetContainer(
86
topLevelType: DOMTopLevelEventType,
87
passive: boolean | void,
88
priority: EventPriority | void,
89
+ capture: boolean,
90
): void {
91
// If it is, find the nearest root or portal and make it
92
// our event handle target container.
@@ -99,7 +99,6 @@ function registerEventOnNearestTargetContainer(
99
);
100
}
101
const listenerMap = getEventListenerMap(targetContainer);
102
- const capture = capturePhaseEvents.has(topLevelType);
102
listenToTopLevelEvent(
103
topLevelType,
104
targetContainer,
@@ -135,6 +134,7 @@ function registerReactDOMEvent(
134
topLevelType,
135
passive,
136
priority,
137
+ capture,
138
);
139
} else if (enableScopeAPI && isReactScope(target)) {
140
const scopeTarget = ((target: any): ReactScopeInstance);
@@ -148,6 +148,7 @@ function registerReactDOMEvent(
148
topLevelType,
149
passive,
150
priority,
151
+ capture,
152
);
153
} else if (isValidEventTarget(target)) {
154
const eventTarget = ((target: any): EventTarget);
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+231
-121
@@ -29,8 +29,8 @@ import {
29
PLUGIN_EVENT_SYSTEM,
30
LEGACY_FB_SUPPORT,
31
IS_REPLAYED,
32
- IS_TARGET_PHASE_ONLY,
32
IS_CAPTURE_PHASE,
33
+ IS_TARGET_PHASE_ONLY,
34
} from './EventSystemFlags';
35
36
import {
@@ -131,6 +131,12 @@ function extractEvents(
131
eventSystemFlags: EventSystemFlags,
132
targetContainer: null | EventTarget,
133
) {
134
+ // TODO: we should remove the concept of a "SimpleEventPlugin".
135
+ // This is the basic functionality of the event system. All
136
+ // the other plugins are essentially polyfills. So the plugin
137
+ // should probably be inlined somewhere and have its logic
138
+ // be core the to event system. This would potentially allow
139
+ // us to ship builds of React without the polyfilled plugins below.
140
ModernSimpleEventPlugin.extractEvents(
141
dispatchQueue,
142
topLevelType,
@@ -140,42 +146,64 @@ function extractEvents(
146
eventSystemFlags,
147
targetContainer,
148
);
143
- ModernEnterLeaveEventPlugin.extractEvents(
144
- dispatchQueue,
145
- topLevelType,
146
- targetInst,
147
- nativeEvent,
148
- nativeEventTarget,
149
- eventSystemFlags,
150
- targetContainer,
151
- );
152
- ModernChangeEventPlugin.extractEvents(
153
- dispatchQueue,
154
- topLevelType,
155
- targetInst,
156
- nativeEvent,
157
- nativeEventTarget,
158
- eventSystemFlags,
159
- targetContainer,
160
- );
161
- ModernSelectEventPlugin.extractEvents(
162
- dispatchQueue,
163
- topLevelType,
164
- targetInst,
165
- nativeEvent,
166
- nativeEventTarget,
167
- eventSystemFlags,
168
- targetContainer,
169
- );
170
- ModernBeforeInputEventPlugin.extractEvents(
171
- dispatchQueue,
172
- topLevelType,
173
- targetInst,
174
- nativeEvent,
175
- nativeEventTarget,
176
- eventSystemFlags,
177
- targetContainer,
178
- );
149
+ const shouldProcessPolyfillPlugins =
150
+ (eventSystemFlags & IS_CAPTURE_PHASE) === 0 ||
151
+ capturePhaseEvents.has(topLevelType);
152
+ // We don't process these events unless we are in the
153
+ // event's native "bubble" phase, which means that we're
154
+ // not in the capture phase. That's because we emulate
155
+ // the capture phase here still. This is a trade-off,
156
+ // because in an ideal world we would not emulate and use
157
+ // the phases properly, like we do with the SimpleEvent
158
+ // plugin. However, the plugins below either expect
159
+ // emulation (EnterLeave) or use state localized to that
160
+ // plugin (BeforeInput, Change, Select). The state in
161
+ // these modules complicates things, as you'll essentially
162
+ // get the case where the capture phase event might change
163
+ // state, only for the following bubble event to come in
164
+ // later and not trigger anything as the state now
165
+ // invalidates the heuristics of the event plugin. We
166
+ // could alter all these plugins to work in such ways, but
167
+ // that might cause other unknown side-effects that we
168
+ // can't forsee right now.
169
+ if (shouldProcessPolyfillPlugins) {
170
+ ModernEnterLeaveEventPlugin.extractEvents(
171
+ dispatchQueue,
172
+ topLevelType,
173
+ targetInst,
174
+ nativeEvent,
175
+ nativeEventTarget,
176
+ eventSystemFlags,
177
+ targetContainer,
178
+ );
179
+ ModernChangeEventPlugin.extractEvents(
180
+ dispatchQueue,
181
+ topLevelType,
182
+ targetInst,
183
+ nativeEvent,
184
+ nativeEventTarget,
185
+ eventSystemFlags,
186
+ targetContainer,
187
+ );
188
+ ModernSelectEventPlugin.extractEvents(
189
+ dispatchQueue,
190
+ topLevelType,
191
+ targetInst,
192
+ nativeEvent,
193
+ nativeEventTarget,
194
+ eventSystemFlags,
195
+ targetContainer,
196
+ );
197
+ ModernBeforeInputEventPlugin.extractEvents(
198
+ dispatchQueue,
199
+ topLevelType,
200
+ targetInst,
201
+ nativeEvent,
202
+ nativeEventTarget,
203
+ eventSystemFlags,
204
+ targetContainer,
205
+ );
206
+ }
207
}
208
209
export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
@@ -229,38 +257,42 @@ function executeDispatch(
257
event.currentTarget = null;
258
}
259
232
-function executeDispatchesInOrder(
260
+function processDispatchQueueItemsInOrder(
261
event: ReactSyntheticEvent,
234
- capture: DispatchQueueItemPhase,
235
- bubble: DispatchQueueItemPhase,
262
+ phase: DispatchQueueItemPhase,
263
+ inCapturePhase: boolean,
264
): void {
265
let previousInstance;
238
- // Dispatch capture phase first.
239
- for (let i = capture.length - 1; i >= 0; i--) {
240
- const {instance, currentTarget, listener} = capture[i];
241
- if (instance !== previousInstance && event.isPropagationStopped()) {
242
- return;
266
+ if (inCapturePhase) {
267
+ for (let i = phase.length - 1; i >= 0; i--) {
268
+ const {instance, currentTarget, listener} = phase[i];
269
+ if (instance !== previousInstance && event.isPropagationStopped()) {
270
+ return;
271
+ }
272
+ executeDispatch(event, listener, currentTarget);
273
+ previousInstance = instance;
274
}
244
- executeDispatch(event, listener, currentTarget);
245
- previousInstance = instance;
246
- }
247
- previousInstance = undefined;
248
- // Dispatch bubble phase second.
249
- for (let i = 0; i < bubble.length; i++) {
250
- const {instance, currentTarget, listener} = bubble[i];
251
- if (instance !== previousInstance && event.isPropagationStopped()) {
252
- return;
275
+ } else {
276
+ for (let i = 0; i < phase.length; i++) {
277
+ const {instance, currentTarget, listener} = phase[i];
278
+ if (instance !== previousInstance && event.isPropagationStopped()) {
279
+ return;
280
+ }
281
+ executeDispatch(event, listener, currentTarget);
282
+ previousInstance = instance;
283
}
254
- executeDispatch(event, listener, currentTarget);
255
- previousInstance = instance;
284
}
285
}
286
259
-export function dispatchEventsInBatch(dispatchQueue: DispatchQueue): void {
287
+export function processDispatchQueue(
288
+ dispatchQueue: DispatchQueue,
289
+ eventSystemFlags: EventSystemFlags,
290
+): void {
291
+ const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
292
for (let i = 0; i < dispatchQueue.length; i++) {
293
const dispatchQueueItem: DispatchQueueItem = dispatchQueue[i];
262
- const {event, capture, bubble} = dispatchQueueItem;
263
- executeDispatchesInOrder(event, capture, bubble);
294
+ const {event, phase} = dispatchQueueItem;
295
+ processDispatchQueueItemsInOrder(event, phase, inCapturePhase);
296
// Modern event system doesn't use pooling.
297
}
298
// This would be a good time to rethrow if any of the event handlers threw.
@@ -285,7 +317,7 @@ function dispatchEventsForPlugins(
317
eventSystemFlags,
318
targetContainer,
319
);
288
- dispatchEventsInBatch(dispatchQueue);
320
+ processDispatchQueue(dispatchQueue, eventSystemFlags);
321
}
322
323
function shouldUpgradeListener(
@@ -348,6 +380,11 @@ export function listenToTopLevelEvent(
380
}
381
}
382
383
+function isCaptureRegistrationName(registrationName: string): boolean {
384
+ const len = registrationName.length;
385
+ return registrationName.substr(len - 7) === 'Capture';
386
+}
387
+
388
export function listenToReactPropEvent(
389
reactPropEvent: string,
390
rootContainerElement: Element,
@@ -362,11 +399,19 @@ export function listenToReactPropEvent(
399
// this React prop event again.
400
listenerMap.set(reactPropEvent, null);
401
const dependencies = registrationNameDependencies[reactPropEvent];
402
+ const dependenciesLength = dependencies.length;
403
+ // If the dependencies length is 1, that means we're not using a polyfill
404
+ // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin and
405
+ // SelectEventPlugin. SimpleEventPlugin always only has a single dependency.
406
+ // Given this, we know that we never need to apply capture phase event
407
+ // listeners to anything other than the SimpleEventPlugin.
408
+ const registrationCapturePhase =
409
+ isCaptureRegistrationName(reactPropEvent) && dependenciesLength === 1;
410
366
- for (let i = 0; i < dependencies.length; i++) {
411
+ for (let i = 0; i < dependenciesLength; i++) {
412
const dependency = dependencies[i];
368
- const capture = capturePhaseEvents.has(dependency);
369
-
413
+ const capture =
414
+ capturePhaseEvents.has(dependency) || registrationCapturePhase;
415
listenToTopLevelEvent(
416
dependency,
417
rootContainerElement,
@@ -524,6 +569,8 @@ export function dispatchEventForPluginEventSystem(
569
(eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
570
// We also don't want to defer during event replaying.
571
(eventSystemFlags & IS_REPLAYED) === 0 &&
572
+ // We don't apply this during capture phase.
573
+ (eventSystemFlags & IS_CAPTURE_PHASE) === 0 &&
574
willDeferLaterForLegacyFBSupport(topLevelType, targetContainer)
575
) {
576
return;
@@ -622,26 +669,23 @@ function createDispatchQueueItemPhaseEntry(
669
670
function createDispatchQueueItem(
671
event: ReactSyntheticEvent,
625
- capture: DispatchQueueItemPhase,
626
- bubble: DispatchQueueItemPhase,
672
+ phase: DispatchQueueItemPhase,
673
): DispatchQueueItem {
674
return {
675
event,
630
- capture,
631
- bubble,
676
+ phase,
677
};
678
}
679
635
-export function accumulateTwoPhaseListeners(
680
+export function accumulateSinglePhaseListeners(
681
targetFiber: Fiber | null,
682
dispatchQueue: DispatchQueue,
683
event: ReactSyntheticEvent,
639
- accumulateEventHandleListeners?: boolean,
684
+ inCapturePhase: boolean,
685
): void {
686
const bubbled = event._reactName;
687
const captured = bubbled !== null ? bubbled + 'Capture' : null;
643
- const capturePhase: DispatchQueueItemPhase = [];
644
- const bubblePhase: DispatchQueueItemPhase = [];
688
+ const phase: DispatchQueueItemPhase = [];
689
690
// If we are not handling EventTarget only phase, then we're doing the
691
// usual two phase accumulation using the React fiber tree to pick up
@@ -649,6 +693,11 @@ export function accumulateTwoPhaseListeners(
693
let instance = targetFiber;
694
let lastHostComponent = null;
695
const targetType = event.type;
696
+ // shouldEmulateTwoPhase is temporary till we can polyfill focus/blur to
697
+ // focusin/focusout.
698
+ const shouldEmulateTwoPhase = capturePhaseEvents.has(
699
+ ((targetType: any): DOMTopLevelEventType),
700
+ );
701
702
// Accumulate all instances and listeners via the target -> root path.
703
while (instance !== null) {
@@ -658,7 +707,7 @@ export function accumulateTwoPhaseListeners(
707
const currentTarget = stateNode;
708
lastHostComponent = currentTarget;
709
// For Event Handle listeners
661
- if (enableCreateEventHandleAPI && accumulateEventHandleListeners) {
710
+ if (enableCreateEventHandleAPI) {
711
const listeners = getEventHandlerListeners(currentTarget);
712
713
if (listeners !== null) {
@@ -667,32 +716,35 @@ export function accumulateTwoPhaseListeners(
716
const listener = listenersArr[i];
717
const {callback, capture, type} = listener;
718
if (type === targetType) {
670
- if (capture === true) {
671
- capturePhase.push(
719
+ if (capture && inCapturePhase) {
720
+ phase.push(
721
createDispatchQueueItemPhaseEntry(
722
instance,
723
callback,
724
currentTarget,
725
),
726
);
678
- } else {
679
- bubblePhase.push(
680
- createDispatchQueueItemPhaseEntry(
681
- instance,
682
- callback,
683
- currentTarget,
684
- ),
727
+ } else if (!capture) {
728
+ const entry = createDispatchQueueItemPhaseEntry(
729
+ instance,
730
+ callback,
731
+ currentTarget,
732
);
733
+ if (shouldEmulateTwoPhase) {
734
+ phase.unshift(entry);
735
+ } else if (!inCapturePhase) {
736
+ phase.push(entry);
737
+ }
738
}
739
}
740
}
741
}
742
}
743
// Standard React on* listeners, i.e. onClick prop
692
- if (captured !== null) {
744
+ if (captured !== null && inCapturePhase) {
745
const captureListener = getListener(instance, captured);
746
if (captureListener != null) {
695
- capturePhase.push(
747
+ phase.push(
748
createDispatchQueueItemPhaseEntry(
749
instance,
750
captureListener,
@@ -704,19 +756,21 @@ export function accumulateTwoPhaseListeners(
756
if (bubbled !== null) {
757
const bubbleListener = getListener(instance, bubbled);
758
if (bubbleListener != null) {
707
- bubblePhase.push(
708
- createDispatchQueueItemPhaseEntry(
709
- instance,
710
- bubbleListener,
711
- currentTarget,
712
- ),
759
+ const entry = createDispatchQueueItemPhaseEntry(
760
+ instance,
761
+ bubbleListener,
762
+ currentTarget,
763
);
764
+ if (shouldEmulateTwoPhase) {
765
+ phase.unshift(entry);
766
+ } else if (!inCapturePhase) {
767
+ phase.push(entry);
768
+ }
769
}
770
}
771
} else if (
772
enableCreateEventHandleAPI &&
773
enableScopeAPI &&
719
- accumulateEventHandleListeners &&
774
tag === ScopeComponent &&
775
lastHostComponent !== null
776
) {
@@ -730,22 +784,25 @@ export function accumulateTwoPhaseListeners(
784
const listener = listenersArr[i];
785
const {callback, capture, type} = listener;
786
if (type === targetType) {
733
- if (capture === true) {
734
- capturePhase.push(
787
+ if (capture && inCapturePhase) {
788
+ phase.push(
789
createDispatchQueueItemPhaseEntry(
790
instance,
791
callback,
792
lastCurrentTarget,
793
),
794
);
741
- } else {
742
- bubblePhase.push(
743
- createDispatchQueueItemPhaseEntry(
744
- instance,
745
- callback,
746
- lastCurrentTarget,
747
- ),
795
+ } else if (!capture) {
796
+ const entry = createDispatchQueueItemPhaseEntry(
797
+ instance,
798
+ callback,
799
+ lastCurrentTarget,
800
);
801
+ if (shouldEmulateTwoPhase) {
802
+ phase.unshift(entry);
803
+ } else if (!inCapturePhase) {
804
+ phase.push(entry);
805
+ }
806
}
807
}
808
}
@@ -753,10 +810,64 @@ export function accumulateTwoPhaseListeners(
810
}
811
instance = instance.return;
812
}
756
- if (capturePhase.length !== 0 || bubblePhase.length !== 0) {
757
- dispatchQueue.push(
758
- createDispatchQueueItem(event, capturePhase, bubblePhase),
759
- );
813
+ if (phase.length !== 0) {
814
+ dispatchQueue.push(createDispatchQueueItem(event, phase));
815
+ }
816
+}
817
+
818
+// We should only use this function for:
819
+// - ModernBeforeInputEventPlugin
820
+// - ModernChangeEventPlugin
821
+// - ModernSelectEventPlugin
822
+// This is because we only process these plugins
823
+// in the bubble phase, so we need to accumulate two
824
+// phase event listeners (via emulation).
825
+export function accumulateTwoPhaseListeners(
826
+ targetFiber: Fiber | null,
827
+ dispatchQueue: DispatchQueue,
828
+ event: ReactSyntheticEvent,
829
+): void {
830
+ const bubbled = event._reactName;
831
+ const captured = bubbled !== null ? bubbled + 'Capture' : null;
832
+ const phase: DispatchQueueItemPhase = [];
833
+ let instance = targetFiber;
834
+
835
+ // Accumulate all instances and listeners via the target -> root path.
836
+ while (instance !== null) {
837
+ const {stateNode, tag} = instance;
838
+ // Handle listeners that are on HostComponents (i.e. <div>)
839
+ if (tag === HostComponent && stateNode !== null) {
840
+ const currentTarget = stateNode;
841
+ // Standard React on* listeners, i.e. onClick prop
842
+ if (captured !== null) {
843
+ const captureListener = getListener(instance, captured);
844
+ if (captureListener != null) {
845
+ phase.unshift(
846
+ createDispatchQueueItemPhaseEntry(
847
+ instance,
848
+ captureListener,
849
+ currentTarget,
850
+ ),
851
+ );
852
+ }
853
+ }
854
+ if (bubbled !== null) {
855
+ const bubbleListener = getListener(instance, bubbled);
856
+ if (bubbleListener != null) {
857
+ phase.push(
858
+ createDispatchQueueItemPhaseEntry(
859
+ instance,
860
+ bubbleListener,
861
+ currentTarget,
862
+ ),
863
+ );
864
+ }
865
+ }
866
+ }
867
+ instance = instance.return;
868
+ }
869
+ if (phase.length !== 0) {
870
+ dispatchQueue.push(createDispatchQueueItem(event, phase));
871
}
872
}
873
@@ -829,8 +940,7 @@ function accumulateEnterLeaveListenersForEvent(
940
if (registrationName === undefined) {
941
return;
942
}
832
- const capturePhase: DispatchQueueItemPhase = [];
833
- const bubblePhase: DispatchQueueItemPhase = [];
943
+ const phase: DispatchQueueItemPhase = [];
944
945
let instance = target;
946
while (instance !== null) {
@@ -846,7 +956,7 @@ function accumulateEnterLeaveListenersForEvent(
956
if (capture) {
957
const captureListener = getListener(instance, registrationName);
958
if (captureListener != null) {
849
- capturePhase.push(
959
+ phase.unshift(
960
createDispatchQueueItemPhaseEntry(
961
instance,
962
captureListener,
@@ -854,10 +964,10 @@ function accumulateEnterLeaveListenersForEvent(
964
),
965
);
966
}
857
- } else {
967
+ } else if (!capture) {
968
const bubbleListener = getListener(instance, registrationName);
969
if (bubbleListener != null) {
860
- bubblePhase.push(
970
+ phase.push(
971
createDispatchQueueItemPhaseEntry(
972
instance,
973
bubbleListener,
@@ -869,14 +979,17 @@ function accumulateEnterLeaveListenersForEvent(
979
}
980
instance = instance.return;
981
}
872
- if (capturePhase.length !== 0 || bubblePhase.length !== 0) {
873
- dispatchQueue.push(
874
- createDispatchQueueItem(event, capturePhase, bubblePhase),
875
- );
982
+ if (phase.length !== 0) {
983
+ dispatchQueue.push(createDispatchQueueItem(event, phase));
984
}
985
}
986
879
-export function accumulateEnterLeaveListeners(
987
+// We should only use this function for:
988
+// - ModernEnterLeaveEventPlugin
989
+// This is because we only process this plugin
990
+// in the bubble phase, so we need to accumulate two
991
+// phase event listeners.
992
+export function accumulateEnterLeaveTwoPhaseListeners(
993
dispatchQueue: DispatchQueue,
994
leaveEvent: ReactSyntheticEvent,
995
enterEvent: null | ReactSyntheticEvent,
@@ -911,8 +1024,7 @@ export function accumulateEventHandleTargetListeners(
1024
currentTarget: EventTarget,
1025
inCapturePhase: boolean,
1026
): void {
914
- const capturePhase: DispatchQueueItemPhase = [];
915
- const bubblePhase: DispatchQueueItemPhase = [];
1027
+ const phase: DispatchQueueItemPhase = [];
1028
1029
const eventListeners = getEventHandlerListeners(currentTarget);
1030
if (eventListeners !== null) {
@@ -924,21 +1036,19 @@ export function accumulateEventHandleTargetListeners(
1036
const {callback, capture, type} = listener;
1037
if (type === targetType) {
1038
if (inCapturePhase && capture) {
927
- capturePhase.push(
1039
+ phase.push(
1040
createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
1041
);
1042
} else if (!inCapturePhase && !capture) {
931
- bubblePhase.push(
1043
+ phase.push(
1044
createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
1045
);
1046
}
1047
}
1048
}
1049
}
938
- if (capturePhase.length !== 0 || bubblePhase.length !== 0) {
939
- dispatchQueue.push(
940
- createDispatchQueueItem(event, capturePhase, bubblePhase),
941
- );
1050
+ if (phase.length !== 0) {
1051
+ dispatchQueue.push(createDispatchQueueItem(event, phase));
1052
}
1053
}
1054
packages/react-dom/src/events/PluginModuleType.js
+1
-2
@@ -26,8 +26,7 @@ export type DispatchQueueItemPhase = Array<DispatchQueueItemPhaseEntry>;
26
27
export type DispatchQueueItem = {|
28
event: ReactSyntheticEvent,
29
- capture: DispatchQueueItemPhase,
30
- bubble: DispatchQueueItemPhase,
29
+ phase: DispatchQueueItemPhase,
30
|};
31
32
export type DispatchQueue = Array<DispatchQueueItem>;
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+25
-25
@@ -189,9 +189,9 @@ describe('DOMModernPluginEventSystem', () => {
189
dispatchClickEvent(divElement);
190
expect(onClick).toHaveBeenCalledTimes(3);
191
expect(onClickCapture).toHaveBeenCalledTimes(3);
192
- expect(log[2]).toEqual(['capture', divElement]);
193
- expect(log[3]).toEqual(['bubble', divElement]);
194
- expect(log[4]).toEqual(['capture', buttonElement]);
192
+ expect(log[2]).toEqual(['capture', buttonElement]);
193
+ expect(log[3]).toEqual(['capture', divElement]);
194
+ expect(log[4]).toEqual(['bubble', divElement]);
195
expect(log[5]).toEqual(['bubble', buttonElement]);
196
});
197
@@ -241,9 +241,9 @@ describe('DOMModernPluginEventSystem', () => {
241
dispatchClickEvent(divElement);
242
expect(onClick).toHaveBeenCalledTimes(3);
243
expect(onClickCapture).toHaveBeenCalledTimes(3);
244
- expect(log[2]).toEqual(['capture', divElement]);
245
- expect(log[3]).toEqual(['bubble', divElement]);
246
- expect(log[4]).toEqual(['capture', buttonElement]);
244
+ expect(log[2]).toEqual(['capture', buttonElement]);
245
+ expect(log[3]).toEqual(['capture', divElement]);
246
+ expect(log[4]).toEqual(['bubble', divElement]);
247
expect(log[5]).toEqual(['bubble', buttonElement]);
248
});
249
@@ -320,9 +320,9 @@ describe('DOMModernPluginEventSystem', () => {
320
dispatchClickEvent(divElement);
321
expect(onClick).toHaveBeenCalledTimes(3);
322
expect(onClickCapture).toHaveBeenCalledTimes(3);
323
- expect(log[2]).toEqual(['capture', divElement]);
324
- expect(log[3]).toEqual(['bubble', divElement]);
325
- expect(log[4]).toEqual(['capture', buttonElement]);
323
+ expect(log[2]).toEqual(['capture', buttonElement]);
324
+ expect(log[3]).toEqual(['capture', divElement]);
325
+ expect(log[4]).toEqual(['bubble', divElement]);
326
expect(log[5]).toEqual(['bubble', buttonElement]);
327
328
// Inside <Parent />
@@ -330,9 +330,9 @@ describe('DOMModernPluginEventSystem', () => {
330
dispatchClickEvent(buttonElement2);
331
expect(onClick).toHaveBeenCalledTimes(5);
332
expect(onClickCapture).toHaveBeenCalledTimes(5);
333
- expect(log[6]).toEqual(['capture', buttonElement2]);
334
- expect(log[7]).toEqual(['bubble', buttonElement2]);
335
- expect(log[8]).toEqual(['capture', buttonElement]);
333
+ expect(log[6]).toEqual(['capture', buttonElement]);
334
+ expect(log[7]).toEqual(['capture', buttonElement2]);
335
+ expect(log[8]).toEqual(['bubble', buttonElement2]);
336
expect(log[9]).toEqual(['bubble', buttonElement]);
337
});
338
@@ -385,9 +385,9 @@ describe('DOMModernPluginEventSystem', () => {
385
dispatchClickEvent(divElement);
386
expect(onClick).toHaveBeenCalledTimes(3);
387
expect(onClickCapture).toHaveBeenCalledTimes(3);
388
- expect(log[2]).toEqual(['capture', divElement]);
389
- expect(log[3]).toEqual(['bubble', divElement]);
390
- expect(log[4]).toEqual(['capture', buttonElement]);
388
+ expect(log[2]).toEqual(['capture', buttonElement]);
389
+ expect(log[3]).toEqual(['capture', divElement]);
390
+ expect(log[4]).toEqual(['bubble', divElement]);
391
expect(log[5]).toEqual(['bubble', buttonElement]);
392
});
393
@@ -442,9 +442,9 @@ describe('DOMModernPluginEventSystem', () => {
442
dispatchClickEvent(divElement);
443
expect(onClick).toHaveBeenCalledTimes(3);
444
expect(onClickCapture).toHaveBeenCalledTimes(3);
445
- expect(log[2]).toEqual(['capture', divElement]);
446
- expect(log[3]).toEqual(['bubble', divElement]);
447
- expect(log[4]).toEqual(['capture', buttonElement]);
445
+ expect(log[2]).toEqual(['capture', buttonElement]);
446
+ expect(log[3]).toEqual(['capture', divElement]);
447
+ expect(log[4]).toEqual(['bubble', divElement]);
448
expect(log[5]).toEqual(['bubble', buttonElement]);
449
});
450
@@ -757,7 +757,7 @@ describe('DOMModernPluginEventSystem', () => {
757
const divElement = divRef.current;
758
dispatchClickEvent(divElement);
759
expect(onClick).toHaveBeenCalledTimes(1);
760
- expect(onClickCapture).toHaveBeenCalledTimes(1);
760
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
761
762
document.body.removeChild(portalElement);
763
});
@@ -1179,7 +1179,7 @@ describe('DOMModernPluginEventSystem', () => {
1179
if (enableLegacyFBSupport) {
1180
// We aren't using roots with legacyFBSupport, we put clicks on the document, so we exbit the previous
1181
// behavior.
1182
- expect(log).toEqual([]);
1182
+ expect(log).toEqual(['capture root', 'capture portal']);
1183
} else {
1184
expect(log).toEqual([
1185
// The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
@@ -2288,8 +2288,8 @@ describe('DOMModernPluginEventSystem', () => {
2288
if (enableLegacyFBSupport) {
2289
expect(log[0]).toEqual(['capture', window]);
2290
expect(log[1]).toEqual(['capture', document]);
2291
- expect(log[2]).toEqual(['bubble', document]);
2292
- expect(log[3]).toEqual(['capture', buttonElement]);
2291
+ expect(log[2]).toEqual(['capture', buttonElement]);
2292
+ expect(log[3]).toEqual(['bubble', document]);
2293
expect(log[4]).toEqual(['bubble', buttonElement]);
2294
expect(log[5]).toEqual(['bubble', window]);
2295
} else {
@@ -2313,9 +2313,9 @@ describe('DOMModernPluginEventSystem', () => {
2313
if (enableLegacyFBSupport) {
2314
expect(log[0]).toEqual(['capture', window]);
2315
expect(log[1]).toEqual(['capture', document]);
2316
- expect(log[2]).toEqual(['bubble', document]);
2317
- expect(log[3]).toEqual(['capture', buttonElement]);
2318
- expect(log[4]).toEqual(['capture', divElement]);
2316
+ expect(log[2]).toEqual(['capture', buttonElement]);
2317
+ expect(log[3]).toEqual(['capture', divElement]);
2318
+ expect(log[4]).toEqual(['bubble', document]);
2319
expect(log[5]).toEqual(['bubble', divElement]);
2320
expect(log[6]).toEqual(['bubble', buttonElement]);
2321
expect(log[7]).toEqual(['bubble', window]);
packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js
+2
-2
@@ -36,7 +36,7 @@ import {enqueueStateRestore} from '../ReactDOMControlledComponent';
36
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
37
import {batchedUpdates} from '../ReactDOMUpdateBatching';
38
import {
39
- dispatchEventsInBatch,
39
+ processDispatchQueue,
40
accumulateTwoPhaseListeners,
41
} from '../DOMModernPluginEventSystem';
42
@@ -106,7 +106,7 @@ function manualDispatchChangeEvent(nativeEvent) {
106
}
107
108
function runEventInBatch(dispatchQueue) {
109
- dispatchEventsInBatch(dispatchQueue);
109
+ processDispatchQueue(dispatchQueue, 0);
110
}
111
112
function getInstIfValueChanged(targetInst: Object) {
packages/react-dom/src/events/plugins/ModernEnterLeaveEventPlugin.js
+2
-2
@@ -19,7 +19,7 @@ import {
19
getClosestInstanceFromNode,
20
getNodeFromInstance,
21
} from '../../client/ReactDOMComponentTree';
22
-import {accumulateEnterLeaveListeners} from '../DOMModernPluginEventSystem';
22
+import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMModernPluginEventSystem';
23
24
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
25
import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
@@ -159,7 +159,7 @@ function extractEvents(
159
enter = null;
160
}
161
162
- accumulateEnterLeaveListeners(dispatchQueue, leave, enter, from, to);
162
+ accumulateEnterLeaveTwoPhaseListeners(dispatchQueue, leave, enter, from, to);
163
}
164
165
export {registerEvents, extractEvents};
packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js
+9
-3
@@ -23,7 +23,7 @@ import {
23
registerSimpleEvents,
24
} from '../DOMEventProperties';
25
import {
26
- accumulateTwoPhaseListeners,
26
+ accumulateSinglePhaseListeners,
27
accumulateEventHandleTargetListeners,
28
} from '../DOMModernPluginEventSystem';
29
import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags';
@@ -152,13 +152,13 @@ function extractEvents(
152
nativeEventTarget,
153
);
154
155
+ const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
156
if (
157
enableCreateEventHandleAPI &&
158
eventSystemFlags !== undefined &&
159
eventSystemFlags & IS_TARGET_PHASE_ONLY &&
160
targetContainer != null
161
) {
161
- const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
162
accumulateEventHandleTargetListeners(
163
dispatchQueue,
164
event,
@@ -166,7 +166,13 @@ function extractEvents(
166
inCapturePhase,
167
);
168
} else {
169
- accumulateTwoPhaseListeners(targetInst, dispatchQueue, event, true);
169
+ // We traverse only capture or bubble phase listeners
170
+ accumulateSinglePhaseListeners(
171
+ targetInst,
172
+ dispatchQueue,
173
+ event,
174
+ inCapturePhase,
175
+ );
176
}
177
return event;
178
}
packages/react-dom/src/events/plugins/__tests__/ModernChangeEventPlugin-test.js
+24
@@ -98,6 +98,30 @@ describe('ChangeEventPlugin', () => {
98
}
99
});
100
101
+ it('should consider initial text value to be current (capture)', () => {
102
+ let called = 0;
103
+
104
+ function cb(e) {
105
+ called++;
106
+ expect(e.type).toBe('change');
107
+ }
108
+
109
+ const node = ReactDOM.render(
110
+ <input type="text" onChangeCapture={cb} defaultValue="foo" />,
111
+ container,
112
+ );
113
+ node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
114
+ node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
115
+
116
+ if (ReactFeatureFlags.disableInputAttributeSyncing) {
117
+ // TODO: figure out why. This might be a bug.
118
+ expect(called).toBe(1);
119
+ } else {
120
+ // There should be no React change events because the value stayed the same.
121
+ expect(called).toBe(0);
122
+ }
123
+ });
124
+
125
it('should consider initial checkbox checked=true to be current', () => {
126
let called = 0;
127
packages/react-dom/src/events/plugins/__tests__/ModernSelectEventPlugin-test.js
+34
@@ -108,6 +108,40 @@ describe('SelectEventPlugin', () => {
108
expect(select).toHaveBeenCalledTimes(1);
109
});
110
111
+ it('should fire `onSelectCapture` when a listener is present', () => {
112
+ const select = jest.fn();
113
+ const onSelectCapture = event => {
114
+ expect(typeof event).toBe('object');
115
+ expect(event.type).toBe('select');
116
+ expect(event.target).toBe(node);
117
+ select(event.currentTarget);
118
+ };
119
+
120
+ const node = ReactDOM.render(
121
+ <input type="text" onSelectCapture={onSelectCapture} />,
122
+ container,
123
+ );
124
+ node.focus();
125
+
126
+ let nativeEvent = new MouseEvent('focus', {
127
+ bubbles: true,
128
+ cancelable: true,
129
+ });
130
+ node.dispatchEvent(nativeEvent);
131
+ expect(select).toHaveBeenCalledTimes(0);
132
+
133
+ nativeEvent = new MouseEvent('mousedown', {
134
+ bubbles: true,
135
+ cancelable: true,
136
+ });
137
+ node.dispatchEvent(nativeEvent);
138
+ expect(select).toHaveBeenCalledTimes(0);
139
+
140
+ nativeEvent = new MouseEvent('mouseup', {bubbles: true, cancelable: true});
141
+ node.dispatchEvent(nativeEvent);
142
+ expect(select).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
// Regression test for https://github.com/facebook/react/issues/11379
146
it('should not wait for `mouseup` after receiving `dragend`', () => {
147
const select = jest.fn();
packages/react-native-renderer/src/legacy-events/PluginModuleType.js
+1
-2
@@ -45,8 +45,7 @@ export type DispatchQueueItemPhase = Array<DispatchQueueItemPhaseEntry>;
45
46
export type DispatchQueueItem = {|
47
event: ReactSyntheticEvent,
48
- capture: DispatchQueueItemPhase,
49
- bubble: DispatchQueueItemPhase,
48
+ phase: DispatchQueueItemPhase,
49
|};
50
51
export type DispatchQueue = Array<DispatchQueueItem>;