Replay capture phase for continuous events (#22856)
Co-authored-by: Dan Abramov <dan.abramov@me.com> Co-authored-by: Marco Salazar <salazarm@fb.com>
Dan Abramov committed
Dec 2, 2021 at 14:33 UTC
71d16750c528ad902e73e2ecb9fc05a40314bb3f
3 files changed
+558
-60
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+519
-6
@@ -684,7 +684,6 @@ describe('ReactDOMServerSelectiveHydration', () => {
684
let suspend = false;
685
let resolve;
686
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
687
-
687
function Child({text}) {
688
if ((text === 'A' || text === 'D') && suspend) {
689
throw promise;
@@ -724,11 +723,8 @@ describe('ReactDOMServerSelectiveHydration', () => {
723
</div>
724
);
725
}
727
-
726
const finalHTML = ReactDOMServer.renderToString(<App />);
729
-
727
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
731
-
728
const container = document.createElement('div');
729
// We need this to be in the document since we'll dispatch events on it.
730
document.body.appendChild(container);
@@ -746,6 +742,156 @@ describe('ReactDOMServerSelectiveHydration', () => {
742
const root = ReactDOM.createRoot(container, {hydrate: true});
743
root.render(<App />);
744
745
+ // Nothing has been hydrated so far.
746
+ expect(Scheduler).toHaveYielded([]);
747
+ // Click D
748
+ dispatchMouseHoverEvent(spanD, null);
749
+ dispatchClickEvent(spanD);
750
+ // Hover over B and then C.
751
+ dispatchMouseHoverEvent(spanB, spanD);
752
+ dispatchMouseHoverEvent(spanC, spanB);
753
+ expect(Scheduler).toHaveYielded(['App']);
754
+ await act(async () => {
755
+ suspend = false;
756
+ resolve();
757
+ await promise;
758
+ });
759
+ if (
760
+ gate(
761
+ flags =>
762
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
763
+ )
764
+ ) {
765
+ // We should prioritize hydrating D first because we clicked it.
766
+ // but event isnt replayed
767
+ expect(Scheduler).toHaveYielded([
768
+ 'D',
769
+ 'B', // Ideally this should be later.
770
+ 'C',
771
+ 'Hover C',
772
+ 'A',
773
+ ]);
774
+ } else {
775
+ // We should prioritize hydrating D first because we clicked it.
776
+ // Next we should hydrate C since that's the current hover target.
777
+ // To simplify implementation details we hydrate both B and C at
778
+ // the same time since B was already scheduled.
779
+ // This is ok because it will at least not continue for nested
780
+ // boundary. See the next test below.
781
+ expect(Scheduler).toHaveYielded([
782
+ 'D',
783
+ 'Clicked D',
784
+ 'B', // Ideally this should be later.
785
+ 'C',
786
+ 'Hover C',
787
+ 'A',
788
+ ]);
789
+ }
790
+
791
+ document.body.removeChild(container);
792
+ });
793
+
794
+ it('replays capture phase for continuous events and respects stopPropagation', async () => {
795
+ let suspend = false;
796
+ let resolve;
797
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
798
+
799
+ function Child({text}) {
800
+ if ((text === 'A' || text === 'D') && suspend) {
801
+ throw promise;
802
+ }
803
+ Scheduler.unstable_yieldValue(text);
804
+ return (
805
+ <span
806
+ id={text}
807
+ onClickCapture={e => {
808
+ e.preventDefault();
809
+ Scheduler.unstable_yieldValue('Capture Clicked ' + text);
810
+ }}
811
+ onClick={e => {
812
+ e.preventDefault();
813
+ Scheduler.unstable_yieldValue('Clicked ' + text);
814
+ }}
815
+ onMouseEnter={e => {
816
+ e.preventDefault();
817
+ Scheduler.unstable_yieldValue('Mouse Enter ' + text);
818
+ }}
819
+ onMouseOut={e => {
820
+ e.preventDefault();
821
+ Scheduler.unstable_yieldValue('Mouse Out ' + text);
822
+ }}
823
+ onMouseOutCapture={e => {
824
+ e.preventDefault();
825
+ e.stopPropagation();
826
+ Scheduler.unstable_yieldValue('Mouse Out Capture ' + text);
827
+ }}
828
+ onMouseOverCapture={e => {
829
+ e.preventDefault();
830
+ e.stopPropagation();
831
+ Scheduler.unstable_yieldValue('Mouse Over Capture ' + text);
832
+ }}
833
+ onMouseOver={e => {
834
+ e.preventDefault();
835
+ Scheduler.unstable_yieldValue('Mouse Over ' + text);
836
+ }}>
837
+ <div
838
+ onMouseOverCapture={e => {
839
+ e.preventDefault();
840
+ Scheduler.unstable_yieldValue('Mouse Over Capture Inner ' + text);
841
+ }}>
842
+ {text}
843
+ </div>
844
+ </span>
845
+ );
846
+ }
847
+
848
+ function App() {
849
+ Scheduler.unstable_yieldValue('App');
850
+ return (
851
+ <div
852
+ onClickCapture={e => {
853
+ e.preventDefault();
854
+ Scheduler.unstable_yieldValue('Capture Clicked Parent');
855
+ }}
856
+ onMouseOverCapture={e => {
857
+ Scheduler.unstable_yieldValue('Mouse Over Capture Parent');
858
+ }}>
859
+ <Suspense fallback="Loading...">
860
+ <Child text="A" />
861
+ </Suspense>
862
+ <Suspense fallback="Loading...">
863
+ <Child text="B" />
864
+ </Suspense>
865
+ <Suspense fallback="Loading...">
866
+ <Child text="C" />
867
+ </Suspense>
868
+ <Suspense fallback="Loading...">
869
+ <Child text="D" />
870
+ </Suspense>
871
+ </div>
872
+ );
873
+ }
874
+
875
+ const finalHTML = ReactDOMServer.renderToString(<App />);
876
+
877
+ expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
878
+
879
+ const container = document.createElement('div');
880
+ // We need this to be in the document since we'll dispatch events on it.
881
+ document.body.appendChild(container);
882
+
883
+ container.innerHTML = finalHTML;
884
+
885
+ const spanB = document.getElementById('B').firstChild;
886
+ const spanC = document.getElementById('C').firstChild;
887
+ const spanD = document.getElementById('D').firstChild;
888
+
889
+ suspend = true;
890
+
891
+ // A and D will be suspended. We'll click on D which should take
892
+ // priority, after we unsuspend.
893
+ ReactDOM.hydrateRoot(container, <App />);
894
+
895
// Nothing has been hydrated so far.
896
expect(Scheduler).toHaveYielded([]);
897
@@ -776,7 +922,14 @@ describe('ReactDOMServerSelectiveHydration', () => {
922
'D',
923
'B', // Ideally this should be later.
924
'C',
779
- 'Hover C',
925
+ // Mouse out events aren't replayed
926
+ // 'Mouse Out Capture B',
927
+ // 'Mouse Out B',
928
+ 'Mouse Over Capture Parent',
929
+ 'Mouse Over Capture C',
930
+ // Stop propagation stops these
931
+ // 'Mouse Over Capture Inner C',
932
+ // 'Mouse Over C',
933
'A',
934
]);
935
} else {
@@ -791,11 +944,371 @@ describe('ReactDOMServerSelectiveHydration', () => {
944
'Clicked D',
945
'B', // Ideally this should be later.
946
'C',
794
- 'Hover C',
947
+ // Capture phase isn't replayed
948
+ // Mouseout isn't replayed
949
+ 'Mouse Over C',
950
+ 'Mouse Enter C',
951
'A',
952
]);
953
}
954
955
+ // This test shows existing quirk where stopPropagation on mouseout
956
+ // prevents mouseEnter from firing
957
+ dispatchMouseHoverEvent(spanC, spanB);
958
+ expect(Scheduler).toHaveYielded([
959
+ 'Mouse Out Capture B',
960
+ // stopPropagation stops these
961
+ // 'Mouse Out B',
962
+ // 'Mouse Enter C',
963
+ 'Mouse Over Capture Parent',
964
+ 'Mouse Over Capture C',
965
+ // Stop propagation stops these
966
+ // 'Mouse Over Capture Inner C',
967
+ // 'Mouse Over C',
968
+ ]);
969
+
970
+ document.body.removeChild(container);
971
+ });
972
+
973
+ describe('can handle replaying events as part of multiple instances of React', () => {
974
+ let resolveInner;
975
+ let resolveOuter;
976
+ let innerPromise;
977
+ let outerPromise;
978
+ let OuterScheduler;
979
+ let InnerScheduler;
980
+ let innerDiv;
981
+
982
+ beforeEach(async () => {
983
+ document.body.innerHTML = '';
984
+ jest.resetModuleRegistry();
985
+ let OuterReactDOM;
986
+ let InnerReactDOM;
987
+ jest.isolateModules(() => {
988
+ OuterReactDOM = require('react-dom');
989
+ OuterScheduler = require('scheduler');
990
+ });
991
+ jest.isolateModules(() => {
992
+ InnerReactDOM = require('react-dom');
993
+ InnerScheduler = require('scheduler');
994
+ });
995
+
996
+ expect(OuterReactDOM).not.toBe(InnerReactDOM);
997
+ expect(OuterScheduler).not.toBe(InnerScheduler);
998
+
999
+ const outerContainer = document.createElement('div');
1000
+ const innerContainer = document.createElement('div');
1001
+
1002
+ let suspendOuter = false;
1003
+ outerPromise = new Promise(res => {
1004
+ resolveOuter = () => {
1005
+ suspendOuter = false;
1006
+ res();
1007
+ };
1008
+ });
1009
+
1010
+ function Outer() {
1011
+ if (suspendOuter) {
1012
+ OuterScheduler.unstable_yieldValue('Suspend Outer');
1013
+ throw outerPromise;
1014
+ }
1015
+ OuterScheduler.unstable_yieldValue('Outer');
1016
+ const innerRoot = outerContainer.querySelector('#inner-root');
1017
+ return (
1018
+ <div
1019
+ id="inner-root"
1020
+ onMouseEnter={() => {
1021
+ Scheduler.unstable_yieldValue('Outer Mouse Enter');
1022
+ }}
1023
+ dangerouslySetInnerHTML={{
1024
+ __html: innerRoot ? innerRoot.innerHTML : '',
1025
+ }}
1026
+ />
1027
+ );
1028
+ }
1029
+ const OuterApp = () => {
1030
+ return (
1031
+ <Suspense fallback={<div>Loading</div>}>
1032
+ <Outer />
1033
+ </Suspense>
1034
+ );
1035
+ };
1036
+
1037
+ let suspendInner = false;
1038
+ innerPromise = new Promise(res => {
1039
+ resolveInner = () => {
1040
+ suspendInner = false;
1041
+ res();
1042
+ };
1043
+ });
1044
+ function Inner() {
1045
+ if (suspendInner) {
1046
+ InnerScheduler.unstable_yieldValue('Suspend Inner');
1047
+ throw innerPromise;
1048
+ }
1049
+ InnerScheduler.unstable_yieldValue('Inner');
1050
+ return (
1051
+ <div
1052
+ id="inner"
1053
+ onMouseEnter={() => {
1054
+ Scheduler.unstable_yieldValue('Inner Mouse Enter');
1055
+ }}
1056
+ />
1057
+ );
1058
+ }
1059
+ const InnerApp = () => {
1060
+ return (
1061
+ <Suspense fallback={<div>Loading</div>}>
1062
+ <Inner />
1063
+ </Suspense>
1064
+ );
1065
+ };
1066
+
1067
+ document.body.appendChild(outerContainer);
1068
+ const outerHTML = ReactDOMServer.renderToString(<OuterApp />);
1069
+ outerContainer.innerHTML = outerHTML;
1070
+
1071
+ const innerWrapper = document.querySelector('#inner-root');
1072
+ innerWrapper.appendChild(innerContainer);
1073
+ const innerHTML = ReactDOMServer.renderToString(<InnerApp />);
1074
+ innerContainer.innerHTML = innerHTML;
1075
+
1076
+ expect(OuterScheduler).toHaveYielded(['Outer']);
1077
+ expect(InnerScheduler).toHaveYielded(['Inner']);
1078
+
1079
+ suspendOuter = true;
1080
+ suspendInner = true;
1081
+
1082
+ OuterReactDOM.hydrateRoot(outerContainer, <OuterApp />);
1083
+ InnerReactDOM.hydrateRoot(innerContainer, <InnerApp />);
1084
+
1085
+ expect(OuterScheduler).toFlushAndYield(['Suspend Outer']);
1086
+ expect(InnerScheduler).toFlushAndYield(['Suspend Inner']);
1087
+
1088
+ innerDiv = document.querySelector('#inner');
1089
+
1090
+ dispatchClickEvent(innerDiv);
1091
+
1092
+ await act(async () => {
1093
+ jest.runAllTimers();
1094
+ Scheduler.unstable_flushAllWithoutAsserting();
1095
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1096
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1097
+ });
1098
+
1099
+ expect(OuterScheduler).toHaveYielded(['Suspend Outer']);
1100
+ if (
1101
+ gate(
1102
+ flags =>
1103
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
1104
+ )
1105
+ ) {
1106
+ // InnerApp doesn't see the event because OuterApp calls stopPropagation in
1107
+ // capture phase since the event is blocked on suspended component
1108
+ expect(InnerScheduler).toHaveYielded([]);
1109
+ } else {
1110
+ // no stopPropagation
1111
+ expect(InnerScheduler).toHaveYielded(['Suspend Inner']);
1112
+ }
1113
+
1114
+ expect(Scheduler).toHaveYielded([]);
1115
+ });
1116
+ afterEach(async () => {
1117
+ document.body.innerHTML = '';
1118
+ });
1119
+
1120
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1121
+ it('Inner hydrates first then Outer', async () => {
1122
+ dispatchMouseHoverEvent(innerDiv);
1123
+
1124
+ await act(async () => {
1125
+ resolveInner();
1126
+ await innerPromise;
1127
+ jest.runAllTimers();
1128
+ Scheduler.unstable_flushAllWithoutAsserting();
1129
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1130
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1131
+ });
1132
+
1133
+ expect(OuterScheduler).toHaveYielded(['Suspend Outer']);
1134
+ // Inner App renders because it is unblocked
1135
+ expect(InnerScheduler).toHaveYielded(['Inner']);
1136
+ // No event is replayed yet
1137
+ expect(Scheduler).toHaveYielded([]);
1138
+
1139
+ dispatchMouseHoverEvent(innerDiv);
1140
+ expect(OuterScheduler).toHaveYielded([]);
1141
+ expect(InnerScheduler).toHaveYielded([]);
1142
+ // No event is replayed yet
1143
+ expect(Scheduler).toHaveYielded([]);
1144
+
1145
+ await act(async () => {
1146
+ resolveOuter();
1147
+ await outerPromise;
1148
+ jest.runAllTimers();
1149
+ Scheduler.unstable_flushAllWithoutAsserting();
1150
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1151
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1152
+ });
1153
+
1154
+ // Nothing happens to inner app yet.
1155
+ // Its blocked on the outer app replaying the event
1156
+ expect(InnerScheduler).toHaveYielded([]);
1157
+ // Outer hydrates and schedules Replay
1158
+ expect(OuterScheduler).toHaveYielded(['Outer']);
1159
+ // No event is replayed yet
1160
+ expect(Scheduler).toHaveYielded([]);
1161
+
1162
+ // fire scheduled Replay
1163
+ await act(async () => {
1164
+ jest.runAllTimers();
1165
+ Scheduler.unstable_flushAllWithoutAsserting();
1166
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1167
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1168
+ });
1169
+
1170
+ // First Inner Mouse Enter fires then Outer Mouse Enter
1171
+ expect(Scheduler).toHaveYielded([
1172
+ 'Inner Mouse Enter',
1173
+ 'Outer Mouse Enter',
1174
+ ]);
1175
+ });
1176
+
1177
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1178
+ it('Outer hydrates first then Inner', async () => {
1179
+ dispatchMouseHoverEvent(innerDiv);
1180
+
1181
+ await act(async () => {
1182
+ resolveOuter();
1183
+ await outerPromise;
1184
+ Scheduler.unstable_flushAllWithoutAsserting();
1185
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1186
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1187
+ });
1188
+
1189
+ // Outer resolves and scheduled replay
1190
+ expect(OuterScheduler).toHaveYielded(['Outer']);
1191
+ // Inner App is still blocked
1192
+ expect(InnerScheduler).toHaveYielded([]);
1193
+
1194
+ // Replay outer event
1195
+ await act(async () => {
1196
+ Scheduler.unstable_flushAllWithoutAsserting();
1197
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1198
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1199
+ });
1200
+
1201
+ // Inner is still blocked so when Outer replays the event in capture phase
1202
+ // inner ends up caling stopPropagation
1203
+ expect(Scheduler).toHaveYielded([]);
1204
+ expect(OuterScheduler).toHaveYielded([]);
1205
+ expect(InnerScheduler).toHaveYielded(['Suspend Inner']);
1206
+
1207
+ dispatchMouseHoverEvent(innerDiv);
1208
+ expect(OuterScheduler).toHaveYielded([]);
1209
+ expect(InnerScheduler).toHaveYielded([]);
1210
+ expect(Scheduler).toHaveYielded([]);
1211
+
1212
+ await act(async () => {
1213
+ resolveInner();
1214
+ await innerPromise;
1215
+ Scheduler.unstable_flushAllWithoutAsserting();
1216
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1217
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1218
+ });
1219
+
1220
+ // Inner hydrates
1221
+ expect(InnerScheduler).toHaveYielded(['Inner']);
1222
+ // Outer was hydrated earlier
1223
+ expect(OuterScheduler).toHaveYielded([]);
1224
+
1225
+ await act(async () => {
1226
+ Scheduler.unstable_flushAllWithoutAsserting();
1227
+ OuterScheduler.unstable_flushAllWithoutAsserting();
1228
+ InnerScheduler.unstable_flushAllWithoutAsserting();
1229
+ });
1230
+
1231
+ // First Inner Mouse Enter fires then Outer Mouse Enter
1232
+ expect(Scheduler).toHaveYielded([
1233
+ 'Inner Mouse Enter',
1234
+ 'Outer Mouse Enter',
1235
+ ]);
1236
+ });
1237
+ });
1238
+
1239
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1240
+ it('replays event with null target when tree is dismounted', async () => {
1241
+ let suspend = false;
1242
+ let resolve;
1243
+ const promise = new Promise(resolvePromise => {
1244
+ resolve = () => {
1245
+ suspend = false;
1246
+ resolvePromise();
1247
+ };
1248
+ });
1249
+
1250
+ function Child() {
1251
+ if (suspend) {
1252
+ throw promise;
1253
+ }
1254
+ Scheduler.unstable_yieldValue('Child');
1255
+ return (
1256
+ <div
1257
+ onMouseOver={() => {
1258
+ Scheduler.unstable_yieldValue('on mouse over');
1259
+ }}>
1260
+ Child
1261
+ </div>
1262
+ );
1263
+ }
1264
+
1265
+ function App() {
1266
+ return (
1267
+ <Suspense>
1268
+ <Child />
1269
+ </Suspense>
1270
+ );
1271
+ }
1272
+
1273
+ const finalHTML = ReactDOMServer.renderToString(<App />);
1274
+ expect(Scheduler).toHaveYielded(['Child']);
1275
+
1276
+ const container = document.createElement('div');
1277
+
1278
+ document.body.appendChild(container);
1279
+ container.innerHTML = finalHTML;
1280
+ suspend = true;
1281
+
1282
+ ReactDOM.hydrateRoot(container, <App />);
1283
+
1284
+ const childDiv = container.firstElementChild;
1285
+ dispatchMouseHoverEvent(childDiv);
1286
+
1287
+ // Not hydrated so event is saved for replay and stopPropagation is called
1288
+ expect(Scheduler).toHaveYielded([]);
1289
+
1290
+ resolve();
1291
+ Scheduler.unstable_flushNumberOfYields(1);
1292
+ expect(Scheduler).toHaveYielded(['Child']);
1293
+
1294
+ Scheduler.unstable_scheduleCallback(
1295
+ Scheduler.unstable_ImmediatePriority,
1296
+ () => {
1297
+ container.removeChild(childDiv);
1298
+
1299
+ const container2 = document.createElement('div');
1300
+ container2.addEventListener('mouseover', () => {
1301
+ Scheduler.unstable_yieldValue('container2 mouse over');
1302
+ });
1303
+ container2.appendChild(childDiv);
1304
+ },
1305
+ );
1306
+ Scheduler.unstable_flushAllWithoutAsserting();
1307
+
1308
+ // Even though the tree is remove the event is still dispatched with native event handler
1309
+ // on the container firing.
1310
+ expect(Scheduler).toHaveYielded(['container2 mouse over']);
1311
+
1312
document.body.removeChild(container);
1313
});
1314
packages/react-dom/src/events/ReactDOMEventListener.js
+17
-45
@@ -267,31 +267,6 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
267
targetContainer: EventTarget,
268
nativeEvent: AnyNativeEvent,
269
) {
270
- // TODO: replaying capture phase events is currently broken
271
- // because we used to do it during top-level native bubble handlers
272
- // but now we use different bubble and capture handlers.
273
- // In eager mode, we attach capture listeners early, so we need
274
- // to filter them out until we fix the logic to handle them correctly.
275
- const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
276
-
277
- if (
278
- allowReplay &&
279
- hasQueuedDiscreteEvents() &&
280
- isDiscreteEventThatRequiresHydration(domEventName)
281
- ) {
282
- // If we already have a queue of discrete events, and this is another discrete
283
- // event, then we can't dispatch it regardless of its target, since they
284
- // need to dispatch in order.
285
- queueDiscreteEvent(
286
- null, // Flags that we're not actually blocked on anything as far as we know.
287
- domEventName,
288
- eventSystemFlags,
289
- targetContainer,
290
- nativeEvent,
291
- );
292
- return;
293
- }
294
-
270
let blockedOn = findInstanceBlockingEvent(
271
domEventName,
272
eventSystemFlags,
@@ -306,28 +281,25 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
281
return_targetInst,
282
targetContainer,
283
);
309
- if (allowReplay) {
310
- clearIfContinuousEvent(domEventName, nativeEvent);
311
- }
284
+ clearIfContinuousEvent(domEventName, nativeEvent);
285
return;
286
}
287
315
- if (allowReplay) {
316
- if (
317
- queueIfContinuousEvent(
318
- blockedOn,
319
- domEventName,
320
- eventSystemFlags,
321
- targetContainer,
322
- nativeEvent,
323
- )
324
- ) {
325
- return;
326
- }
327
- // We need to clear only if we didn't queue because
328
- // queueing is accumulative.
329
- clearIfContinuousEvent(domEventName, nativeEvent);
288
+ if (
289
+ queueIfContinuousEvent(
290
+ blockedOn,
291
+ domEventName,
292
+ eventSystemFlags,
293
+ targetContainer,
294
+ nativeEvent,
295
+ )
296
+ ) {
297
+ nativeEvent.stopPropagation();
298
+ return;
299
}
300
+ // We need to clear only if we didn't queue because
301
+ // queueing is accumulative.
302
+ clearIfContinuousEvent(domEventName, nativeEvent);
303
304
if (
305
eventSystemFlags & IS_CAPTURE_PHASE &&
@@ -358,10 +330,10 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
330
}
331
blockedOn = nextBlockedOn;
332
}
361
- if (blockedOn) {
333
+ if (blockedOn !== null) {
334
nativeEvent.stopPropagation();
363
- return;
335
}
336
+ return;
337
}
338
339
// This is not replayable so we'll invoke it but without a target,
packages/react-dom/src/events/ReactDOMEventReplaying.js
+22
-9
@@ -472,15 +472,26 @@ function attemptReplayContinuousQueuedEvent(
472
queuedEvent.nativeEvent,
473
);
474
if (nextBlockedOn === null) {
475
- setReplayingEvent(queuedEvent.nativeEvent);
476
- dispatchEventForPluginEventSystem(
477
- queuedEvent.domEventName,
478
- queuedEvent.eventSystemFlags,
479
- queuedEvent.nativeEvent,
480
- return_targetInst,
481
- targetContainer,
482
- );
483
- resetReplayingEvent();
475
+ if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
476
+ const nativeEvent = queuedEvent.nativeEvent;
477
+ const nativeEventClone = new nativeEvent.constructor(
478
+ nativeEvent.type,
479
+ (nativeEvent: any),
480
+ );
481
+ setReplayingEvent(nativeEventClone);
482
+ nativeEvent.target.dispatchEvent(nativeEventClone);
483
+ resetReplayingEvent();
484
+ } else {
485
+ setReplayingEvent(queuedEvent.nativeEvent);
486
+ dispatchEventForPluginEventSystem(
487
+ queuedEvent.domEventName,
488
+ queuedEvent.eventSystemFlags,
489
+ queuedEvent.nativeEvent,
490
+ return_targetInst,
491
+ targetContainer,
492
+ );
493
+ resetReplayingEvent();
494
+ }
495
} else {
496
// We're still blocked. Try again later.
497
const fiber = getInstanceFromNode(nextBlockedOn);
@@ -532,6 +543,8 @@ function replayUnblockedEvents() {
543
nextDiscreteEvent.nativeEvent,
544
);
545
if (nextBlockedOn === null) {
546
+ // This whole function is in !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
547
+ // so we don't need the new replay behavior code branch.
548
setReplayingEvent(nextDiscreteEvent.nativeEvent);
549
dispatchEventForPluginEventSystem(
550
nextDiscreteEvent.domEventName,