@samitouri / QOS-React-2 / commits / f5ea39c182

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>;