Modern Event System: refactor legacy FB support logic (#18336)
Dominic Gannaway committed
Mar 19, 2020 at 13:22 UTC
8311cb5d24fd1648ddffb659bf9b3317f5f885d0
13 files changed
+1721
-1686
packages/legacy-events/EventSystemFlags.js
+1
@@ -16,3 +16,4 @@ export const IS_ACTIVE = 1 << 3;
16
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
17
export const IS_REPLAYED = 1 << 5;
18
export const IS_FIRST_ANCESTOR = 1 << 6;
19
+export const LEGACY_FB_SUPPORT = 1 << 7;
packages/react-dom/src/client/ReactDOMComponent.js
-1
@@ -1365,7 +1365,6 @@ export function listenToEventResponderEventTypes(
1365
(targetEventType: any),
1366
true,
1367
passiveItem.listener,
1368
- true,
1368
);
1369
listenerMap.delete(passiveKey);
1370
}
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+29
-39
@@ -24,6 +24,7 @@ import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
24
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
25
import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
26
import {plugins} from 'legacy-events/EventPluginRegistry';
27
+import {LEGACY_FB_SUPPORT, IS_REPLAYED} from 'legacy-events/EventSystemFlags';
28
29
import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
30
@@ -66,6 +67,7 @@ import {
67
TOP_RATE_CHANGE,
68
TOP_PROGRESS,
69
TOP_PLAYING,
70
+ TOP_CLICK,
71
} from './DOMTopLevelEventTypes';
72
import {
73
getClosestInstanceFromNode,
@@ -78,7 +80,7 @@ import {
80
ELEMENT_NODE,
81
} from '../shared/HTMLNodeType';
82
81
-import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
83
+import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
84
85
const capturePhaseEvents = new Set([
86
TOP_FOCUS,
@@ -197,7 +199,6 @@ export function listenToTopLevelEvent(
199
topLevelType,
200
isCapturePhase,
201
((listenerEntry: any): ElementListenerMapEntry).listener,
200
- ((listenerEntry: any): ElementListenerMapEntry).passive,
202
);
203
}
204
const listener = addTrappedEventListener(
@@ -225,42 +226,24 @@ export function listenToEvent(
226
}
227
}
228
228
-const validFBLegacyPrimerRels = new Set([
229
- 'dialog',
230
- 'dialog-post',
231
- 'async',
232
- 'async-post',
233
- 'theater',
234
- 'toggle',
235
-]);
236
-
237
-function willDeferLaterForFBLegacyPrimer(nativeEvent: any): boolean {
238
- let node = nativeEvent.target;
239
- const type = nativeEvent.type;
240
- if (type !== 'click') {
229
+function willDeferLaterForLegacyFBSupport(
230
+ topLevelType: DOMTopLevelEventType,
231
+ targetContainer: EventTarget,
232
+): boolean {
233
+ if (topLevelType !== TOP_CLICK) {
234
return false;
235
}
243
- while (node !== null) {
244
- // Primer works by intercepting a click event on an <a> element
245
- // that has a "rel" attribute that matches one of the valid ones
246
- // in the Set above. If we intercept this before Primer does, we
247
- // will need to defer the current event till later and discontinue
248
- // execution of the current event. To do this we can add a document
249
- // event listener and continue again later after propagation.
250
- if (node.tagName === 'A' && validFBLegacyPrimerRels.has(node.rel)) {
251
- const legacyFBSupport = true;
252
- const isCapture = nativeEvent.eventPhase === 1;
253
- addTrappedEventListener(
254
- null,
255
- ((type: any): DOMTopLevelEventType),
256
- isCapture,
257
- legacyFBSupport,
258
- );
259
- return true;
260
- }
261
- node = node.parentNode;
262
- }
263
- return false;
236
+ // We defer all click events with legacy FB support mode on.
237
+ // This means we add a one time event listener to trigger
238
+ // after the FB delegated listeners fire.
239
+ const isDeferredListenerForLegacyFBSupport = true;
240
+ addTrappedEventListener(
241
+ targetContainer,
242
+ topLevelType,
243
+ false,
244
+ isDeferredListenerForLegacyFBSupport,
245
+ );
246
+ return true;
247
}
248
249
function isMatchingRootContainer(
@@ -303,12 +286,19 @@ export function dispatchEventForPluginEventSystem(
286
// TODO: useEvent for document and window
287
return;
288
}
306
- // If we detect the FB legacy primer system, we
289
+ // If we are using the legacy FB support flag, we
290
// defer the event to the null with a one
291
// time event listener so we can defer the event.
292
if (
310
- enableLegacyFBPrimerSupport &&
311
- willDeferLaterForFBLegacyPrimer(nativeEvent)
293
+ enableLegacyFBSupport &&
294
+ // We do not want to defer if the event system has already been
295
+ // set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
296
+ // we call willDeferLaterForLegacyFBSupport, thus not bailing out
297
+ // will result in endless cycles like an infinite loop.
298
+ (eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
299
+ // We also don't want to defer during event replaying.
300
+ (eventSystemFlags & IS_REPLAYED) === 0 &&
301
+ willDeferLaterForLegacyFBSupport(topLevelType, targetContainer)
302
) {
303
return;
304
}
packages/react-dom/src/events/ReactDOMEventListener.js
+20
-13
@@ -42,6 +42,7 @@ import {
42
IS_PASSIVE,
43
IS_ACTIVE,
44
PASSIVE_NOT_SUPPORTED,
45
+ LEGACY_FB_SUPPORT,
46
} from 'legacy-events/EventSystemFlags';
47
48
import {
@@ -59,7 +60,7 @@ import {passiveBrowserEventsSupported} from './checkPassiveEvents';
60
import {
61
enableDeprecatedFlareAPI,
62
enableModernEventSystem,
62
- enableLegacyFBPrimerSupport,
63
+ enableLegacyFBSupport,
64
enableUseEventAPI,
65
} from 'shared/ReactFeatureFlags';
66
import {
@@ -129,10 +130,10 @@ export function addResponderEventSystemEvent(
130
}
131
132
export function addTrappedEventListener(
132
- targetContainer: null | EventTarget,
133
+ targetContainer: EventTarget,
134
topLevelType: DOMTopLevelEventType,
135
capture: boolean,
135
- legacyFBSupport?: boolean,
136
+ isDeferredListenerForLegacyFBSupport?: boolean,
137
passive?: boolean,
138
priority?: EventPriority,
139
): any => void {
@@ -159,11 +160,15 @@ export function addTrappedEventListener(
160
if (passive === true && !passiveBrowserEventsSupported) {
161
passive = false;
162
}
163
+ const eventSystemFlags =
164
+ enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
165
+ ? PLUGIN_EVENT_SYSTEM | LEGACY_FB_SUPPORT
166
+ : PLUGIN_EVENT_SYSTEM;
167
168
listener = listenerWrapper.bind(
169
null,
170
topLevelType,
166
- PLUGIN_EVENT_SYSTEM,
171
+ eventSystemFlags,
172
targetContainer,
173
);
174
@@ -177,14 +182,17 @@ export function addTrappedEventListener(
182
targetContainer = document;
183
}
184
180
- const validTargetContainer = ((targetContainer: any): EventTarget);
185
+ targetContainer =
186
+ enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
187
+ ? (targetContainer: any).ownerDocument
188
+ : targetContainer;
189
190
const rawEventName = getRawEventName(topLevelType);
191
192
let unsubscribeListener;
193
// When legacyFBSupport is enabled, it's for when we
194
// want to add a one time event listener to a container.
187
- // This should only be used with enableLegacyFBPrimerSupport
195
+ // This should only be used with enableLegacyFBSupport
196
// due to requirement to provide compatibility with
197
// internal FB www event tooling. This works by removing
198
// the event listener as soon as it is invoked. We could
@@ -193,14 +201,14 @@ export function addTrappedEventListener(
201
// browsers do not support this today, and given this is
202
// to support legacy code patterns, it's likely they'll
203
// need support for such browsers.
196
- if (enableLegacyFBPrimerSupport && legacyFBSupport) {
204
+ if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
205
const originalListener = listener;
206
listener = function(...p) {
207
try {
208
return originalListener.apply(this, p);
209
} finally {
210
removeEventListener(
203
- validTargetContainer,
211
+ targetContainer,
212
rawEventName,
213
unsubscribeListener,
214
capture,
@@ -212,14 +220,14 @@ export function addTrappedEventListener(
220
if (enableUseEventAPI && passive !== undefined) {
221
// This is only used with passive is either true or false.
222
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
215
- validTargetContainer,
223
+ targetContainer,
224
rawEventName,
225
listener,
226
passive,
227
);
228
} else {
229
unsubscribeListener = addEventCaptureListener(
222
- validTargetContainer,
230
+ targetContainer,
231
rawEventName,
232
listener,
233
);
@@ -228,14 +236,14 @@ export function addTrappedEventListener(
236
if (enableUseEventAPI && passive !== undefined) {
237
// This is only used with passive is either true or false.
238
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
231
- validTargetContainer,
239
+ targetContainer,
240
rawEventName,
241
listener,
242
passive,
243
);
244
} else {
245
unsubscribeListener = addEventBubbleListener(
238
- validTargetContainer,
246
+ targetContainer,
247
rawEventName,
248
listener,
249
);
@@ -249,7 +257,6 @@ export function removeTrappedEventListener(
257
topLevelType: DOMTopLevelEventType,
258
capture: boolean,
259
listener: any => void,
252
- passive: void | boolean,
260
): void {
261
const rawEventName = getRawEventName(topLevelType);
262
removeEventListener(targetContainer, rawEventName, listener, capture);
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+1663
-1625
@@ -24,1731 +24,1769 @@ function dispatchClickEvent(element) {
24
describe('DOMModernPluginEventSystem', () => {
25
let container;
26
27
- beforeEach(() => {
28
- jest.resetModules();
29
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
30
- ReactFeatureFlags.enableModernEventSystem = true;
31
-
32
- React = require('react');
33
- ReactDOM = require('react-dom');
34
- Scheduler = require('scheduler');
35
- ReactDOMServer = require('react-dom/server');
36
- container = document.createElement('div');
37
- document.body.appendChild(container);
38
- });
39
-
40
- afterEach(() => {
41
- document.body.removeChild(container);
42
- container = null;
43
- });
44
-
45
- it('handle propagation of click events', () => {
46
- const buttonRef = React.createRef();
47
- const divRef = React.createRef();
48
- const log = [];
49
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
50
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
51
-
52
- function Test() {
53
- return (
54
- <button
55
- ref={buttonRef}
56
- onClick={onClick}
57
- onClickCapture={onClickCapture}>
58
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
59
- Click me!
60
- </div>
61
- </button>
62
- );
63
- }
64
-
65
- ReactDOM.render(<Test />, container);
66
-
67
- let buttonElement = buttonRef.current;
68
- dispatchClickEvent(buttonElement);
69
- expect(onClick).toHaveBeenCalledTimes(1);
70
- expect(onClickCapture).toHaveBeenCalledTimes(1);
71
- expect(log[0]).toEqual(['capture', buttonElement]);
72
- expect(log[1]).toEqual(['bubble', buttonElement]);
73
-
74
- let divElement = divRef.current;
75
- dispatchClickEvent(divElement);
76
- expect(onClick).toHaveBeenCalledTimes(3);
77
- expect(onClickCapture).toHaveBeenCalledTimes(3);
78
- expect(log[2]).toEqual(['capture', buttonElement]);
79
- expect(log[3]).toEqual(['capture', divElement]);
80
- expect(log[4]).toEqual(['bubble', divElement]);
81
- expect(log[5]).toEqual(['bubble', buttonElement]);
82
- });
83
-
84
- it('handle propagation of click events between roots', () => {
85
- const buttonRef = React.createRef();
86
- const divRef = React.createRef();
87
- const childRef = React.createRef();
88
- const log = [];
89
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
90
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
91
-
92
- function Child() {
93
- return (
94
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
95
- Click me!
96
- </div>
97
- );
98
- }
99
-
100
- function Parent() {
101
- return (
102
- <button
103
- ref={buttonRef}
104
- onClick={onClick}
105
- onClickCapture={onClickCapture}>
106
- <div ref={childRef} />
107
- </button>
108
- );
109
- }
110
-
111
- ReactDOM.render(<Parent />, container);
112
- ReactDOM.render(<Child />, childRef.current);
113
-
114
- let buttonElement = buttonRef.current;
115
- dispatchClickEvent(buttonElement);
116
- expect(onClick).toHaveBeenCalledTimes(1);
117
- expect(onClickCapture).toHaveBeenCalledTimes(1);
118
- expect(log[0]).toEqual(['capture', buttonElement]);
119
- expect(log[1]).toEqual(['bubble', buttonElement]);
120
-
121
- let divElement = divRef.current;
122
- dispatchClickEvent(divElement);
123
- expect(onClick).toHaveBeenCalledTimes(3);
124
- expect(onClickCapture).toHaveBeenCalledTimes(3);
125
- expect(log[2]).toEqual(['capture', divElement]);
126
- expect(log[3]).toEqual(['bubble', divElement]);
127
- expect(log[4]).toEqual(['capture', buttonElement]);
128
- expect(log[5]).toEqual(['bubble', buttonElement]);
129
- });
130
-
131
- it('handle propagation of click events between disjointed roots', () => {
132
- const buttonRef = React.createRef();
133
- const divRef = React.createRef();
134
- const log = [];
135
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
136
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
137
-
138
- function Child() {
139
- return (
140
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
141
- Click me!
142
- </div>
143
- );
144
- }
145
-
146
- function Parent() {
147
- return (
148
- <button
149
- ref={buttonRef}
150
- onClick={onClick}
151
- onClickCapture={onClickCapture}
152
- />
153
- );
154
- }
155
-
156
- const disjointedNode = document.createElement('div');
157
- ReactDOM.render(<Parent />, container);
158
- buttonRef.current.appendChild(disjointedNode);
159
- ReactDOM.render(<Child />, disjointedNode);
160
-
161
- let buttonElement = buttonRef.current;
162
- dispatchClickEvent(buttonElement);
163
- expect(onClick).toHaveBeenCalledTimes(1);
164
- expect(onClickCapture).toHaveBeenCalledTimes(1);
165
- expect(log[0]).toEqual(['capture', buttonElement]);
166
- expect(log[1]).toEqual(['bubble', buttonElement]);
167
-
168
- let divElement = divRef.current;
169
- dispatchClickEvent(divElement);
170
- expect(onClick).toHaveBeenCalledTimes(3);
171
- expect(onClickCapture).toHaveBeenCalledTimes(3);
172
- expect(log[2]).toEqual(['capture', divElement]);
173
- expect(log[3]).toEqual(['bubble', divElement]);
174
- expect(log[4]).toEqual(['capture', buttonElement]);
175
- expect(log[5]).toEqual(['bubble', buttonElement]);
176
- });
177
-
178
- it('handle propagation of click events between disjointed comment roots', () => {
179
- const buttonRef = React.createRef();
180
- const divRef = React.createRef();
181
- const log = [];
182
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
183
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
184
-
185
- function Child() {
186
- return (
187
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
188
- Click me!
189
- </div>
190
- );
191
- }
192
-
193
- function Parent() {
194
- return (
195
- <button
196
- ref={buttonRef}
197
- onClick={onClick}
198
- onClickCapture={onClickCapture}
199
- />
200
- );
201
- }
202
-
203
- // We use a comment node here, then mount to it
204
- const disjointedNode = document.createComment(
205
- ' react-mount-point-unstable ',
206
- );
207
- ReactDOM.render(<Parent />, container);
208
- buttonRef.current.appendChild(disjointedNode);
209
- ReactDOM.render(<Child />, disjointedNode);
210
-
211
- let buttonElement = buttonRef.current;
212
- dispatchClickEvent(buttonElement);
213
- expect(onClick).toHaveBeenCalledTimes(1);
214
- expect(onClickCapture).toHaveBeenCalledTimes(1);
215
- expect(log[0]).toEqual(['capture', buttonElement]);
216
- expect(log[1]).toEqual(['bubble', buttonElement]);
217
-
218
- let divElement = divRef.current;
219
- dispatchClickEvent(divElement);
220
- expect(onClick).toHaveBeenCalledTimes(3);
221
- expect(onClickCapture).toHaveBeenCalledTimes(3);
222
- expect(log[2]).toEqual(['capture', divElement]);
223
- expect(log[3]).toEqual(['bubble', divElement]);
224
- expect(log[4]).toEqual(['capture', buttonElement]);
225
- expect(log[5]).toEqual(['bubble', buttonElement]);
226
- });
227
-
228
- it('handle propagation of click events between disjointed comment roots #2', () => {
229
- const buttonRef = React.createRef();
230
- const divRef = React.createRef();
231
- const spanRef = React.createRef();
232
- const log = [];
233
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
234
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
235
-
236
- function Child() {
237
- return (
238
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
239
- Click me!
240
- </div>
241
- );
242
- }
243
-
244
- function Parent() {
245
- return (
246
- <button
247
- ref={buttonRef}
248
- onClick={onClick}
249
- onClickCapture={onClickCapture}>
250
- <span ref={spanRef} />
251
- </button>
252
- );
253
- }
254
-
255
- // We use a comment node here, then mount to it
256
- const disjointedNode = document.createComment(
257
- ' react-mount-point-unstable ',
258
- );
259
- ReactDOM.render(<Parent />, container);
260
- spanRef.current.appendChild(disjointedNode);
261
- ReactDOM.render(<Child />, disjointedNode);
262
-
263
- let buttonElement = buttonRef.current;
264
- dispatchClickEvent(buttonElement);
265
- expect(onClick).toHaveBeenCalledTimes(1);
266
- expect(onClickCapture).toHaveBeenCalledTimes(1);
267
- expect(log[0]).toEqual(['capture', buttonElement]);
268
- expect(log[1]).toEqual(['bubble', buttonElement]);
269
-
270
- let divElement = divRef.current;
271
- dispatchClickEvent(divElement);
272
- expect(onClick).toHaveBeenCalledTimes(3);
273
- expect(onClickCapture).toHaveBeenCalledTimes(3);
274
- expect(log[2]).toEqual(['capture', divElement]);
275
- expect(log[3]).toEqual(['bubble', divElement]);
276
- expect(log[4]).toEqual(['capture', buttonElement]);
277
- expect(log[5]).toEqual(['bubble', buttonElement]);
278
- });
279
-
280
- it('handle propagation of click events between portals', () => {
281
- const buttonRef = React.createRef();
282
- const divRef = React.createRef();
283
- const log = [];
284
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
285
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
286
-
287
- const portalElement = document.createElement('div');
288
- document.body.appendChild(portalElement);
289
-
290
- function Child() {
291
- return (
292
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
293
- Click me!
294
- </div>
295
- );
296
- }
297
-
298
- function Parent() {
299
- return (
300
- <button
301
- ref={buttonRef}
302
- onClick={onClick}
303
- onClickCapture={onClickCapture}>
304
- {ReactDOM.createPortal(<Child />, portalElement)}
305
- </button>
306
- );
307
- }
308
-
309
- ReactDOM.render(<Parent />, container);
310
-
311
- let buttonElement = buttonRef.current;
312
- dispatchClickEvent(buttonElement);
313
- expect(onClick).toHaveBeenCalledTimes(1);
314
- expect(onClickCapture).toHaveBeenCalledTimes(1);
315
- expect(log[0]).toEqual(['capture', buttonElement]);
316
- expect(log[1]).toEqual(['bubble', buttonElement]);
317
-
318
- let divElement = divRef.current;
319
- dispatchClickEvent(divElement);
320
- expect(onClick).toHaveBeenCalledTimes(3);
321
- expect(onClickCapture).toHaveBeenCalledTimes(3);
322
- expect(log[2]).toEqual(['capture', buttonElement]);
323
- expect(log[3]).toEqual(['capture', divElement]);
324
- expect(log[4]).toEqual(['bubble', divElement]);
325
- expect(log[5]).toEqual(['bubble', buttonElement]);
326
-
327
- document.body.removeChild(portalElement);
328
- });
329
-
330
- it('handle click events on document.body portals', () => {
331
- const log = [];
332
-
333
- function Child({label}) {
334
- return <div onClick={() => log.push(label)}>{label}</div>;
335
- }
336
-
337
- function Parent() {
338
- return (
339
- <>
340
- {ReactDOM.createPortal(<Child label={'first'} />, document.body)}
341
- {ReactDOM.createPortal(<Child label={'second'} />, document.body)}
342
- </>
343
- );
344
- }
345
-
346
- ReactDOM.render(<Parent />, container);
347
-
348
- const second = document.body.lastChild;
349
- expect(second.textContent).toEqual('second');
350
- dispatchClickEvent(second);
351
-
352
- expect(log).toEqual(['second']);
353
-
354
- const first = second.previousSibling;
355
- expect(first.textContent).toEqual('first');
356
- dispatchClickEvent(first);
357
-
358
- expect(log).toEqual(['second', 'first']);
359
- });
360
-
361
- it.experimental(
362
- 'does not invoke an event on a parent tree when a subtree is dehydrated',
363
- async () => {
364
- let suspend = false;
365
- let resolve;
366
- let promise = new Promise(resolvePromise => (resolve = resolvePromise));
367
-
368
- let clicks = 0;
369
- let childSlotRef = React.createRef();
370
-
371
- function Parent() {
372
- return <div onClick={() => clicks++} ref={childSlotRef} />;
373
- }
374
-
375
- function Child({text}) {
376
- if (suspend) {
377
- throw promise;
378
- } else {
379
- return <a>Click me</a>;
380
- }
381
- }
382
-
383
- function App() {
384
- // The root is a Suspense boundary.
385
- return (
386
- <React.Suspense fallback="Loading...">
387
- <Child />
388
- </React.Suspense>
389
- );
390
- }
27
+ function withEnableLegacyFBSupport(enableLegacyFBSupport) {
28
+ describe(
29
+ 'enableLegacyFBSupport ' +
30
+ (enableLegacyFBSupport ? 'enabled' : 'disabled'),
31
+ () => {
32
+ beforeEach(() => {
33
+ jest.resetModules();
34
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
35
+ ReactFeatureFlags.enableModernEventSystem = true;
36
+ ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
37
+
38
+ React = require('react');
39
+ ReactDOM = require('react-dom');
40
+ Scheduler = require('scheduler');
41
+ ReactDOMServer = require('react-dom/server');
42
+ container = document.createElement('div');
43
+ document.body.appendChild(container);
44
+ });
45
392
- suspend = false;
393
- let finalHTML = ReactDOMServer.renderToString(<App />);
46
+ afterEach(() => {
47
+ document.body.removeChild(container);
48
+ container = null;
49
+ });
50
395
- let parentContainer = document.createElement('div');
396
- let childContainer = document.createElement('div');
51
+ it('handle propagation of click events', () => {
52
+ const buttonRef = React.createRef();
53
+ const divRef = React.createRef();
54
+ const log = [];
55
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
56
+ const onClickCapture = jest.fn(e =>
57
+ log.push(['capture', e.currentTarget]),
58
+ );
59
+
60
+ function Test() {
61
+ return (
62
+ <button
63
+ ref={buttonRef}
64
+ onClick={onClick}
65
+ onClickCapture={onClickCapture}>
66
+ <div
67
+ ref={divRef}
68
+ onClick={onClick}
69
+ onClickCapture={onClickCapture}>
70
+ Click me!
71
+ </div>
72
+ </button>
73
+ );
74
+ }
75
398
- // We need this to be in the document since we'll dispatch events on it.
399
- document.body.appendChild(parentContainer);
76
+ ReactDOM.render(<Test />, container);
77
401
- // We're going to use a different root as a parent.
402
- // This lets us detect whether an event goes through React's event system.
403
- let parentRoot = ReactDOM.createRoot(parentContainer);
404
- parentRoot.render(<Parent />);
405
- Scheduler.unstable_flushAll();
78
+ let buttonElement = buttonRef.current;
79
+ dispatchClickEvent(buttonElement);
80
407
- childSlotRef.current.appendChild(childContainer);
81
+ expect(onClick).toHaveBeenCalledTimes(1);
82
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
83
+ expect(log[0]).toEqual(['capture', buttonElement]);
84
+ expect(log[1]).toEqual(['bubble', buttonElement]);
85
409
- childContainer.innerHTML = finalHTML;
86
+ let divElement = divRef.current;
87
+ dispatchClickEvent(divElement);
88
+ expect(onClick).toHaveBeenCalledTimes(3);
89
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
90
+ expect(log[2]).toEqual(['capture', buttonElement]);
91
+ expect(log[3]).toEqual(['capture', divElement]);
92
+ expect(log[4]).toEqual(['bubble', divElement]);
93
+ expect(log[5]).toEqual(['bubble', buttonElement]);
94
+ });
95
411
- let a = childContainer.getElementsByTagName('a')[0];
96
+ it('handle propagation of click events between roots', () => {
97
+ const buttonRef = React.createRef();
98
+ const divRef = React.createRef();
99
+ const childRef = React.createRef();
100
+ const log = [];
101
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
102
+ const onClickCapture = jest.fn(e =>
103
+ log.push(['capture', e.currentTarget]),
104
+ );
105
+
106
+ function Child() {
107
+ return (
108
+ <div
109
+ ref={divRef}
110
+ onClick={onClick}
111
+ onClickCapture={onClickCapture}>
112
+ Click me!
113
+ </div>
114
+ );
115
+ }
116
413
- suspend = true;
117
+ function Parent() {
118
+ return (
119
+ <button
120
+ ref={buttonRef}
121
+ onClick={onClick}
122
+ onClickCapture={onClickCapture}>
123
+ <div ref={childRef} />
124
+ </button>
125
+ );
126
+ }
127
415
- // Hydrate asynchronously.
416
- let root = ReactDOM.createRoot(childContainer, {hydrate: true});
417
- root.render(<App />);
418
- jest.runAllTimers();
419
- Scheduler.unstable_flushAll();
128
+ ReactDOM.render(<Parent />, container);
129
+ ReactDOM.render(<Child />, childRef.current);
130
+
131
+ let buttonElement = buttonRef.current;
132
+ dispatchClickEvent(buttonElement);
133
+ expect(onClick).toHaveBeenCalledTimes(1);
134
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
135
+ expect(log[0]).toEqual(['capture', buttonElement]);
136
+ expect(log[1]).toEqual(['bubble', buttonElement]);
137
+
138
+ let divElement = divRef.current;
139
+ dispatchClickEvent(divElement);
140
+ expect(onClick).toHaveBeenCalledTimes(3);
141
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
142
+ expect(log[2]).toEqual(['capture', divElement]);
143
+ expect(log[3]).toEqual(['bubble', divElement]);
144
+ expect(log[4]).toEqual(['capture', buttonElement]);
145
+ expect(log[5]).toEqual(['bubble', buttonElement]);
146
+ });
147
421
- // The Suspense boundary is not yet hydrated.
422
- a.click();
423
- expect(clicks).toBe(0);
148
+ it('handle propagation of click events between disjointed roots', () => {
149
+ const buttonRef = React.createRef();
150
+ const divRef = React.createRef();
151
+ const log = [];
152
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
153
+ const onClickCapture = jest.fn(e =>
154
+ log.push(['capture', e.currentTarget]),
155
+ );
156
+
157
+ function Child() {
158
+ return (
159
+ <div
160
+ ref={divRef}
161
+ onClick={onClick}
162
+ onClickCapture={onClickCapture}>
163
+ Click me!
164
+ </div>
165
+ );
166
+ }
167
425
- // Resolving the promise so that rendering can complete.
426
- suspend = false;
427
- resolve();
428
- await promise;
168
+ function Parent() {
169
+ return (
170
+ <button
171
+ ref={buttonRef}
172
+ onClick={onClick}
173
+ onClickCapture={onClickCapture}
174
+ />
175
+ );
176
+ }
177
430
- Scheduler.unstable_flushAll();
431
- jest.runAllTimers();
178
+ const disjointedNode = document.createElement('div');
179
+ ReactDOM.render(<Parent />, container);
180
+ buttonRef.current.appendChild(disjointedNode);
181
+ ReactDOM.render(<Child />, disjointedNode);
182
+
183
+ let buttonElement = buttonRef.current;
184
+ dispatchClickEvent(buttonElement);
185
+ expect(onClick).toHaveBeenCalledTimes(1);
186
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
187
+ expect(log[0]).toEqual(['capture', buttonElement]);
188
+ expect(log[1]).toEqual(['bubble', buttonElement]);
189
+
190
+ let divElement = divRef.current;
191
+ dispatchClickEvent(divElement);
192
+ expect(onClick).toHaveBeenCalledTimes(3);
193
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
194
+ expect(log[2]).toEqual(['capture', divElement]);
195
+ expect(log[3]).toEqual(['bubble', divElement]);
196
+ expect(log[4]).toEqual(['capture', buttonElement]);
197
+ expect(log[5]).toEqual(['bubble', buttonElement]);
198
+ });
199
433
- // We're now full hydrated.
200
+ it('handle propagation of click events between disjointed comment roots', () => {
201
+ const buttonRef = React.createRef();
202
+ const divRef = React.createRef();
203
+ const log = [];
204
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
205
+ const onClickCapture = jest.fn(e =>
206
+ log.push(['capture', e.currentTarget]),
207
+ );
208
+
209
+ function Child() {
210
+ return (
211
+ <div
212
+ ref={divRef}
213
+ onClick={onClick}
214
+ onClickCapture={onClickCapture}>
215
+ Click me!
216
+ </div>
217
+ );
218
+ }
219
435
- expect(clicks).toBe(1);
220
+ function Parent() {
221
+ return (
222
+ <button
223
+ ref={buttonRef}
224
+ onClick={onClick}
225
+ onClickCapture={onClickCapture}
226
+ />
227
+ );
228
+ }
229
437
- document.body.removeChild(parentContainer);
438
- },
439
- );
230
+ // We use a comment node here, then mount to it
231
+ const disjointedNode = document.createComment(
232
+ ' react-mount-point-unstable ',
233
+ );
234
+ ReactDOM.render(<Parent />, container);
235
+ buttonRef.current.appendChild(disjointedNode);
236
+ ReactDOM.render(<Child />, disjointedNode);
237
+
238
+ let buttonElement = buttonRef.current;
239
+ dispatchClickEvent(buttonElement);
240
+ expect(onClick).toHaveBeenCalledTimes(1);
241
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
242
+ expect(log[0]).toEqual(['capture', buttonElement]);
243
+ expect(log[1]).toEqual(['bubble', buttonElement]);
244
+
245
+ let divElement = divRef.current;
246
+ dispatchClickEvent(divElement);
247
+ expect(onClick).toHaveBeenCalledTimes(3);
248
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
249
+ expect(log[2]).toEqual(['capture', divElement]);
250
+ expect(log[3]).toEqual(['bubble', divElement]);
251
+ expect(log[4]).toEqual(['capture', buttonElement]);
252
+ expect(log[5]).toEqual(['bubble', buttonElement]);
253
+ });
254
441
- it('handle click events on dynamic portals', () => {
442
- const log = [];
255
+ it('handle propagation of click events between disjointed comment roots #2', () => {
256
+ const buttonRef = React.createRef();
257
+ const divRef = React.createRef();
258
+ const spanRef = React.createRef();
259
+ const log = [];
260
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
261
+ const onClickCapture = jest.fn(e =>
262
+ log.push(['capture', e.currentTarget]),
263
+ );
264
+
265
+ function Child() {
266
+ return (
267
+ <div
268
+ ref={divRef}
269
+ onClick={onClick}
270
+ onClickCapture={onClickCapture}>
271
+ Click me!
272
+ </div>
273
+ );
274
+ }
275
444
- function Parent() {
445
- const ref = React.useRef(null);
446
- const [portal, setPortal] = React.useState(null);
276
+ function Parent() {
277
+ return (
278
+ <button
279
+ ref={buttonRef}
280
+ onClick={onClick}
281
+ onClickCapture={onClickCapture}>
282
+ <span ref={spanRef} />
283
+ </button>
284
+ );
285
+ }
286
448
- React.useEffect(() => {
449
- setPortal(
450
- ReactDOM.createPortal(
451
- <span onClick={() => log.push('child')} id="child" />,
452
- ref.current,
453
- ),
454
- );
455
- });
456
-
457
- return (
458
- <div ref={ref} onClick={() => log.push('parent')} id="parent">
459
- {portal}
460
- </div>
461
- );
462
- }
463
-
464
- ReactDOM.render(<Parent />, container);
465
-
466
- const parent = container.lastChild;
467
- expect(parent.id).toEqual('parent');
468
- dispatchClickEvent(parent);
469
-
470
- expect(log).toEqual(['parent']);
471
-
472
- const child = parent.lastChild;
473
- expect(child.id).toEqual('child');
474
- dispatchClickEvent(child);
475
-
476
- // we add both 'child' and 'parent' due to bubbling
477
- expect(log).toEqual(['parent', 'child', 'parent']);
478
- });
479
-
480
- // Slight alteration to the last test, to catch
481
- // a subtle difference in traversal.
482
- it('handle click events on dynamic portals #2', () => {
483
- const log = [];
484
-
485
- function Parent() {
486
- const ref = React.useRef(null);
487
- const [portal, setPortal] = React.useState(null);
488
-
489
- React.useEffect(() => {
490
- setPortal(
491
- ReactDOM.createPortal(
492
- <span onClick={() => log.push('child')} id="child" />,
493
- ref.current,
494
- ),
495
- );
496
- });
497
-
498
- return (
499
- <div ref={ref} onClick={() => log.push('parent')} id="parent">
500
- <div>{portal}</div>
501
- </div>
502
- );
503
- }
504
-
505
- ReactDOM.render(<Parent />, container);
506
-
507
- const parent = container.lastChild;
508
- expect(parent.id).toEqual('parent');
509
- dispatchClickEvent(parent);
510
-
511
- expect(log).toEqual(['parent']);
512
-
513
- const child = parent.lastChild;
514
- expect(child.id).toEqual('child');
515
- dispatchClickEvent(child);
516
-
517
- // we add both 'child' and 'parent' due to bubbling
518
- expect(log).toEqual(['parent', 'child', 'parent']);
519
- });
520
-
521
- it('native stopPropagation on click events between portals', () => {
522
- const buttonRef = React.createRef();
523
- const divRef = React.createRef();
524
- const middelDivRef = React.createRef();
525
- const log = [];
526
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
527
- const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
528
-
529
- const portalElement = document.createElement('div');
530
- document.body.appendChild(portalElement);
531
-
532
- function Child() {
533
- return (
534
- <div ref={middelDivRef}>
535
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
536
- Click me!
537
- </div>
538
- </div>
539
- );
540
- }
541
-
542
- function Parent() {
543
- React.useLayoutEffect(() => {
544
- // This should prevent the portalElement listeners from
545
- // capturing the events in the bubble phase.
546
- middelDivRef.current.addEventListener('click', e => {
547
- e.stopPropagation();
548
- });
549
- });
550
-
551
- return (
552
- <button
553
- ref={buttonRef}
554
- onClick={onClick}
555
- onClickCapture={onClickCapture}>
556
- {ReactDOM.createPortal(<Child />, portalElement)}
557
- </button>
558
- );
559
- }
560
-
561
- ReactDOM.render(<Parent />, container);
562
-
563
- let buttonElement = buttonRef.current;
564
- dispatchClickEvent(buttonElement);
565
- expect(onClick).toHaveBeenCalledTimes(1);
566
- expect(onClickCapture).toHaveBeenCalledTimes(1);
567
- expect(log[0]).toEqual(['capture', buttonElement]);
568
- expect(log[1]).toEqual(['bubble', buttonElement]);
569
-
570
- let divElement = divRef.current;
571
- dispatchClickEvent(divElement);
572
- expect(onClick).toHaveBeenCalledTimes(1);
573
- expect(onClickCapture).toHaveBeenCalledTimes(1);
574
-
575
- document.body.removeChild(portalElement);
576
- });
577
-
578
- it('handle propagation of focus events', () => {
579
- const buttonRef = React.createRef();
580
- const divRef = React.createRef();
581
- const log = [];
582
- const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
583
- const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
584
-
585
- function Test() {
586
- return (
587
- <button
588
- ref={buttonRef}
589
- onFocus={onFocus}
590
- onFocusCapture={onFocusCapture}>
591
- <div
592
- ref={divRef}
593
- onFocus={onFocus}
594
- onFocusCapture={onFocusCapture}
595
- tabIndex={0}>
596
- Click me!
597
- </div>
598
- </button>
599
- );
600
- }
601
-
602
- ReactDOM.render(<Test />, container);
603
-
604
- let buttonElement = buttonRef.current;
605
- buttonElement.focus();
606
- expect(onFocus).toHaveBeenCalledTimes(1);
607
- expect(onFocusCapture).toHaveBeenCalledTimes(1);
608
- expect(log[0]).toEqual(['capture', buttonElement]);
609
- expect(log[1]).toEqual(['bubble', buttonElement]);
610
-
611
- let divElement = divRef.current;
612
- divElement.focus();
613
- expect(onFocus).toHaveBeenCalledTimes(3);
614
- expect(onFocusCapture).toHaveBeenCalledTimes(3);
615
- expect(log[2]).toEqual(['capture', buttonElement]);
616
- expect(log[3]).toEqual(['capture', divElement]);
617
- expect(log[4]).toEqual(['bubble', divElement]);
618
- expect(log[5]).toEqual(['bubble', buttonElement]);
619
- });
620
-
621
- it('handle propagation of focus events between roots', () => {
622
- const buttonRef = React.createRef();
623
- const divRef = React.createRef();
624
- const childRef = React.createRef();
625
- const log = [];
626
- const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
627
- const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
628
-
629
- function Child() {
630
- return (
631
- <div
632
- ref={divRef}
633
- onFocus={onFocus}
634
- onFocusCapture={onFocusCapture}
635
- tabIndex={0}>
636
- Click me!
637
- </div>
638
- );
639
- }
640
-
641
- function Parent() {
642
- return (
643
- <button
644
- ref={buttonRef}
645
- onFocus={onFocus}
646
- onFocusCapture={onFocusCapture}>
647
- <div ref={childRef} />
648
- </button>
649
- );
650
- }
651
-
652
- ReactDOM.render(<Parent />, container);
653
- ReactDOM.render(<Child />, childRef.current);
654
-
655
- let buttonElement = buttonRef.current;
656
- buttonElement.focus();
657
- expect(onFocus).toHaveBeenCalledTimes(1);
658
- expect(onFocusCapture).toHaveBeenCalledTimes(1);
659
- expect(log[0]).toEqual(['capture', buttonElement]);
660
- expect(log[1]).toEqual(['bubble', buttonElement]);
661
-
662
- let divElement = divRef.current;
663
- divElement.focus();
664
- expect(onFocus).toHaveBeenCalledTimes(3);
665
- expect(onFocusCapture).toHaveBeenCalledTimes(3);
666
- expect(log[2]).toEqual(['capture', buttonElement]);
667
- expect(log[3]).toEqual(['bubble', buttonElement]);
668
- expect(log[4]).toEqual(['capture', divElement]);
669
- expect(log[5]).toEqual(['bubble', divElement]);
670
- });
671
-
672
- it('handle propagation of focus events between portals', () => {
673
- const buttonRef = React.createRef();
674
- const divRef = React.createRef();
675
- const log = [];
676
- const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
677
- const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
678
-
679
- const portalElement = document.createElement('div');
680
- document.body.appendChild(portalElement);
681
-
682
- function Child() {
683
- return (
684
- <div
685
- ref={divRef}
686
- onFocus={onFocus}
687
- onFocusCapture={onFocusCapture}
688
- tabIndex={0}>
689
- Click me!
690
- </div>
691
- );
692
- }
693
-
694
- function Parent() {
695
- return (
696
- <button
697
- ref={buttonRef}
698
- onFocus={onFocus}
699
- onFocusCapture={onFocusCapture}>
700
- {ReactDOM.createPortal(<Child />, portalElement)}
701
- </button>
702
- );
703
- }
704
-
705
- ReactDOM.render(<Parent />, container);
706
-
707
- let buttonElement = buttonRef.current;
708
- buttonElement.focus();
709
- expect(onFocus).toHaveBeenCalledTimes(1);
710
- expect(onFocusCapture).toHaveBeenCalledTimes(1);
711
- expect(log[0]).toEqual(['capture', buttonElement]);
712
- expect(log[1]).toEqual(['bubble', buttonElement]);
713
-
714
- let divElement = divRef.current;
715
- divElement.focus();
716
- expect(onFocus).toHaveBeenCalledTimes(3);
717
- expect(onFocusCapture).toHaveBeenCalledTimes(3);
718
- expect(log[2]).toEqual(['capture', buttonElement]);
719
- expect(log[3]).toEqual(['capture', divElement]);
720
- expect(log[4]).toEqual(['bubble', divElement]);
721
- expect(log[5]).toEqual(['bubble', buttonElement]);
722
-
723
- document.body.removeChild(portalElement);
724
- });
725
-
726
- it('native stopPropagation on focus events between portals', () => {
727
- const buttonRef = React.createRef();
728
- const divRef = React.createRef();
729
- const middelDivRef = React.createRef();
730
- const log = [];
731
- const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
732
- const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
733
-
734
- const portalElement = document.createElement('div');
735
- document.body.appendChild(portalElement);
736
-
737
- function Child() {
738
- return (
739
- <div ref={middelDivRef}>
740
- <div
741
- ref={divRef}
742
- onClick={onFocus}
743
- onClickCapture={onFocusCapture}
744
- tabIndex={0}>
745
- Click me!
746
- </div>
747
- </div>
748
- );
749
- }
750
-
751
- function Parent() {
752
- React.useLayoutEffect(() => {
753
- // This should prevent the portalElement listeners from
754
- // capturing the events in the bubble phase.
755
- middelDivRef.current.addEventListener('click', e => {
756
- e.stopPropagation();
287
+ // We use a comment node here, then mount to it
288
+ const disjointedNode = document.createComment(
289
+ ' react-mount-point-unstable ',
290
+ );
291
+ ReactDOM.render(<Parent />, container);
292
+ spanRef.current.appendChild(disjointedNode);
293
+ ReactDOM.render(<Child />, disjointedNode);
294
+
295
+ let buttonElement = buttonRef.current;
296
+ dispatchClickEvent(buttonElement);
297
+ expect(onClick).toHaveBeenCalledTimes(1);
298
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
299
+ expect(log[0]).toEqual(['capture', buttonElement]);
300
+ expect(log[1]).toEqual(['bubble', buttonElement]);
301
+
302
+ let divElement = divRef.current;
303
+ dispatchClickEvent(divElement);
304
+ expect(onClick).toHaveBeenCalledTimes(3);
305
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
306
+ expect(log[2]).toEqual(['capture', divElement]);
307
+ expect(log[3]).toEqual(['bubble', divElement]);
308
+ expect(log[4]).toEqual(['capture', buttonElement]);
309
+ expect(log[5]).toEqual(['bubble', buttonElement]);
310
});
758
- });
759
-
760
- return (
761
- <button
762
- ref={buttonRef}
763
- onFocus={onFocus}
764
- onFocusCapture={onFocusCapture}>
765
- {ReactDOM.createPortal(<Child />, portalElement)}
766
- </button>
767
- );
768
- }
769
-
770
- ReactDOM.render(<Parent />, container);
771
-
772
- let buttonElement = buttonRef.current;
773
- buttonElement.focus();
774
- expect(onFocus).toHaveBeenCalledTimes(1);
775
- expect(onFocusCapture).toHaveBeenCalledTimes(1);
776
- expect(log[0]).toEqual(['capture', buttonElement]);
777
- expect(log[1]).toEqual(['bubble', buttonElement]);
778
-
779
- let divElement = divRef.current;
780
- divElement.focus();
781
- expect(onFocus).toHaveBeenCalledTimes(1);
782
- expect(onFocusCapture).toHaveBeenCalledTimes(1);
783
-
784
- document.body.removeChild(portalElement);
785
- });
786
-
787
- it('handle propagation of enter and leave events between portals', () => {
788
- const buttonRef = React.createRef();
789
- const divRef = React.createRef();
790
- const log = [];
791
- const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
792
- const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
793
-
794
- const portalElement = document.createElement('div');
795
- document.body.appendChild(portalElement);
796
-
797
- function Child() {
798
- return (
799
- <div
800
- ref={divRef}
801
- onMouseEnter={onMouseEnter}
802
- onMouseLeave={onMouseLeave}
803
- />
804
- );
805
- }
806
-
807
- function Parent() {
808
- return (
809
- <button
810
- ref={buttonRef}
811
- onMouseEnter={onMouseEnter}
812
- onMouseLeave={onMouseLeave}>
813
- {ReactDOM.createPortal(<Child />, portalElement)}
814
- </button>
815
- );
816
- }
817
-
818
- ReactDOM.render(<Parent />, container);
819
-
820
- let buttonElement = buttonRef.current;
821
- buttonElement.dispatchEvent(
822
- new MouseEvent('mouseover', {
823
- bubbles: true,
824
- cancelable: true,
825
- relatedTarget: null,
826
- }),
827
- );
828
- expect(onMouseEnter).toHaveBeenCalledTimes(1);
829
- expect(onMouseLeave).toHaveBeenCalledTimes(0);
830
- expect(log[0]).toEqual(buttonElement);
831
-
832
- let divElement = divRef.current;
833
- buttonElement.dispatchEvent(
834
- new MouseEvent('mouseout', {
835
- bubbles: true,
836
- cancelable: true,
837
- relatedTarget: divElement,
838
- }),
839
- );
840
- divElement.dispatchEvent(
841
- new MouseEvent('mouseover', {
842
- bubbles: true,
843
- cancelable: true,
844
- relatedTarget: buttonElement,
845
- }),
846
- );
847
- expect(onMouseEnter).toHaveBeenCalledTimes(2);
848
- expect(onMouseLeave).toHaveBeenCalledTimes(0);
849
- expect(log[1]).toEqual(divElement);
850
-
851
- document.body.removeChild(portalElement);
852
- });
853
-
854
- it('handle propagation of enter and leave events between portals #2', () => {
855
- const buttonRef = React.createRef();
856
- const divRef = React.createRef();
857
- const portalRef = React.createRef();
858
- const log = [];
859
- const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
860
- const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
861
-
862
- function Child() {
863
- return (
864
- <div
865
- ref={divRef}
866
- onMouseEnter={onMouseEnter}
867
- onMouseLeave={onMouseLeave}
868
- />
869
- );
870
- }
871
-
872
- function Parent() {
873
- const [portal, setPortal] = React.useState(null);
874
-
875
- React.useLayoutEffect(() => {
876
- setPortal(ReactDOM.createPortal(<Child />, portalRef.current));
877
- }, []);
878
-
879
- return (
880
- <button
881
- ref={buttonRef}
882
- onMouseEnter={onMouseEnter}
883
- onMouseLeave={onMouseLeave}>
884
- <div ref={portalRef}>{portal}</div>
885
- </button>
886
- );
887
- }
888
-
889
- ReactDOM.render(<Parent />, container);
890
-
891
- let buttonElement = buttonRef.current;
892
- buttonElement.dispatchEvent(
893
- new MouseEvent('mouseover', {
894
- bubbles: true,
895
- cancelable: true,
896
- relatedTarget: null,
897
- }),
898
- );
899
- expect(onMouseEnter).toHaveBeenCalledTimes(1);
900
- expect(onMouseLeave).toHaveBeenCalledTimes(0);
901
- expect(log[0]).toEqual(buttonElement);
902
-
903
- let divElement = divRef.current;
904
- buttonElement.dispatchEvent(
905
- new MouseEvent('mouseout', {
906
- bubbles: true,
907
- cancelable: true,
908
- relatedTarget: divElement,
909
- }),
910
- );
911
- divElement.dispatchEvent(
912
- new MouseEvent('mouseover', {
913
- bubbles: true,
914
- cancelable: true,
915
- relatedTarget: buttonElement,
916
- }),
917
- );
918
- expect(onMouseEnter).toHaveBeenCalledTimes(2);
919
- expect(onMouseLeave).toHaveBeenCalledTimes(0);
920
- expect(log[1]).toEqual(divElement);
921
- });
922
-
923
- it('should preserve bubble/capture order between roots and nested portals', () => {
924
- const targetRef = React.createRef();
925
- let log = [];
926
- const onClickRoot = jest.fn(e => log.push('bubble root'));
927
- const onClickCaptureRoot = jest.fn(e => log.push('capture root'));
928
- const onClickPortal = jest.fn(e => log.push('bubble portal'));
929
- const onClickCapturePortal = jest.fn(e => log.push('capture portal'));
930
-
931
- function Portal() {
932
- return (
933
- <div
934
- onClick={onClickPortal}
935
- onClickCapture={onClickCapturePortal}
936
- ref={targetRef}>
937
- Click me!
938
- </div>
939
- );
940
- }
941
-
942
- const portalContainer = document.createElement('div');
943
-
944
- let shouldStopPropagation = false;
945
- portalContainer.addEventListener(
946
- 'click',
947
- e => {
948
- if (shouldStopPropagation) {
949
- e.stopPropagation();
950
- }
951
- },
952
- false,
953
- );
311
955
- function Root() {
956
- let portalTargetRef = React.useRef(null);
957
- React.useLayoutEffect(() => {
958
- portalTargetRef.current.appendChild(portalContainer);
959
- });
960
- return (
961
- <div onClick={onClickRoot} onClickCapture={onClickCaptureRoot}>
962
- <div ref={portalTargetRef} />
963
- {ReactDOM.createPortal(<Portal />, portalContainer)}
964
- </div>
965
- );
966
- }
967
-
968
- ReactDOM.render(<Root />, container);
969
-
970
- let divElement = targetRef.current;
971
- dispatchClickEvent(divElement);
972
- expect(log).toEqual([
973
- 'capture root',
974
- 'capture portal',
975
- 'bubble portal',
976
- 'bubble root',
977
- ]);
978
-
979
- log = [];
980
-
981
- shouldStopPropagation = true;
982
- dispatchClickEvent(divElement);
983
- expect(log).toEqual([
984
- // The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
985
- 'capture root',
986
- 'capture portal',
987
- 'bubble portal',
988
- 'bubble root',
989
- ]);
990
- });
991
-
992
- it('handle propagation of click events correctly with FB primer', () => {
993
- ReactFeatureFlags.enableLegacyFBPrimerSupport = true;
994
- const aRef = React.createRef();
995
-
996
- const log = [];
997
- // Stop propagation throught the React system
998
- const onClick = jest.fn(e => e.stopPropagation());
999
- const onDivClick = jest.fn();
1000
-
1001
- function Test() {
1002
- return (
1003
- <div onClick={onDivClick}>
1004
- <a ref={aRef} href="#" onClick={onClick} rel="dialog">
1005
- Click me
1006
- </a>
1007
- </div>
1008
- );
1009
- }
1010
- ReactDOM.render(<Test />, container);
1011
-
1012
- // Fake primer
1013
- document.addEventListener('click', e => {
1014
- if (e.target.rel === 'dialog') {
1015
- log.push('primer');
1016
- }
1017
- });
1018
- let aElement = aRef.current;
1019
- dispatchClickEvent(aElement);
1020
- expect(onClick).toHaveBeenCalledTimes(1);
1021
- expect(log).toEqual(['primer']);
1022
- expect(onDivClick).toHaveBeenCalledTimes(0);
1023
-
1024
- log.length = 0;
1025
- // This isn't something that should be picked up by Primer
1026
- function Test2() {
1027
- return (
1028
- <div onClick={onDivClick}>
1029
- <a ref={aRef} href="#" onClick={onClick} rel="dialog-foo">
1030
- Click me
1031
- </a>
1032
- </div>
1033
- );
1034
- }
1035
- ReactDOM.render(<Test2 />, container);
1036
- aElement = aRef.current;
1037
- dispatchClickEvent(aElement);
1038
- expect(onClick).toHaveBeenCalledTimes(2);
1039
- expect(log).toEqual([]);
1040
- expect(onDivClick).toHaveBeenCalledTimes(0);
1041
- });
1042
-
1043
- describe('ReactDOM.useEvent', () => {
1044
- beforeEach(() => {
1045
- jest.resetModules();
1046
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
1047
- ReactFeatureFlags.enableModernEventSystem = true;
1048
- ReactFeatureFlags.enableUseEventAPI = true;
1049
-
1050
- React = require('react');
1051
- ReactDOM = require('react-dom');
1052
- Scheduler = require('scheduler');
1053
- ReactDOMServer = require('react-dom/server');
1054
- });
1055
-
1056
- if (!__EXPERIMENTAL__) {
1057
- it("empty test so Jest doesn't complain", () => {});
1058
- return;
1059
- }
1060
-
1061
- it('should create the same event listener map', () => {
1062
- let listenerMaps = [];
1063
-
1064
- function Test() {
1065
- const listenerMap = ReactDOM.unstable_useEvent('click');
1066
-
1067
- listenerMaps.push(listenerMap);
1068
-
1069
- return <div />;
1070
- }
1071
-
1072
- ReactDOM.render(<Test />, container);
1073
- ReactDOM.render(<Test />, container);
1074
- expect(listenerMaps.length).toEqual(2);
1075
- expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1076
- });
1077
-
1078
- it('can render correctly with the ReactDOMServer', () => {
1079
- const clickEvent = jest.fn();
1080
-
1081
- function Test() {
1082
- const divRef = React.useRef(null);
1083
- const click = ReactDOM.unstable_useEvent('click');
1084
-
1085
- React.useEffect(() => {
1086
- click.setListener(divRef.current, clickEvent);
312
+ it('handle propagation of click events between portals', () => {
313
+ const buttonRef = React.createRef();
314
+ const divRef = React.createRef();
315
+ const log = [];
316
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
317
+ const onClickCapture = jest.fn(e =>
318
+ log.push(['capture', e.currentTarget]),
319
+ );
320
+
321
+ const portalElement = document.createElement('div');
322
+ document.body.appendChild(portalElement);
323
+
324
+ function Child() {
325
+ return (
326
+ <div
327
+ ref={divRef}
328
+ onClick={onClick}
329
+ onClickCapture={onClickCapture}>
330
+ Click me!
331
+ </div>
332
+ );
333
+ }
334
+
335
+ function Parent() {
336
+ return (
337
+ <button
338
+ ref={buttonRef}
339
+ onClick={onClick}
340
+ onClickCapture={onClickCapture}>
341
+ {ReactDOM.createPortal(<Child />, portalElement)}
342
+ </button>
343
+ );
344
+ }
345
+
346
+ ReactDOM.render(<Parent />, container);
347
+
348
+ let buttonElement = buttonRef.current;
349
+ dispatchClickEvent(buttonElement);
350
+ expect(onClick).toHaveBeenCalledTimes(1);
351
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
352
+ expect(log[0]).toEqual(['capture', buttonElement]);
353
+ expect(log[1]).toEqual(['bubble', buttonElement]);
354
+
355
+ let divElement = divRef.current;
356
+ dispatchClickEvent(divElement);
357
+ expect(onClick).toHaveBeenCalledTimes(3);
358
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
359
+ expect(log[2]).toEqual(['capture', buttonElement]);
360
+ expect(log[3]).toEqual(['capture', divElement]);
361
+ expect(log[4]).toEqual(['bubble', divElement]);
362
+ expect(log[5]).toEqual(['bubble', buttonElement]);
363
+
364
+ document.body.removeChild(portalElement);
365
});
366
1089
- return <div ref={divRef}>Hello world</div>;
1090
- }
1091
- const output = ReactDOMServer.renderToString(<Test />);
1092
- expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
1093
- });
367
+ it('handle click events on document.body portals', () => {
368
+ const log = [];
369
1095
- it('can render correctly with the ReactDOMServer hydration', () => {
1096
- const clickEvent = jest.fn();
1097
- const spanRef = React.createRef();
370
+ function Child({label}) {
371
+ return <div onClick={() => log.push(label)}>{label}</div>;
372
+ }
373
1099
- function Test() {
1100
- const click = ReactDOM.unstable_useEvent('click');
374
+ function Parent() {
375
+ return (
376
+ <>
377
+ {ReactDOM.createPortal(
378
+ <Child label={'first'} />,
379
+ document.body,
380
+ )}
381
+ {ReactDOM.createPortal(
382
+ <Child label={'second'} />,
383
+ document.body,
384
+ )}
385
+ </>
386
+ );
387
+ }
388
1102
- React.useEffect(() => {
1103
- click.setListener(spanRef.current, clickEvent);
1104
- });
389
+ ReactDOM.render(<Parent />, container);
390
1106
- return (
1107
- <div>
1108
- <span ref={spanRef}>Hello world</span>
1109
- </div>
1110
- );
1111
- }
1112
- const output = ReactDOMServer.renderToString(<Test />);
1113
- expect(output).toBe(
1114
- `<div data-reactroot=""><span>Hello world</span></div>`,
1115
- );
1116
- container.innerHTML = output;
1117
- ReactDOM.hydrate(<Test />, container);
1118
- Scheduler.unstable_flushAll();
1119
- dispatchClickEvent(spanRef.current);
1120
- expect(clickEvent).toHaveBeenCalledTimes(1);
1121
- });
1122
-
1123
- it('should correctly work for a basic "click" listener', () => {
1124
- let log = [];
1125
- const clickEvent = jest.fn(event => {
1126
- log.push({
1127
- eventPhase: event.eventPhase,
1128
- type: event.type,
1129
- currentTarget: event.currentTarget,
1130
- target: event.target,
1131
- });
1132
- });
1133
- const divRef = React.createRef();
1134
- const buttonRef = React.createRef();
391
+ const second = document.body.lastChild;
392
+ expect(second.textContent).toEqual('second');
393
+ dispatchClickEvent(second);
394
+
395
+ expect(log).toEqual(['second']);
396
1136
- function Test() {
1137
- const click = ReactDOM.unstable_useEvent('click');
397
+ const first = second.previousSibling;
398
+ expect(first.textContent).toEqual('first');
399
+ dispatchClickEvent(first);
400
1139
- React.useEffect(() => {
1140
- click.setListener(buttonRef.current, clickEvent);
401
+ expect(log).toEqual(['second', 'first']);
402
});
403
1143
- return (
1144
- <button ref={buttonRef}>
1145
- <div ref={divRef}>Click me!</div>
1146
- </button>
404
+ it.experimental(
405
+ 'does not invoke an event on a parent tree when a subtree is dehydrated',
406
+ async () => {
407
+ let suspend = false;
408
+ let resolve;
409
+ let promise = new Promise(
410
+ resolvePromise => (resolve = resolvePromise),
411
+ );
412
+
413
+ let clicks = 0;
414
+ let childSlotRef = React.createRef();
415
+
416
+ function Parent() {
417
+ return <div onClick={() => clicks++} ref={childSlotRef} />;
418
+ }
419
+
420
+ function Child({text}) {
421
+ if (suspend) {
422
+ throw promise;
423
+ } else {
424
+ return <a>Click me</a>;
425
+ }
426
+ }
427
+
428
+ function App() {
429
+ // The root is a Suspense boundary.
430
+ return (
431
+ <React.Suspense fallback="Loading...">
432
+ <Child />
433
+ </React.Suspense>
434
+ );
435
+ }
436
+
437
+ suspend = false;
438
+ let finalHTML = ReactDOMServer.renderToString(<App />);
439
+
440
+ let parentContainer = document.createElement('div');
441
+ let childContainer = document.createElement('div');
442
+
443
+ // We need this to be in the document since we'll dispatch events on it.
444
+ document.body.appendChild(parentContainer);
445
+
446
+ // We're going to use a different root as a parent.
447
+ // This lets us detect whether an event goes through React's event system.
448
+ let parentRoot = ReactDOM.createRoot(parentContainer);
449
+ parentRoot.render(<Parent />);
450
+ Scheduler.unstable_flushAll();
451
+
452
+ childSlotRef.current.appendChild(childContainer);
453
+
454
+ childContainer.innerHTML = finalHTML;
455
+
456
+ let a = childContainer.getElementsByTagName('a')[0];
457
+
458
+ suspend = true;
459
+
460
+ // Hydrate asynchronously.
461
+ let root = ReactDOM.createRoot(childContainer, {hydrate: true});
462
+ root.render(<App />);
463
+ jest.runAllTimers();
464
+ Scheduler.unstable_flushAll();
465
+
466
+ // The Suspense boundary is not yet hydrated.
467
+ a.click();
468
+ expect(clicks).toBe(0);
469
+
470
+ // Resolving the promise so that rendering can complete.
471
+ suspend = false;
472
+ resolve();
473
+ await promise;
474
+
475
+ Scheduler.unstable_flushAll();
476
+ jest.runAllTimers();
477
+
478
+ // We're now full hydrated.
479
+
480
+ expect(clicks).toBe(1);
481
+
482
+ document.body.removeChild(parentContainer);
483
+ },
484
);
1148
- }
1149
-
1150
- ReactDOM.render(<Test />, container);
1151
- Scheduler.unstable_flushAll();
1152
-
1153
- expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
1154
-
1155
- // Clicking the button should trigger the event callback
1156
- let divElement = divRef.current;
1157
- dispatchClickEvent(divElement);
1158
- expect(log).toEqual([
1159
- {
1160
- eventPhase: 3,
1161
- type: 'click',
1162
- currentTarget: buttonRef.current,
1163
- target: divRef.current,
1164
- },
1165
- ]);
1166
- expect(clickEvent).toBeCalledTimes(1);
1167
-
1168
- // Unmounting the container and clicking should not work
1169
- ReactDOM.render(null, container);
1170
- Scheduler.unstable_flushAll();
1171
-
1172
- dispatchClickEvent(divElement);
1173
- expect(clickEvent).toBeCalledTimes(1);
1174
-
1175
- // Re-rendering the container and clicking should work
1176
- ReactDOM.render(<Test />, container);
1177
- Scheduler.unstable_flushAll();
1178
-
1179
- divElement = divRef.current;
1180
- dispatchClickEvent(divElement);
1181
- expect(clickEvent).toBeCalledTimes(2);
1182
-
1183
- log = [];
1184
-
1185
- // Clicking the button should also work
1186
- let buttonElement = buttonRef.current;
1187
- dispatchClickEvent(buttonElement);
1188
- expect(log).toEqual([
1189
- {
1190
- eventPhase: 3,
1191
- type: 'click',
1192
- currentTarget: buttonRef.current,
1193
- target: buttonRef.current,
1194
- },
1195
- ]);
1196
-
1197
- function Test2({clickEvent2}) {
1198
- const click = ReactDOM.unstable_useEvent('click', clickEvent2);
1199
-
1200
- React.useEffect(() => {
1201
- click.setListener(buttonRef.current, clickEvent2);
485
+
486
+ it('handle click events on dynamic portals', () => {
487
+ const log = [];
488
+
489
+ function Parent() {
490
+ const ref = React.useRef(null);
491
+ const [portal, setPortal] = React.useState(null);
492
+
493
+ React.useEffect(() => {
494
+ setPortal(
495
+ ReactDOM.createPortal(
496
+ <span onClick={() => log.push('child')} id="child" />,
497
+ ref.current,
498
+ ),
499
+ );
500
+ });
501
+
502
+ return (
503
+ <div ref={ref} onClick={() => log.push('parent')} id="parent">
504
+ {portal}
505
+ </div>
506
+ );
507
+ }
508
+
509
+ ReactDOM.render(<Parent />, container);
510
+
511
+ const parent = container.lastChild;
512
+ expect(parent.id).toEqual('parent');
513
+ dispatchClickEvent(parent);
514
+
515
+ expect(log).toEqual(['parent']);
516
+
517
+ const child = parent.lastChild;
518
+ expect(child.id).toEqual('child');
519
+ dispatchClickEvent(child);
520
+
521
+ // we add both 'child' and 'parent' due to bubbling
522
+ expect(log).toEqual(['parent', 'child', 'parent']);
523
});
524
1204
- return (
1205
- <button ref={buttonRef}>
1206
- <div ref={divRef}>Click me!</div>
1207
- </button>
1208
- );
1209
- }
525
+ // Slight alteration to the last test, to catch
526
+ // a subtle difference in traversal.
527
+ it('handle click events on dynamic portals #2', () => {
528
+ const log = [];
529
+
530
+ function Parent() {
531
+ const ref = React.useRef(null);
532
+ const [portal, setPortal] = React.useState(null);
533
+
534
+ React.useEffect(() => {
535
+ setPortal(
536
+ ReactDOM.createPortal(
537
+ <span onClick={() => log.push('child')} id="child" />,
538
+ ref.current,
539
+ ),
540
+ );
541
+ });
542
+
543
+ return (
544
+ <div ref={ref} onClick={() => log.push('parent')} id="parent">
545
+ <div>{portal}</div>
546
+ </div>
547
+ );
548
+ }
549
+
550
+ ReactDOM.render(<Parent />, container);
551
+
552
+ const parent = container.lastChild;
553
+ expect(parent.id).toEqual('parent');
554
+ dispatchClickEvent(parent);
555
+
556
+ expect(log).toEqual(['parent']);
557
+
558
+ const child = parent.lastChild;
559
+ expect(child.id).toEqual('child');
560
+ dispatchClickEvent(child);
561
1211
- let clickEvent2 = jest.fn();
1212
- ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1213
- Scheduler.unstable_flushAll();
562
+ // we add both 'child' and 'parent' due to bubbling
563
+ expect(log).toEqual(['parent', 'child', 'parent']);
564
+ });
565
1215
- divElement = divRef.current;
1216
- dispatchClickEvent(divElement);
1217
- expect(clickEvent2).toBeCalledTimes(1);
566
+ it('native stopPropagation on click events between portals', () => {
567
+ const buttonRef = React.createRef();
568
+ const divRef = React.createRef();
569
+ const middelDivRef = React.createRef();
570
+ const log = [];
571
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
572
+ const onClickCapture = jest.fn(e =>
573
+ log.push(['capture', e.currentTarget]),
574
+ );
575
+
576
+ const portalElement = document.createElement('div');
577
+ document.body.appendChild(portalElement);
578
+
579
+ function Child() {
580
+ return (
581
+ <div ref={middelDivRef}>
582
+ <div
583
+ ref={divRef}
584
+ onClick={onClick}
585
+ onClickCapture={onClickCapture}>
586
+ Click me!
587
+ </div>
588
+ </div>
589
+ );
590
+ }
591
1219
- // Reset the function we pass in, so it's different
1220
- clickEvent2 = jest.fn();
1221
- ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1222
- Scheduler.unstable_flushAll();
592
+ function Parent() {
593
+ React.useLayoutEffect(() => {
594
+ // This should prevent the portalElement listeners from
595
+ // capturing the events in the bubble phase.
596
+ middelDivRef.current.addEventListener('click', e => {
597
+ e.stopPropagation();
598
+ });
599
+ });
600
+
601
+ return (
602
+ <button
603
+ ref={buttonRef}
604
+ onClick={onClick}
605
+ onClickCapture={onClickCapture}>
606
+ {ReactDOM.createPortal(<Child />, portalElement)}
607
+ </button>
608
+ );
609
+ }
610
1224
- divElement = divRef.current;
1225
- dispatchClickEvent(divElement);
1226
- expect(clickEvent2).toBeCalledTimes(1);
1227
- });
611
+ ReactDOM.render(<Parent />, container);
612
1229
- it('should correctly work for setting and clearing a basic "click" listener', () => {
1230
- const clickEvent = jest.fn();
1231
- const divRef = React.createRef();
1232
- const buttonRef = React.createRef();
613
+ let buttonElement = buttonRef.current;
614
+ dispatchClickEvent(buttonElement);
615
+ expect(onClick).toHaveBeenCalledTimes(1);
616
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
617
+ expect(log[0]).toEqual(['capture', buttonElement]);
618
+ expect(log[1]).toEqual(['bubble', buttonElement]);
619
1234
- function Test({off}) {
1235
- const click = ReactDOM.unstable_useEvent('click');
620
+ let divElement = divRef.current;
621
+ dispatchClickEvent(divElement);
622
+ expect(onClick).toHaveBeenCalledTimes(1);
623
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
624
1237
- React.useEffect(() => {
1238
- click.setListener(buttonRef.current, clickEvent);
625
+ document.body.removeChild(portalElement);
626
});
627
1241
- React.useEffect(() => {
1242
- if (off) {
1243
- click.setListener(buttonRef.current, null);
628
+ it('handle propagation of focus events', () => {
629
+ const buttonRef = React.createRef();
630
+ const divRef = React.createRef();
631
+ const log = [];
632
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
633
+ const onFocusCapture = jest.fn(e =>
634
+ log.push(['capture', e.currentTarget]),
635
+ );
636
+
637
+ function Test() {
638
+ return (
639
+ <button
640
+ ref={buttonRef}
641
+ onFocus={onFocus}
642
+ onFocusCapture={onFocusCapture}>
643
+ <div
644
+ ref={divRef}
645
+ onFocus={onFocus}
646
+ onFocusCapture={onFocusCapture}
647
+ tabIndex={0}>
648
+ Click me!
649
+ </div>
650
+ </button>
651
+ );
652
}
1245
- }, [off]);
653
1247
- return (
1248
- <button ref={buttonRef}>
1249
- <div ref={divRef}>Click me!</div>
1250
- </button>
1251
- );
1252
- }
1253
-
1254
- ReactDOM.render(<Test off={false} />, container);
1255
- Scheduler.unstable_flushAll();
1256
-
1257
- let divElement = divRef.current;
1258
- dispatchClickEvent(divElement);
1259
- expect(clickEvent).toBeCalledTimes(1);
1260
-
1261
- // The listener should get unmounted in the second effect
1262
- ReactDOM.render(<Test off={true} />, container);
1263
- Scheduler.unstable_flushAll();
1264
-
1265
- clickEvent.mockClear();
1266
-
1267
- divElement = divRef.current;
1268
- dispatchClickEvent(divElement);
1269
- expect(clickEvent).toBeCalledTimes(0);
1270
- });
1271
-
1272
- it('handle propagation of click events', () => {
1273
- const buttonRef = React.createRef();
1274
- const divRef = React.createRef();
1275
- const log = [];
1276
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1277
- const onClickCapture = jest.fn(e =>
1278
- log.push(['capture', e.currentTarget]),
1279
- );
1280
-
1281
- function Test() {
1282
- const click = ReactDOM.unstable_useEvent('click');
1283
- const clickCapture = ReactDOM.unstable_useEvent('click', {
1284
- capture: true,
654
+ ReactDOM.render(<Test />, container);
655
+
656
+ let buttonElement = buttonRef.current;
657
+ buttonElement.focus();
658
+ expect(onFocus).toHaveBeenCalledTimes(1);
659
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
660
+ expect(log[0]).toEqual(['capture', buttonElement]);
661
+ expect(log[1]).toEqual(['bubble', buttonElement]);
662
+
663
+ let divElement = divRef.current;
664
+ divElement.focus();
665
+ expect(onFocus).toHaveBeenCalledTimes(3);
666
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
667
+ expect(log[2]).toEqual(['capture', buttonElement]);
668
+ expect(log[3]).toEqual(['capture', divElement]);
669
+ expect(log[4]).toEqual(['bubble', divElement]);
670
+ expect(log[5]).toEqual(['bubble', buttonElement]);
671
});
672
1287
- React.useEffect(() => {
1288
- click.setListener(buttonRef.current, onClick);
1289
- clickCapture.setListener(buttonRef.current, onClickCapture);
1290
- click.setListener(divRef.current, onClick);
1291
- clickCapture.setListener(divRef.current, onClickCapture);
673
+ it('handle propagation of focus events between roots', () => {
674
+ const buttonRef = React.createRef();
675
+ const divRef = React.createRef();
676
+ const childRef = React.createRef();
677
+ const log = [];
678
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
679
+ const onFocusCapture = jest.fn(e =>
680
+ log.push(['capture', e.currentTarget]),
681
+ );
682
+
683
+ function Child() {
684
+ return (
685
+ <div
686
+ ref={divRef}
687
+ onFocus={onFocus}
688
+ onFocusCapture={onFocusCapture}
689
+ tabIndex={0}>
690
+ Click me!
691
+ </div>
692
+ );
693
+ }
694
+
695
+ function Parent() {
696
+ return (
697
+ <button
698
+ ref={buttonRef}
699
+ onFocus={onFocus}
700
+ onFocusCapture={onFocusCapture}>
701
+ <div ref={childRef} />
702
+ </button>
703
+ );
704
+ }
705
+
706
+ ReactDOM.render(<Parent />, container);
707
+ ReactDOM.render(<Child />, childRef.current);
708
+
709
+ let buttonElement = buttonRef.current;
710
+ buttonElement.focus();
711
+ expect(onFocus).toHaveBeenCalledTimes(1);
712
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
713
+ expect(log[0]).toEqual(['capture', buttonElement]);
714
+ expect(log[1]).toEqual(['bubble', buttonElement]);
715
+
716
+ let divElement = divRef.current;
717
+ divElement.focus();
718
+ expect(onFocus).toHaveBeenCalledTimes(3);
719
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
720
+ expect(log[2]).toEqual(['capture', buttonElement]);
721
+ expect(log[3]).toEqual(['bubble', buttonElement]);
722
+ expect(log[4]).toEqual(['capture', divElement]);
723
+ expect(log[5]).toEqual(['bubble', divElement]);
724
});
725
1294
- return (
1295
- <button ref={buttonRef}>
1296
- <div ref={divRef}>Click me!</div>
1297
- </button>
1298
- );
1299
- }
1300
-
1301
- ReactDOM.render(<Test />, container);
1302
- Scheduler.unstable_flushAll();
1303
-
1304
- let buttonElement = buttonRef.current;
1305
- dispatchClickEvent(buttonElement);
1306
- expect(onClick).toHaveBeenCalledTimes(1);
1307
- expect(onClickCapture).toHaveBeenCalledTimes(1);
1308
- expect(log[0]).toEqual(['capture', buttonElement]);
1309
- expect(log[1]).toEqual(['bubble', buttonElement]);
1310
-
1311
- let divElement = divRef.current;
1312
- dispatchClickEvent(divElement);
1313
- expect(onClick).toHaveBeenCalledTimes(3);
1314
- expect(onClickCapture).toHaveBeenCalledTimes(3);
1315
- expect(log[2]).toEqual(['capture', buttonElement]);
1316
- expect(log[3]).toEqual(['capture', divElement]);
1317
- expect(log[4]).toEqual(['bubble', divElement]);
1318
- expect(log[5]).toEqual(['bubble', buttonElement]);
1319
- });
1320
-
1321
- it('handle propagation of click events mixed with onClick events', () => {
1322
- const buttonRef = React.createRef();
1323
- const divRef = React.createRef();
1324
- const log = [];
1325
- const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1326
- const onClickCapture = jest.fn(e =>
1327
- log.push(['capture', e.currentTarget]),
1328
- );
1329
-
1330
- function Test() {
1331
- const click = ReactDOM.unstable_useEvent('click');
1332
- const clickCapture = ReactDOM.unstable_useEvent('click', {
1333
- capture: true,
726
+ it('handle propagation of focus events between portals', () => {
727
+ const buttonRef = React.createRef();
728
+ const divRef = React.createRef();
729
+ const log = [];
730
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
731
+ const onFocusCapture = jest.fn(e =>
732
+ log.push(['capture', e.currentTarget]),
733
+ );
734
+
735
+ const portalElement = document.createElement('div');
736
+ document.body.appendChild(portalElement);
737
+
738
+ function Child() {
739
+ return (
740
+ <div
741
+ ref={divRef}
742
+ onFocus={onFocus}
743
+ onFocusCapture={onFocusCapture}
744
+ tabIndex={0}>
745
+ Click me!
746
+ </div>
747
+ );
748
+ }
749
+
750
+ function Parent() {
751
+ return (
752
+ <button
753
+ ref={buttonRef}
754
+ onFocus={onFocus}
755
+ onFocusCapture={onFocusCapture}>
756
+ {ReactDOM.createPortal(<Child />, portalElement)}
757
+ </button>
758
+ );
759
+ }
760
+
761
+ ReactDOM.render(<Parent />, container);
762
+
763
+ let buttonElement = buttonRef.current;
764
+ buttonElement.focus();
765
+ expect(onFocus).toHaveBeenCalledTimes(1);
766
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
767
+ expect(log[0]).toEqual(['capture', buttonElement]);
768
+ expect(log[1]).toEqual(['bubble', buttonElement]);
769
+
770
+ let divElement = divRef.current;
771
+ divElement.focus();
772
+ expect(onFocus).toHaveBeenCalledTimes(3);
773
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
774
+ expect(log[2]).toEqual(['capture', buttonElement]);
775
+ expect(log[3]).toEqual(['capture', divElement]);
776
+ expect(log[4]).toEqual(['bubble', divElement]);
777
+ expect(log[5]).toEqual(['bubble', buttonElement]);
778
+
779
+ document.body.removeChild(portalElement);
780
});
781
1336
- React.useEffect(() => {
1337
- click.setListener(buttonRef.current, onClick);
1338
- clickCapture.setListener(buttonRef.current, onClickCapture);
782
+ it('native stopPropagation on focus events between portals', () => {
783
+ const buttonRef = React.createRef();
784
+ const divRef = React.createRef();
785
+ const middelDivRef = React.createRef();
786
+ const log = [];
787
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
788
+ const onFocusCapture = jest.fn(e =>
789
+ log.push(['capture', e.currentTarget]),
790
+ );
791
+
792
+ const portalElement = document.createElement('div');
793
+ document.body.appendChild(portalElement);
794
+
795
+ function Child() {
796
+ return (
797
+ <div ref={middelDivRef}>
798
+ <div
799
+ ref={divRef}
800
+ onClick={onFocus}
801
+ onClickCapture={onFocusCapture}
802
+ tabIndex={0}>
803
+ Click me!
804
+ </div>
805
+ </div>
806
+ );
807
+ }
808
+
809
+ function Parent() {
810
+ React.useLayoutEffect(() => {
811
+ // This should prevent the portalElement listeners from
812
+ // capturing the events in the bubble phase.
813
+ middelDivRef.current.addEventListener('click', e => {
814
+ e.stopPropagation();
815
+ });
816
+ });
817
+
818
+ return (
819
+ <button
820
+ ref={buttonRef}
821
+ onFocus={onFocus}
822
+ onFocusCapture={onFocusCapture}>
823
+ {ReactDOM.createPortal(<Child />, portalElement)}
824
+ </button>
825
+ );
826
+ }
827
+
828
+ ReactDOM.render(<Parent />, container);
829
+
830
+ let buttonElement = buttonRef.current;
831
+ buttonElement.focus();
832
+ expect(onFocus).toHaveBeenCalledTimes(1);
833
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
834
+ expect(log[0]).toEqual(['capture', buttonElement]);
835
+ expect(log[1]).toEqual(['bubble', buttonElement]);
836
+
837
+ let divElement = divRef.current;
838
+ divElement.focus();
839
+ expect(onFocus).toHaveBeenCalledTimes(1);
840
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
841
+
842
+ document.body.removeChild(portalElement);
843
});
844
1341
- return (
1342
- <button ref={buttonRef}>
1343
- <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
1344
- Click me!
1345
- </div>
1346
- </button>
1347
- );
1348
- }
1349
-
1350
- ReactDOM.render(<Test />, container);
1351
- Scheduler.unstable_flushAll();
1352
-
1353
- let buttonElement = buttonRef.current;
1354
- dispatchClickEvent(buttonElement);
1355
- expect(onClick).toHaveBeenCalledTimes(1);
1356
- expect(onClickCapture).toHaveBeenCalledTimes(1);
1357
- expect(log[0]).toEqual(['capture', buttonElement]);
1358
- expect(log[1]).toEqual(['bubble', buttonElement]);
1359
-
1360
- let divElement = divRef.current;
1361
- dispatchClickEvent(divElement);
1362
- expect(onClick).toHaveBeenCalledTimes(3);
1363
- expect(onClickCapture).toHaveBeenCalledTimes(3);
1364
- expect(log[2]).toEqual(['capture', buttonElement]);
1365
- expect(log[3]).toEqual(['capture', divElement]);
1366
- expect(log[4]).toEqual(['bubble', divElement]);
1367
- expect(log[5]).toEqual(['bubble', buttonElement]);
1368
- });
1369
-
1370
- it('should correctly work for a basic "click" listener on the outer target', () => {
1371
- const log = [];
1372
- const clickEvent = jest.fn(event => {
1373
- log.push({
1374
- eventPhase: event.eventPhase,
1375
- type: event.type,
1376
- currentTarget: event.currentTarget,
1377
- target: event.target,
845
+ it('handle propagation of enter and leave events between portals', () => {
846
+ const buttonRef = React.createRef();
847
+ const divRef = React.createRef();
848
+ const log = [];
849
+ const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
850
+ const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
851
+
852
+ const portalElement = document.createElement('div');
853
+ document.body.appendChild(portalElement);
854
+
855
+ function Child() {
856
+ return (
857
+ <div
858
+ ref={divRef}
859
+ onMouseEnter={onMouseEnter}
860
+ onMouseLeave={onMouseLeave}
861
+ />
862
+ );
863
+ }
864
+
865
+ function Parent() {
866
+ return (
867
+ <button
868
+ ref={buttonRef}
869
+ onMouseEnter={onMouseEnter}
870
+ onMouseLeave={onMouseLeave}>
871
+ {ReactDOM.createPortal(<Child />, portalElement)}
872
+ </button>
873
+ );
874
+ }
875
+
876
+ ReactDOM.render(<Parent />, container);
877
+
878
+ let buttonElement = buttonRef.current;
879
+ buttonElement.dispatchEvent(
880
+ new MouseEvent('mouseover', {
881
+ bubbles: true,
882
+ cancelable: true,
883
+ relatedTarget: null,
884
+ }),
885
+ );
886
+ expect(onMouseEnter).toHaveBeenCalledTimes(1);
887
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
888
+ expect(log[0]).toEqual(buttonElement);
889
+
890
+ let divElement = divRef.current;
891
+ buttonElement.dispatchEvent(
892
+ new MouseEvent('mouseout', {
893
+ bubbles: true,
894
+ cancelable: true,
895
+ relatedTarget: divElement,
896
+ }),
897
+ );
898
+ divElement.dispatchEvent(
899
+ new MouseEvent('mouseover', {
900
+ bubbles: true,
901
+ cancelable: true,
902
+ relatedTarget: buttonElement,
903
+ }),
904
+ );
905
+ expect(onMouseEnter).toHaveBeenCalledTimes(2);
906
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
907
+ expect(log[1]).toEqual(divElement);
908
+
909
+ document.body.removeChild(portalElement);
910
});
1379
- });
1380
- const divRef = React.createRef();
1381
- const buttonRef = React.createRef();
911
1383
- function Test() {
1384
- const click = ReactDOM.unstable_useEvent('click');
912
+ it('handle propagation of enter and leave events between portals #2', () => {
913
+ const buttonRef = React.createRef();
914
+ const divRef = React.createRef();
915
+ const portalRef = React.createRef();
916
+ const log = [];
917
+ const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
918
+ const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
919
+
920
+ function Child() {
921
+ return (
922
+ <div
923
+ ref={divRef}
924
+ onMouseEnter={onMouseEnter}
925
+ onMouseLeave={onMouseLeave}
926
+ />
927
+ );
928
+ }
929
+
930
+ function Parent() {
931
+ const [portal, setPortal] = React.useState(null);
932
+
933
+ React.useLayoutEffect(() => {
934
+ setPortal(ReactDOM.createPortal(<Child />, portalRef.current));
935
+ }, []);
936
+
937
+ return (
938
+ <button
939
+ ref={buttonRef}
940
+ onMouseEnter={onMouseEnter}
941
+ onMouseLeave={onMouseLeave}>
942
+ <div ref={portalRef}>{portal}</div>
943
+ </button>
944
+ );
945
+ }
946
1386
- React.useEffect(() => {
1387
- click.setListener(divRef.current, clickEvent);
947
+ ReactDOM.render(<Parent />, container);
948
+
949
+ let buttonElement = buttonRef.current;
950
+ buttonElement.dispatchEvent(
951
+ new MouseEvent('mouseover', {
952
+ bubbles: true,
953
+ cancelable: true,
954
+ relatedTarget: null,
955
+ }),
956
+ );
957
+ expect(onMouseEnter).toHaveBeenCalledTimes(1);
958
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
959
+ expect(log[0]).toEqual(buttonElement);
960
+
961
+ let divElement = divRef.current;
962
+ buttonElement.dispatchEvent(
963
+ new MouseEvent('mouseout', {
964
+ bubbles: true,
965
+ cancelable: true,
966
+ relatedTarget: divElement,
967
+ }),
968
+ );
969
+ divElement.dispatchEvent(
970
+ new MouseEvent('mouseover', {
971
+ bubbles: true,
972
+ cancelable: true,
973
+ relatedTarget: buttonElement,
974
+ }),
975
+ );
976
+ expect(onMouseEnter).toHaveBeenCalledTimes(2);
977
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
978
+ expect(log[1]).toEqual(divElement);
979
});
980
1390
- return (
1391
- <button ref={buttonRef}>
1392
- <div ref={divRef}>Click me!</div>
1393
- </button>
1394
- );
1395
- }
1396
-
1397
- ReactDOM.render(<Test />, container);
1398
- Scheduler.unstable_flushAll();
1399
-
1400
- expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
1401
-
1402
- // Clicking the button should trigger the event callback
1403
- let divElement = divRef.current;
1404
- dispatchClickEvent(divElement);
1405
- expect(log).toEqual([
1406
- {
1407
- eventPhase: 3,
1408
- type: 'click',
1409
- currentTarget: divRef.current,
1410
- target: divRef.current,
1411
- },
1412
- ]);
1413
-
1414
- // Unmounting the container and clicking should not work
1415
- ReactDOM.render(null, container);
1416
- dispatchClickEvent(divElement);
1417
- expect(clickEvent).toBeCalledTimes(1);
1418
-
1419
- // Re-rendering the container and clicking should work
1420
- ReactDOM.render(<Test />, container);
1421
- Scheduler.unstable_flushAll();
1422
-
1423
- divElement = divRef.current;
1424
- dispatchClickEvent(divElement);
1425
- expect(clickEvent).toBeCalledTimes(2);
1426
-
1427
- // Clicking the button should not work
1428
- let buttonElement = buttonRef.current;
1429
- dispatchClickEvent(buttonElement);
1430
- expect(clickEvent).toBeCalledTimes(2);
1431
- });
1432
-
1433
- it('should correctly handle many nested target listeners', () => {
1434
- const buttonRef = React.createRef();
1435
- const targetListener1 = jest.fn();
1436
- const targetListener2 = jest.fn();
1437
- const targetListener3 = jest.fn();
1438
- const targetListener4 = jest.fn();
1439
-
1440
- function Test() {
1441
- const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
1442
- const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
1443
- const click3 = ReactDOM.unstable_useEvent('click');
1444
- const click4 = ReactDOM.unstable_useEvent('click');
1445
-
1446
- React.useEffect(() => {
1447
- click1.setListener(buttonRef.current, targetListener1);
1448
- click2.setListener(buttonRef.current, targetListener2);
1449
- click3.setListener(buttonRef.current, targetListener3);
1450
- click4.setListener(buttonRef.current, targetListener4);
981
+ it('should preserve bubble/capture order between roots and nested portals', () => {
982
+ const targetRef = React.createRef();
983
+ let log = [];
984
+ const onClickRoot = jest.fn(e => log.push('bubble root'));
985
+ const onClickCaptureRoot = jest.fn(e => log.push('capture root'));
986
+ const onClickPortal = jest.fn(e => log.push('bubble portal'));
987
+ const onClickCapturePortal = jest.fn(e => log.push('capture portal'));
988
+
989
+ function Portal() {
990
+ return (
991
+ <div
992
+ onClick={onClickPortal}
993
+ onClickCapture={onClickCapturePortal}
994
+ ref={targetRef}>
995
+ Click me!
996
+ </div>
997
+ );
998
+ }
999
+
1000
+ const portalContainer = document.createElement('div');
1001
+
1002
+ let shouldStopPropagation = false;
1003
+ portalContainer.addEventListener(
1004
+ 'click',
1005
+ e => {
1006
+ if (shouldStopPropagation) {
1007
+ e.stopPropagation();
1008
+ }
1009
+ },
1010
+ false,
1011
+ );
1012
+
1013
+ function Root() {
1014
+ let portalTargetRef = React.useRef(null);
1015
+ React.useLayoutEffect(() => {
1016
+ portalTargetRef.current.appendChild(portalContainer);
1017
+ });
1018
+ return (
1019
+ <div onClick={onClickRoot} onClickCapture={onClickCaptureRoot}>
1020
+ <div ref={portalTargetRef} />
1021
+ {ReactDOM.createPortal(<Portal />, portalContainer)}
1022
+ </div>
1023
+ );
1024
+ }
1025
+
1026
+ ReactDOM.render(<Root />, container);
1027
+
1028
+ let divElement = targetRef.current;
1029
+ dispatchClickEvent(divElement);
1030
+ expect(log).toEqual([
1031
+ 'capture root',
1032
+ 'capture portal',
1033
+ 'bubble portal',
1034
+ 'bubble root',
1035
+ ]);
1036
+
1037
+ log = [];
1038
+
1039
+ shouldStopPropagation = true;
1040
+ dispatchClickEvent(divElement);
1041
+
1042
+ if (enableLegacyFBSupport) {
1043
+ // We aren't using roots with legacyFBSupport, we put clicks on the document, so we exbit the previous
1044
+ // behavior.
1045
+ expect(log).toEqual([]);
1046
+ } else {
1047
+ expect(log).toEqual([
1048
+ // The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
1049
+ 'capture root',
1050
+ 'capture portal',
1051
+ 'bubble portal',
1052
+ 'bubble root',
1053
+ ]);
1054
+ }
1055
});
1056
1453
- return <button ref={buttonRef}>Click me!</button>;
1454
- }
1057
+ describe('ReactDOM.useEvent', () => {
1058
+ beforeEach(() => {
1059
+ jest.resetModules();
1060
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
1061
+ ReactFeatureFlags.enableModernEventSystem = true;
1062
+ ReactFeatureFlags.enableUseEventAPI = true;
1063
+
1064
+ React = require('react');
1065
+ ReactDOM = require('react-dom');
1066
+ Scheduler = require('scheduler');
1067
+ ReactDOMServer = require('react-dom/server');
1068
+ });
1069
1456
- ReactDOM.render(<Test />, container);
1457
- Scheduler.unstable_flushAll();
1070
+ if (!__EXPERIMENTAL__) {
1071
+ it("empty test so Jest doesn't complain", () => {});
1072
+ return;
1073
+ }
1074
1459
- let buttonElement = buttonRef.current;
1460
- dispatchClickEvent(buttonElement);
1075
+ it('should create the same event listener map', () => {
1076
+ let listenerMaps = [];
1077
1462
- expect(targetListener1).toHaveBeenCalledTimes(1);
1463
- expect(targetListener2).toHaveBeenCalledTimes(1);
1464
- expect(targetListener3).toHaveBeenCalledTimes(1);
1465
- expect(targetListener4).toHaveBeenCalledTimes(1);
1078
+ function Test() {
1079
+ const listenerMap = ReactDOM.unstable_useEvent('click');
1080
1467
- function Test2() {
1468
- const click1 = ReactDOM.unstable_useEvent('click');
1469
- const click2 = ReactDOM.unstable_useEvent('click');
1470
- const click3 = ReactDOM.unstable_useEvent('click');
1471
- const click4 = ReactDOM.unstable_useEvent('click');
1081
+ listenerMaps.push(listenerMap);
1082
1473
- React.useEffect(() => {
1474
- click1.setListener(buttonRef.current, targetListener1);
1475
- click2.setListener(buttonRef.current, targetListener2);
1476
- click3.setListener(buttonRef.current, targetListener3);
1477
- click4.setListener(buttonRef.current, targetListener4);
1478
- });
1083
+ return <div />;
1084
+ }
1085
1480
- return <button ref={buttonRef}>Click me!</button>;
1481
- }
1086
+ ReactDOM.render(<Test />, container);
1087
+ ReactDOM.render(<Test />, container);
1088
+ expect(listenerMaps.length).toEqual(2);
1089
+ expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1090
+ });
1091
1483
- ReactDOM.render(<Test2 />, container);
1484
- Scheduler.unstable_flushAll();
1092
+ it('can render correctly with the ReactDOMServer', () => {
1093
+ const clickEvent = jest.fn();
1094
1486
- buttonElement = buttonRef.current;
1487
- dispatchClickEvent(buttonElement);
1488
- expect(targetListener1).toHaveBeenCalledTimes(2);
1489
- expect(targetListener2).toHaveBeenCalledTimes(2);
1490
- expect(targetListener3).toHaveBeenCalledTimes(2);
1491
- expect(targetListener4).toHaveBeenCalledTimes(2);
1492
- });
1095
+ function Test() {
1096
+ const divRef = React.useRef(null);
1097
+ const click = ReactDOM.unstable_useEvent('click');
1098
1494
- it('should correctly handle stopPropagation corrrectly for target events', () => {
1495
- const buttonRef = React.createRef();
1496
- const divRef = React.createRef();
1497
- let clickEvent = jest.fn();
1099
+ React.useEffect(() => {
1100
+ click.setListener(divRef.current, clickEvent);
1101
+ });
1102
1499
- function Test() {
1500
- const click1 = ReactDOM.unstable_useEvent('click', {
1501
- bind: buttonRef,
1502
- });
1503
- const click2 = ReactDOM.unstable_useEvent('click');
1103
+ return <div ref={divRef}>Hello world</div>;
1104
+ }
1105
+ const output = ReactDOMServer.renderToString(<Test />);
1106
+ expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
1107
+ });
1108
1505
- React.useEffect(() => {
1506
- click1.setListener(buttonRef.current, clickEvent);
1507
- click2.setListener(divRef.current, e => {
1508
- e.stopPropagation();
1109
+ it('can render correctly with the ReactDOMServer hydration', () => {
1110
+ const clickEvent = jest.fn();
1111
+ const spanRef = React.createRef();
1112
+
1113
+ function Test() {
1114
+ const click = ReactDOM.unstable_useEvent('click');
1115
+
1116
+ React.useEffect(() => {
1117
+ click.setListener(spanRef.current, clickEvent);
1118
+ });
1119
+
1120
+ return (
1121
+ <div>
1122
+ <span ref={spanRef}>Hello world</span>
1123
+ </div>
1124
+ );
1125
+ }
1126
+ const output = ReactDOMServer.renderToString(<Test />);
1127
+ expect(output).toBe(
1128
+ `<div data-reactroot=""><span>Hello world</span></div>`,
1129
+ );
1130
+ container.innerHTML = output;
1131
+ ReactDOM.hydrate(<Test />, container);
1132
+ Scheduler.unstable_flushAll();
1133
+ dispatchClickEvent(spanRef.current);
1134
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1135
});
1510
- });
1136
1512
- return (
1513
- <button ref={buttonRef}>
1514
- <div ref={divRef}>Click me!</div>
1515
- </button>
1516
- );
1517
- }
1518
-
1519
- ReactDOM.render(<Test />, container);
1520
- Scheduler.unstable_flushAll();
1521
-
1522
- let divElement = divRef.current;
1523
- dispatchClickEvent(divElement);
1524
- expect(clickEvent).toHaveBeenCalledTimes(0);
1525
- });
1526
-
1527
- it('should correctly handle stopPropagation corrrectly for many target events', () => {
1528
- const buttonRef = React.createRef();
1529
- const targetListerner1 = jest.fn(e => e.stopPropagation());
1530
- const targetListerner2 = jest.fn(e => e.stopPropagation());
1531
- const targetListerner3 = jest.fn(e => e.stopPropagation());
1532
- const targetListerner4 = jest.fn(e => e.stopPropagation());
1533
-
1534
- function Test() {
1535
- const click1 = ReactDOM.unstable_useEvent('click');
1536
- const click2 = ReactDOM.unstable_useEvent('click');
1537
- const click3 = ReactDOM.unstable_useEvent('click');
1538
- const click4 = ReactDOM.unstable_useEvent('click');
1539
-
1540
- React.useEffect(() => {
1541
- click1.setListener(buttonRef.current, targetListerner1);
1542
- click2.setListener(buttonRef.current, targetListerner2);
1543
- click3.setListener(buttonRef.current, targetListerner3);
1544
- click4.setListener(buttonRef.current, targetListerner4);
1545
- });
1137
+ it('should correctly work for a basic "click" listener', () => {
1138
+ let log = [];
1139
+ const clickEvent = jest.fn(event => {
1140
+ log.push({
1141
+ eventPhase: event.eventPhase,
1142
+ type: event.type,
1143
+ currentTarget: event.currentTarget,
1144
+ target: event.target,
1145
+ });
1146
+ });
1147
+ const divRef = React.createRef();
1148
+ const buttonRef = React.createRef();
1149
+
1150
+ function Test() {
1151
+ const click = ReactDOM.unstable_useEvent('click');
1152
+
1153
+ React.useEffect(() => {
1154
+ click.setListener(buttonRef.current, clickEvent);
1155
+ });
1156
+
1157
+ return (
1158
+ <button ref={buttonRef}>
1159
+ <div ref={divRef}>Click me!</div>
1160
+ </button>
1161
+ );
1162
+ }
1163
+
1164
+ ReactDOM.render(<Test />, container);
1165
+ Scheduler.unstable_flushAll();
1166
+
1167
+ expect(container.innerHTML).toBe(
1168
+ '<button><div>Click me!</div></button>',
1169
+ );
1170
+
1171
+ // Clicking the button should trigger the event callback
1172
+ let divElement = divRef.current;
1173
+ dispatchClickEvent(divElement);
1174
+ expect(log).toEqual([
1175
+ {
1176
+ eventPhase: 3,
1177
+ type: 'click',
1178
+ currentTarget: buttonRef.current,
1179
+ target: divRef.current,
1180
+ },
1181
+ ]);
1182
+ expect(clickEvent).toBeCalledTimes(1);
1183
+
1184
+ // Unmounting the container and clicking should not work
1185
+ ReactDOM.render(null, container);
1186
+ Scheduler.unstable_flushAll();
1187
+
1188
+ dispatchClickEvent(divElement);
1189
+ expect(clickEvent).toBeCalledTimes(1);
1190
+
1191
+ // Re-rendering the container and clicking should work
1192
+ ReactDOM.render(<Test />, container);
1193
+ Scheduler.unstable_flushAll();
1194
+
1195
+ divElement = divRef.current;
1196
+ dispatchClickEvent(divElement);
1197
+ expect(clickEvent).toBeCalledTimes(2);
1198
+
1199
+ log = [];
1200
+
1201
+ // Clicking the button should also work
1202
+ let buttonElement = buttonRef.current;
1203
+ dispatchClickEvent(buttonElement);
1204
+ expect(log).toEqual([
1205
+ {
1206
+ eventPhase: 3,
1207
+ type: 'click',
1208
+ currentTarget: buttonRef.current,
1209
+ target: buttonRef.current,
1210
+ },
1211
+ ]);
1212
+
1213
+ function Test2({clickEvent2}) {
1214
+ const click = ReactDOM.unstable_useEvent('click', clickEvent2);
1215
+
1216
+ React.useEffect(() => {
1217
+ click.setListener(buttonRef.current, clickEvent2);
1218
+ });
1219
+
1220
+ return (
1221
+ <button ref={buttonRef}>
1222
+ <div ref={divRef}>Click me!</div>
1223
+ </button>
1224
+ );
1225
+ }
1226
+
1227
+ let clickEvent2 = jest.fn();
1228
+ ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1229
+ Scheduler.unstable_flushAll();
1230
+
1231
+ divElement = divRef.current;
1232
+ dispatchClickEvent(divElement);
1233
+ expect(clickEvent2).toBeCalledTimes(1);
1234
+
1235
+ // Reset the function we pass in, so it's different
1236
+ clickEvent2 = jest.fn();
1237
+ ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1238
+ Scheduler.unstable_flushAll();
1239
+
1240
+ divElement = divRef.current;
1241
+ dispatchClickEvent(divElement);
1242
+ expect(clickEvent2).toBeCalledTimes(1);
1243
+ });
1244
1547
- return <button ref={buttonRef}>Click me!</button>;
1548
- }
1549
-
1550
- ReactDOM.render(<Test />, container);
1551
- Scheduler.unstable_flushAll();
1552
-
1553
- let buttonElement = buttonRef.current;
1554
- dispatchClickEvent(buttonElement);
1555
- expect(targetListerner1).toHaveBeenCalledTimes(1);
1556
- expect(targetListerner2).toHaveBeenCalledTimes(1);
1557
- expect(targetListerner3).toHaveBeenCalledTimes(1);
1558
- expect(targetListerner4).toHaveBeenCalledTimes(1);
1559
- });
1560
-
1561
- it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
1562
- const buttonRef = React.createRef();
1563
- const targetListerner1 = jest.fn(e => e.stopPropagation());
1564
- const targetListerner2 = jest.fn(e => e.stopPropagation());
1565
- const targetListerner3 = jest.fn(e => e.stopPropagation());
1566
- const targetListerner4 = jest.fn(e => e.stopPropagation());
1567
-
1568
- function Test() {
1569
- const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
1570
- const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
1571
- const click3 = ReactDOM.unstable_useEvent('click');
1572
- const click4 = ReactDOM.unstable_useEvent('click');
1573
-
1574
- React.useEffect(() => {
1575
- click1.setListener(buttonRef.current, targetListerner1);
1576
- click2.setListener(buttonRef.current, targetListerner2);
1577
- click3.setListener(buttonRef.current, targetListerner3);
1578
- click4.setListener(buttonRef.current, targetListerner4);
1579
- });
1245
+ it('should correctly work for setting and clearing a basic "click" listener', () => {
1246
+ const clickEvent = jest.fn();
1247
+ const divRef = React.createRef();
1248
+ const buttonRef = React.createRef();
1249
1581
- return <button ref={buttonRef}>Click me!</button>;
1582
- }
1250
+ function Test({off}) {
1251
+ const click = ReactDOM.unstable_useEvent('click');
1252
1584
- ReactDOM.render(<Test />, container);
1585
- Scheduler.unstable_flushAll();
1253
+ React.useEffect(() => {
1254
+ click.setListener(buttonRef.current, clickEvent);
1255
+ });
1256
1587
- let buttonElement = buttonRef.current;
1588
- dispatchClickEvent(buttonElement);
1589
- expect(targetListerner1).toHaveBeenCalledTimes(1);
1590
- expect(targetListerner2).toHaveBeenCalledTimes(1);
1591
- expect(targetListerner3).toHaveBeenCalledTimes(1);
1592
- expect(targetListerner4).toHaveBeenCalledTimes(1);
1593
- });
1257
+ React.useEffect(() => {
1258
+ if (off) {
1259
+ click.setListener(buttonRef.current, null);
1260
+ }
1261
+ }, [off]);
1262
1595
- it.experimental('should work with concurrent mode updates', async () => {
1596
- const log = [];
1597
- const ref = React.createRef();
1263
+ return (
1264
+ <button ref={buttonRef}>
1265
+ <div ref={divRef}>Click me!</div>
1266
+ </button>
1267
+ );
1268
+ }
1269
1599
- function Test({counter}) {
1600
- const click = ReactDOM.unstable_useEvent('click');
1270
+ ReactDOM.render(<Test off={false} />, container);
1271
+ Scheduler.unstable_flushAll();
1272
1602
- React.useLayoutEffect(() => {
1603
- click.setListener(ref.current, () => {
1604
- log.push({counter});
1605
- });
1606
- });
1273
+ let divElement = divRef.current;
1274
+ dispatchClickEvent(divElement);
1275
+ expect(clickEvent).toBeCalledTimes(1);
1276
1608
- Scheduler.unstable_yieldValue('Test');
1609
- return <button ref={ref}>Press me</button>;
1610
- }
1611
-
1612
- let root = ReactDOM.createRoot(container);
1613
- root.render(<Test counter={0} />);
1614
-
1615
- // Dev double-render
1616
- if (__DEV__) {
1617
- expect(Scheduler).toFlushAndYield(['Test', 'Test']);
1618
- } else {
1619
- expect(Scheduler).toFlushAndYield(['Test']);
1620
- }
1621
-
1622
- // Click the button
1623
- dispatchClickEvent(ref.current);
1624
- expect(log).toEqual([{counter: 0}]);
1625
-
1626
- // Clear log
1627
- log.length = 0;
1628
-
1629
- // Increase counter
1630
- root.render(<Test counter={1} />);
1631
- // Yield before committing
1632
- // Dev double-render
1633
- if (__DEV__) {
1634
- expect(Scheduler).toFlushAndYieldThrough(['Test', 'Test']);
1635
- } else {
1636
- expect(Scheduler).toFlushAndYieldThrough(['Test']);
1637
- }
1638
-
1639
- // Click the button again
1640
- dispatchClickEvent(ref.current);
1641
- expect(log).toEqual([{counter: 0}]);
1642
-
1643
- // Clear log
1644
- log.length = 0;
1645
-
1646
- // Commit
1647
- expect(Scheduler).toFlushAndYield([]);
1648
- dispatchClickEvent(ref.current);
1649
- expect(log).toEqual([{counter: 1}]);
1650
- });
1651
-
1652
- it('should correctly work for a basic "click" listener that upgrades', () => {
1653
- const clickEvent = jest.fn();
1654
- const buttonRef = React.createRef();
1655
- const button2Ref = React.createRef();
1656
-
1657
- function Test2() {
1658
- const click = ReactDOM.unstable_useEvent('click', {
1659
- passive: false,
1660
- });
1277
+ // The listener should get unmounted in the second effect
1278
+ ReactDOM.render(<Test off={true} />, container);
1279
+ Scheduler.unstable_flushAll();
1280
1662
- React.useEffect(() => {
1663
- click.setListener(button2Ref.current, clickEvent);
1664
- });
1281
+ clickEvent.mockClear();
1282
1666
- return <button ref={button2Ref}>Click me!</button>;
1667
- }
1283
+ divElement = divRef.current;
1284
+ dispatchClickEvent(divElement);
1285
+ expect(clickEvent).toBeCalledTimes(0);
1286
+ });
1287
1669
- function Test({extra}) {
1670
- const click = ReactDOM.unstable_useEvent('click', {
1671
- passive: true,
1672
- });
1288
+ it('handle propagation of click events', () => {
1289
+ const buttonRef = React.createRef();
1290
+ const divRef = React.createRef();
1291
+ const log = [];
1292
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1293
+ const onClickCapture = jest.fn(e =>
1294
+ log.push(['capture', e.currentTarget]),
1295
+ );
1296
+
1297
+ function Test() {
1298
+ const click = ReactDOM.unstable_useEvent('click');
1299
+ const clickCapture = ReactDOM.unstable_useEvent('click', {
1300
+ capture: true,
1301
+ });
1302
+
1303
+ React.useEffect(() => {
1304
+ click.setListener(buttonRef.current, onClick);
1305
+ clickCapture.setListener(buttonRef.current, onClickCapture);
1306
+ click.setListener(divRef.current, onClick);
1307
+ clickCapture.setListener(divRef.current, onClickCapture);
1308
+ });
1309
+
1310
+ return (
1311
+ <button ref={buttonRef}>
1312
+ <div ref={divRef}>Click me!</div>
1313
+ </button>
1314
+ );
1315
+ }
1316
+
1317
+ ReactDOM.render(<Test />, container);
1318
+ Scheduler.unstable_flushAll();
1319
+
1320
+ let buttonElement = buttonRef.current;
1321
+ dispatchClickEvent(buttonElement);
1322
+ expect(onClick).toHaveBeenCalledTimes(1);
1323
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
1324
+ expect(log[0]).toEqual(['capture', buttonElement]);
1325
+ expect(log[1]).toEqual(['bubble', buttonElement]);
1326
+
1327
+ let divElement = divRef.current;
1328
+ dispatchClickEvent(divElement);
1329
+ expect(onClick).toHaveBeenCalledTimes(3);
1330
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
1331
+ expect(log[2]).toEqual(['capture', buttonElement]);
1332
+ expect(log[3]).toEqual(['capture', divElement]);
1333
+ expect(log[4]).toEqual(['bubble', divElement]);
1334
+ expect(log[5]).toEqual(['bubble', buttonElement]);
1335
+ });
1336
1674
- React.useEffect(() => {
1675
- click.setListener(buttonRef.current, clickEvent);
1676
- });
1337
+ it('handle propagation of click events mixed with onClick events', () => {
1338
+ const buttonRef = React.createRef();
1339
+ const divRef = React.createRef();
1340
+ const log = [];
1341
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1342
+ const onClickCapture = jest.fn(e =>
1343
+ log.push(['capture', e.currentTarget]),
1344
+ );
1345
+
1346
+ function Test() {
1347
+ const click = ReactDOM.unstable_useEvent('click');
1348
+ const clickCapture = ReactDOM.unstable_useEvent('click', {
1349
+ capture: true,
1350
+ });
1351
+
1352
+ React.useEffect(() => {
1353
+ click.setListener(buttonRef.current, onClick);
1354
+ clickCapture.setListener(buttonRef.current, onClickCapture);
1355
+ });
1356
+
1357
+ return (
1358
+ <button ref={buttonRef}>
1359
+ <div
1360
+ ref={divRef}
1361
+ onClick={onClick}
1362
+ onClickCapture={onClickCapture}>
1363
+ Click me!
1364
+ </div>
1365
+ </button>
1366
+ );
1367
+ }
1368
+
1369
+ ReactDOM.render(<Test />, container);
1370
+ Scheduler.unstable_flushAll();
1371
+
1372
+ let buttonElement = buttonRef.current;
1373
+ dispatchClickEvent(buttonElement);
1374
+ expect(onClick).toHaveBeenCalledTimes(1);
1375
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
1376
+ expect(log[0]).toEqual(['capture', buttonElement]);
1377
+ expect(log[1]).toEqual(['bubble', buttonElement]);
1378
+
1379
+ let divElement = divRef.current;
1380
+ dispatchClickEvent(divElement);
1381
+ expect(onClick).toHaveBeenCalledTimes(3);
1382
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
1383
+ expect(log[2]).toEqual(['capture', buttonElement]);
1384
+ expect(log[3]).toEqual(['capture', divElement]);
1385
+ expect(log[4]).toEqual(['bubble', divElement]);
1386
+ expect(log[5]).toEqual(['bubble', buttonElement]);
1387
+ });
1388
1678
- return (
1679
- <>
1680
- <button ref={buttonRef}>Click me!</button>
1681
- {extra && <Test2 />}
1682
- </>
1683
- );
1684
- }
1389
+ it('should correctly work for a basic "click" listener on the outer target', () => {
1390
+ const log = [];
1391
+ const clickEvent = jest.fn(event => {
1392
+ log.push({
1393
+ eventPhase: event.eventPhase,
1394
+ type: event.type,
1395
+ currentTarget: event.currentTarget,
1396
+ target: event.target,
1397
+ });
1398
+ });
1399
+ const divRef = React.createRef();
1400
+ const buttonRef = React.createRef();
1401
+
1402
+ function Test() {
1403
+ const click = ReactDOM.unstable_useEvent('click');
1404
+
1405
+ React.useEffect(() => {
1406
+ click.setListener(divRef.current, clickEvent);
1407
+ });
1408
+
1409
+ return (
1410
+ <button ref={buttonRef}>
1411
+ <div ref={divRef}>Click me!</div>
1412
+ </button>
1413
+ );
1414
+ }
1415
+
1416
+ ReactDOM.render(<Test />, container);
1417
+ Scheduler.unstable_flushAll();
1418
+
1419
+ expect(container.innerHTML).toBe(
1420
+ '<button><div>Click me!</div></button>',
1421
+ );
1422
+
1423
+ // Clicking the button should trigger the event callback
1424
+ let divElement = divRef.current;
1425
+ dispatchClickEvent(divElement);
1426
+ expect(log).toEqual([
1427
+ {
1428
+ eventPhase: 3,
1429
+ type: 'click',
1430
+ currentTarget: divRef.current,
1431
+ target: divRef.current,
1432
+ },
1433
+ ]);
1434
+
1435
+ // Unmounting the container and clicking should not work
1436
+ ReactDOM.render(null, container);
1437
+ dispatchClickEvent(divElement);
1438
+ expect(clickEvent).toBeCalledTimes(1);
1439
+
1440
+ // Re-rendering the container and clicking should work
1441
+ ReactDOM.render(<Test />, container);
1442
+ Scheduler.unstable_flushAll();
1443
+
1444
+ divElement = divRef.current;
1445
+ dispatchClickEvent(divElement);
1446
+ expect(clickEvent).toBeCalledTimes(2);
1447
+
1448
+ // Clicking the button should not work
1449
+ let buttonElement = buttonRef.current;
1450
+ dispatchClickEvent(buttonElement);
1451
+ expect(clickEvent).toBeCalledTimes(2);
1452
+ });
1453
1686
- ReactDOM.render(<Test />, container);
1687
- Scheduler.unstable_flushAll();
1454
+ it('should correctly handle many nested target listeners', () => {
1455
+ const buttonRef = React.createRef();
1456
+ const targetListener1 = jest.fn();
1457
+ const targetListener2 = jest.fn();
1458
+ const targetListener3 = jest.fn();
1459
+ const targetListener4 = jest.fn();
1460
+
1461
+ function Test() {
1462
+ const click1 = ReactDOM.unstable_useEvent('click', {
1463
+ capture: true,
1464
+ });
1465
+ const click2 = ReactDOM.unstable_useEvent('click', {
1466
+ capture: true,
1467
+ });
1468
+ const click3 = ReactDOM.unstable_useEvent('click');
1469
+ const click4 = ReactDOM.unstable_useEvent('click');
1470
+
1471
+ React.useEffect(() => {
1472
+ click1.setListener(buttonRef.current, targetListener1);
1473
+ click2.setListener(buttonRef.current, targetListener2);
1474
+ click3.setListener(buttonRef.current, targetListener3);
1475
+ click4.setListener(buttonRef.current, targetListener4);
1476
+ });
1477
+
1478
+ return <button ref={buttonRef}>Click me!</button>;
1479
+ }
1480
+
1481
+ ReactDOM.render(<Test />, container);
1482
+ Scheduler.unstable_flushAll();
1483
+
1484
+ let buttonElement = buttonRef.current;
1485
+ dispatchClickEvent(buttonElement);
1486
+
1487
+ expect(targetListener1).toHaveBeenCalledTimes(1);
1488
+ expect(targetListener2).toHaveBeenCalledTimes(1);
1489
+ expect(targetListener3).toHaveBeenCalledTimes(1);
1490
+ expect(targetListener4).toHaveBeenCalledTimes(1);
1491
+
1492
+ function Test2() {
1493
+ const click1 = ReactDOM.unstable_useEvent('click');
1494
+ const click2 = ReactDOM.unstable_useEvent('click');
1495
+ const click3 = ReactDOM.unstable_useEvent('click');
1496
+ const click4 = ReactDOM.unstable_useEvent('click');
1497
+
1498
+ React.useEffect(() => {
1499
+ click1.setListener(buttonRef.current, targetListener1);
1500
+ click2.setListener(buttonRef.current, targetListener2);
1501
+ click3.setListener(buttonRef.current, targetListener3);
1502
+ click4.setListener(buttonRef.current, targetListener4);
1503
+ });
1504
+
1505
+ return <button ref={buttonRef}>Click me!</button>;
1506
+ }
1507
+
1508
+ ReactDOM.render(<Test2 />, container);
1509
+ Scheduler.unstable_flushAll();
1510
+
1511
+ buttonElement = buttonRef.current;
1512
+ dispatchClickEvent(buttonElement);
1513
+ expect(targetListener1).toHaveBeenCalledTimes(2);
1514
+ expect(targetListener2).toHaveBeenCalledTimes(2);
1515
+ expect(targetListener3).toHaveBeenCalledTimes(2);
1516
+ expect(targetListener4).toHaveBeenCalledTimes(2);
1517
+ });
1518
1689
- let button = buttonRef.current;
1690
- dispatchClickEvent(button);
1691
- expect(clickEvent).toHaveBeenCalledTimes(1);
1519
+ it('should correctly handle stopPropagation corrrectly for target events', () => {
1520
+ const buttonRef = React.createRef();
1521
+ const divRef = React.createRef();
1522
+ let clickEvent = jest.fn();
1523
+
1524
+ function Test() {
1525
+ const click1 = ReactDOM.unstable_useEvent('click', {
1526
+ bind: buttonRef,
1527
+ });
1528
+ const click2 = ReactDOM.unstable_useEvent('click');
1529
+
1530
+ React.useEffect(() => {
1531
+ click1.setListener(buttonRef.current, clickEvent);
1532
+ click2.setListener(divRef.current, e => {
1533
+ e.stopPropagation();
1534
+ });
1535
+ });
1536
+
1537
+ return (
1538
+ <button ref={buttonRef}>
1539
+ <div ref={divRef}>Click me!</div>
1540
+ </button>
1541
+ );
1542
+ }
1543
+
1544
+ ReactDOM.render(<Test />, container);
1545
+ Scheduler.unstable_flushAll();
1546
+
1547
+ let divElement = divRef.current;
1548
+ dispatchClickEvent(divElement);
1549
+ expect(clickEvent).toHaveBeenCalledTimes(0);
1550
+ });
1551
1693
- ReactDOM.render(<Test extra={true} />, container);
1694
- Scheduler.unstable_flushAll();
1552
+ it('should correctly handle stopPropagation corrrectly for many target events', () => {
1553
+ const buttonRef = React.createRef();
1554
+ const targetListerner1 = jest.fn(e => e.stopPropagation());
1555
+ const targetListerner2 = jest.fn(e => e.stopPropagation());
1556
+ const targetListerner3 = jest.fn(e => e.stopPropagation());
1557
+ const targetListerner4 = jest.fn(e => e.stopPropagation());
1558
+
1559
+ function Test() {
1560
+ const click1 = ReactDOM.unstable_useEvent('click');
1561
+ const click2 = ReactDOM.unstable_useEvent('click');
1562
+ const click3 = ReactDOM.unstable_useEvent('click');
1563
+ const click4 = ReactDOM.unstable_useEvent('click');
1564
+
1565
+ React.useEffect(() => {
1566
+ click1.setListener(buttonRef.current, targetListerner1);
1567
+ click2.setListener(buttonRef.current, targetListerner2);
1568
+ click3.setListener(buttonRef.current, targetListerner3);
1569
+ click4.setListener(buttonRef.current, targetListerner4);
1570
+ });
1571
+
1572
+ return <button ref={buttonRef}>Click me!</button>;
1573
+ }
1574
+
1575
+ ReactDOM.render(<Test />, container);
1576
+ Scheduler.unstable_flushAll();
1577
+
1578
+ let buttonElement = buttonRef.current;
1579
+ dispatchClickEvent(buttonElement);
1580
+ expect(targetListerner1).toHaveBeenCalledTimes(1);
1581
+ expect(targetListerner2).toHaveBeenCalledTimes(1);
1582
+ expect(targetListerner3).toHaveBeenCalledTimes(1);
1583
+ expect(targetListerner4).toHaveBeenCalledTimes(1);
1584
+ });
1585
1696
- clickEvent.mockClear();
1586
+ it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
1587
+ const buttonRef = React.createRef();
1588
+ const targetListerner1 = jest.fn(e => e.stopPropagation());
1589
+ const targetListerner2 = jest.fn(e => e.stopPropagation());
1590
+ const targetListerner3 = jest.fn(e => e.stopPropagation());
1591
+ const targetListerner4 = jest.fn(e => e.stopPropagation());
1592
+
1593
+ function Test() {
1594
+ const click1 = ReactDOM.unstable_useEvent('click', {
1595
+ capture: true,
1596
+ });
1597
+ const click2 = ReactDOM.unstable_useEvent('click', {
1598
+ capture: true,
1599
+ });
1600
+ const click3 = ReactDOM.unstable_useEvent('click');
1601
+ const click4 = ReactDOM.unstable_useEvent('click');
1602
+
1603
+ React.useEffect(() => {
1604
+ click1.setListener(buttonRef.current, targetListerner1);
1605
+ click2.setListener(buttonRef.current, targetListerner2);
1606
+ click3.setListener(buttonRef.current, targetListerner3);
1607
+ click4.setListener(buttonRef.current, targetListerner4);
1608
+ });
1609
+
1610
+ return <button ref={buttonRef}>Click me!</button>;
1611
+ }
1612
+
1613
+ ReactDOM.render(<Test />, container);
1614
+ Scheduler.unstable_flushAll();
1615
+
1616
+ let buttonElement = buttonRef.current;
1617
+ dispatchClickEvent(buttonElement);
1618
+ expect(targetListerner1).toHaveBeenCalledTimes(1);
1619
+ expect(targetListerner2).toHaveBeenCalledTimes(1);
1620
+ expect(targetListerner3).toHaveBeenCalledTimes(1);
1621
+ expect(targetListerner4).toHaveBeenCalledTimes(1);
1622
+ });
1623
1698
- button = button2Ref.current;
1699
- dispatchClickEvent(button);
1700
- expect(clickEvent).toHaveBeenCalledTimes(1);
1701
- });
1624
+ it.experimental(
1625
+ 'should work with concurrent mode updates',
1626
+ async () => {
1627
+ const log = [];
1628
+ const ref = React.createRef();
1629
+
1630
+ function Test({counter}) {
1631
+ const click = ReactDOM.unstable_useEvent('click');
1632
+
1633
+ React.useLayoutEffect(() => {
1634
+ click.setListener(ref.current, () => {
1635
+ log.push({counter});
1636
+ });
1637
+ });
1638
+
1639
+ Scheduler.unstable_yieldValue('Test');
1640
+ return <button ref={ref}>Press me</button>;
1641
+ }
1642
+
1643
+ let root = ReactDOM.createRoot(container);
1644
+ root.render(<Test counter={0} />);
1645
+
1646
+ // Dev double-render
1647
+ if (__DEV__) {
1648
+ expect(Scheduler).toFlushAndYield(['Test', 'Test']);
1649
+ } else {
1650
+ expect(Scheduler).toFlushAndYield(['Test']);
1651
+ }
1652
+
1653
+ // Click the button
1654
+ dispatchClickEvent(ref.current);
1655
+ expect(log).toEqual([{counter: 0}]);
1656
+
1657
+ // Clear log
1658
+ log.length = 0;
1659
+
1660
+ // Increase counter
1661
+ root.render(<Test counter={1} />);
1662
+ // Yield before committing
1663
+ // Dev double-render
1664
+ if (__DEV__) {
1665
+ expect(Scheduler).toFlushAndYieldThrough(['Test', 'Test']);
1666
+ } else {
1667
+ expect(Scheduler).toFlushAndYieldThrough(['Test']);
1668
+ }
1669
+
1670
+ // Click the button again
1671
+ dispatchClickEvent(ref.current);
1672
+ expect(log).toEqual([{counter: 0}]);
1673
+
1674
+ // Clear log
1675
+ log.length = 0;
1676
+
1677
+ // Commit
1678
+ expect(Scheduler).toFlushAndYield([]);
1679
+ dispatchClickEvent(ref.current);
1680
+ expect(log).toEqual([{counter: 1}]);
1681
+ },
1682
+ );
1683
+
1684
+ it('should correctly work for a basic "click" listener that upgrades', () => {
1685
+ const clickEvent = jest.fn();
1686
+ const buttonRef = React.createRef();
1687
+ const button2Ref = React.createRef();
1688
+
1689
+ function Test2() {
1690
+ const click = ReactDOM.unstable_useEvent('click', {
1691
+ passive: false,
1692
+ });
1693
+
1694
+ React.useEffect(() => {
1695
+ click.setListener(button2Ref.current, clickEvent);
1696
+ });
1697
+
1698
+ return <button ref={button2Ref}>Click me!</button>;
1699
+ }
1700
+
1701
+ function Test({extra}) {
1702
+ const click = ReactDOM.unstable_useEvent('click', {
1703
+ passive: true,
1704
+ });
1705
+
1706
+ React.useEffect(() => {
1707
+ click.setListener(buttonRef.current, clickEvent);
1708
+ });
1709
+
1710
+ return (
1711
+ <>
1712
+ <button ref={buttonRef}>Click me!</button>
1713
+ {extra && <Test2 />}
1714
+ </>
1715
+ );
1716
+ }
1717
+
1718
+ ReactDOM.render(<Test />, container);
1719
+ Scheduler.unstable_flushAll();
1720
+
1721
+ let button = buttonRef.current;
1722
+ dispatchClickEvent(button);
1723
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1724
+
1725
+ ReactDOM.render(<Test extra={true} />, container);
1726
+ Scheduler.unstable_flushAll();
1727
+
1728
+ clickEvent.mockClear();
1729
+
1730
+ button = button2Ref.current;
1731
+ dispatchClickEvent(button);
1732
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1733
+ });
1734
1703
- it('should correctly work for a basic "click" listener that upgrades #2', () => {
1704
- const clickEvent = jest.fn();
1705
- const buttonRef = React.createRef();
1706
- const button2Ref = React.createRef();
1735
+ it('should correctly work for a basic "click" listener that upgrades #2', () => {
1736
+ const clickEvent = jest.fn();
1737
+ const buttonRef = React.createRef();
1738
+ const button2Ref = React.createRef();
1739
1708
- function Test2() {
1709
- const click = ReactDOM.unstable_useEvent('click', {
1710
- passive: false,
1711
- });
1740
+ function Test2() {
1741
+ const click = ReactDOM.unstable_useEvent('click', {
1742
+ passive: false,
1743
+ });
1744
1713
- React.useEffect(() => {
1714
- click.setListener(button2Ref.current, clickEvent);
1715
- });
1745
+ React.useEffect(() => {
1746
+ click.setListener(button2Ref.current, clickEvent);
1747
+ });
1748
1717
- return <button ref={button2Ref}>Click me!</button>;
1718
- }
1749
+ return <button ref={button2Ref}>Click me!</button>;
1750
+ }
1751
1720
- function Test({extra}) {
1721
- const click = ReactDOM.unstable_useEvent('click', {
1722
- passive: undefined,
1723
- });
1752
+ function Test({extra}) {
1753
+ const click = ReactDOM.unstable_useEvent('click', {
1754
+ passive: undefined,
1755
+ });
1756
1725
- React.useEffect(() => {
1726
- click.setListener(buttonRef.current, clickEvent);
1727
- });
1757
+ React.useEffect(() => {
1758
+ click.setListener(buttonRef.current, clickEvent);
1759
+ });
1760
1729
- return (
1730
- <>
1731
- <button ref={buttonRef}>Click me!</button>
1732
- {extra && <Test2 />}
1733
- </>
1734
- );
1735
- }
1761
+ return (
1762
+ <>
1763
+ <button ref={buttonRef}>Click me!</button>
1764
+ {extra && <Test2 />}
1765
+ </>
1766
+ );
1767
+ }
1768
+
1769
+ ReactDOM.render(<Test />, container);
1770
+ Scheduler.unstable_flushAll();
1771
1737
- ReactDOM.render(<Test />, container);
1738
- Scheduler.unstable_flushAll();
1772
+ let button = buttonRef.current;
1773
+ dispatchClickEvent(button);
1774
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1775
1740
- let button = buttonRef.current;
1741
- dispatchClickEvent(button);
1742
- expect(clickEvent).toHaveBeenCalledTimes(1);
1776
+ ReactDOM.render(<Test extra={true} />, container);
1777
+ Scheduler.unstable_flushAll();
1778
1744
- ReactDOM.render(<Test extra={true} />, container);
1745
- Scheduler.unstable_flushAll();
1779
+ clickEvent.mockClear();
1780
1747
- clickEvent.mockClear();
1781
+ button = button2Ref.current;
1782
+ dispatchClickEvent(button);
1783
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1784
+ });
1785
+ });
1786
+ },
1787
+ );
1788
+ }
1789
1749
- button = button2Ref.current;
1750
- dispatchClickEvent(button);
1751
- expect(clickEvent).toHaveBeenCalledTimes(1);
1752
- });
1753
- });
1790
+ withEnableLegacyFBSupport(false);
1791
+ withEnableLegacyFBSupport(true);
1792
});
packages/shared/ReactFeatureFlags.js
+1
-1
@@ -140,4 +140,4 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
140
export const enableModernEventSystem = false;
141
142
// Support legacy Primer support on internal FB www
143
-export const enableLegacyFBPrimerSupport = false;
143
+export const enableLegacyFBSupport = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
-1
@@ -47,7 +47,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
47
export const runAllPassiveEffectDestroysBeforeCreates = false;
48
export const enableModernEventSystem = false;
49
export const warnAboutSpreadingKeyToJSX = false;
50
-export const enableLegacyFBPrimerSupport = false;
50
+export const enableLegacyFBSupport = false;
51
52
// Internal-only attempt to debug a React Native issue. See D20130868.
53
export const throwEarlyForMysteriousError = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
-1
@@ -46,7 +46,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
46
export const runAllPassiveEffectDestroysBeforeCreates = false;
47
export const enableModernEventSystem = false;
48
export const warnAboutSpreadingKeyToJSX = false;
49
-export const enableLegacyFBPrimerSupport = false;
49
+export const enableLegacyFBSupport = false;
50
51
// Internal-only attempt to debug a React Native issue. See D20130868.
52
export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
-1
@@ -46,7 +46,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
46
export const runAllPassiveEffectDestroysBeforeCreates = false;
47
export const enableModernEventSystem = false;
48
export const warnAboutSpreadingKeyToJSX = false;
49
-export const enableLegacyFBPrimerSupport = false;
49
+export const enableLegacyFBSupport = false;
50
51
// Internal-only attempt to debug a React Native issue. See D20130868.
52
export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
-1
@@ -46,7 +46,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
46
export const runAllPassiveEffectDestroysBeforeCreates = false;
47
export const enableModernEventSystem = false;
48
export const warnAboutSpreadingKeyToJSX = false;
49
-export const enableLegacyFBPrimerSupport = false;
49
+export const enableLegacyFBSupport = false;
50
51
// Internal-only attempt to debug a React Native issue. See D20130868.
52
export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
-1
@@ -46,7 +46,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
46
export const runAllPassiveEffectDestroysBeforeCreates = false;
47
export const enableModernEventSystem = false;
48
export const warnAboutSpreadingKeyToJSX = false;
49
-export const enableLegacyFBPrimerSupport = false;
49
+export const enableLegacyFBSupport = false;
50
51
// Internal-only attempt to debug a React Native issue. See D20130868.
52
export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
-1
@@ -46,7 +46,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
46
export const runAllPassiveEffectDestroysBeforeCreates = false;
47
export const enableModernEventSystem = false;
48
export const warnAboutSpreadingKeyToJSX = false;
49
-export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__;
49
+export const enableLegacyFBSupport = !__EXPERIMENTAL__;
50
51
// Internal-only attempt to debug a React Native issue. See D20130868.
52
export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
-1
@@ -104,7 +104,7 @@ export const disableModulePatternComponents = __EXPERIMENTAL__;
104
105
export const warnUnstableRenderSubtreeIntoContainer = false;
106
107
-export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__;
107
+export const enableLegacyFBSupport = !__EXPERIMENTAL__;
108
109
// Internal-only attempt to debug a React Native issue. See D20130868.
110
export const throwEarlyForMysteriousError = false;