@samitouri / QOS-React / commits / 8311cb5d24

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;