main
js 3,408 lines 116 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 import {createEventTarget} from 'dom-event-testing-library';
13
14 let React;
15 let ReactFeatureFlags;
16 let ReactDOM;
17 let ReactDOMClient;
18 let ReactDOMServer;
19 let Scheduler;
20 let act;
21 let waitForAll;
22 let waitFor;
23
24 function dispatchEvent(element, type) {
25 const event = document.createEvent('Event');
26 event.initEvent(type, true, true);
27 element.dispatchEvent(event);
28 }
29
30 function dispatchClickEvent(element) {
31 dispatchEvent(element, 'click');
32 }
33
34 const eventListenersToClear = [];
35
36 function startNativeEventListenerClearDown() {
37 const nativeWindowEventListener = window.addEventListener;
38 window.addEventListener = function (...params) {
39 eventListenersToClear.push({target: window, params});
40 return nativeWindowEventListener.apply(this, params);
41 };
42 const nativeDocumentEventListener = document.addEventListener;
43 document.addEventListener = function (...params) {
44 eventListenersToClear.push({target: document, params});
45 return nativeDocumentEventListener.apply(this, params);
46 };
47 }
48
49 function endNativeEventListenerClearDown() {
50 eventListenersToClear.forEach(({target, params}) => {
51 target.removeEventListener(...params);
52 });
53 }
54
55 describe('DOMPluginEventSystem', () => {
56 let container;
57
58 function withEnableLegacyFBSupport(enableLegacyFBSupport) {
59 describe(
60 'enableLegacyFBSupport ' +
61 (enableLegacyFBSupport ? 'enabled' : 'disabled'),
62 () => {
63 beforeAll(() => {
64 // These tests are run twice, once with legacyFBSupport enabled and once disabled.
65 // The document needs to be cleaned up a bit before the second pass otherwise it is
66 // operating in a non pristine environment
67 document.removeChild(document.documentElement);
68 document.appendChild(document.createElement('html'));
69 document.documentElement.appendChild(document.createElement('head'));
70 document.documentElement.appendChild(document.createElement('body'));
71 });
72
73 beforeEach(() => {
74 jest.resetModules();
75 ReactFeatureFlags = require('shared/ReactFeatureFlags');
76 ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
77
78 React = require('react');
79 ReactDOM = require('react-dom');
80 ReactDOMClient = require('react-dom/client');
81 Scheduler = require('scheduler');
82 ReactDOMServer = require('react-dom/server');
83
84 const InternalTestUtils = require('internal-test-utils');
85 waitForAll = InternalTestUtils.waitForAll;
86 waitFor = InternalTestUtils.waitFor;
87 act = InternalTestUtils.act;
88
89 container = document.createElement('div');
90 document.body.appendChild(container);
91 startNativeEventListenerClearDown();
92 });
93
94 afterEach(() => {
95 document.body.removeChild(container);
96 container = null;
97 endNativeEventListenerClearDown();
98 });
99
100 it('does not pool events', async () => {
101 const buttonRef = React.createRef();
102 const log = [];
103 const onClick = jest.fn(e => log.push(e));
104
105 function Test() {
106 return <button ref={buttonRef} onClick={onClick} />;
107 }
108
109 const root = ReactDOMClient.createRoot(container);
110 await act(() => {
111 root.render(<Test />);
112 });
113
114 const buttonElement = buttonRef.current;
115 dispatchClickEvent(buttonElement);
116 expect(onClick).toHaveBeenCalledTimes(1);
117 dispatchClickEvent(buttonElement);
118 expect(onClick).toHaveBeenCalledTimes(2);
119 expect(log[0]).not.toBe(log[1]);
120 expect(log[0].type).toBe('click');
121 expect(log[1].type).toBe('click');
122 });
123
124 it('handle propagation of click events', async () => {
125 const buttonRef = React.createRef();
126 const divRef = React.createRef();
127 const log = [];
128 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
129 const onClickCapture = jest.fn(e =>
130 log.push(['capture', e.currentTarget]),
131 );
132
133 function Test() {
134 return (
135 <button
136 ref={buttonRef}
137 onClick={onClick}
138 onClickCapture={onClickCapture}>
139 <div
140 ref={divRef}
141 onClick={onClick}
142 onClickCapture={onClickCapture}>
143 Click me!
144 </div>
145 </button>
146 );
147 }
148
149 const root = ReactDOMClient.createRoot(container);
150 await act(() => {
151 root.render(<Test />);
152 });
153
154 const buttonElement = buttonRef.current;
155 dispatchClickEvent(buttonElement);
156
157 expect(onClick).toHaveBeenCalledTimes(1);
158 expect(onClickCapture).toHaveBeenCalledTimes(1);
159 expect(log[0]).toEqual(['capture', buttonElement]);
160 expect(log[1]).toEqual(['bubble', buttonElement]);
161
162 const divElement = divRef.current;
163 dispatchClickEvent(divElement);
164 expect(onClick).toHaveBeenCalledTimes(3);
165 expect(onClickCapture).toHaveBeenCalledTimes(3);
166 expect(log[2]).toEqual(['capture', buttonElement]);
167 expect(log[3]).toEqual(['capture', divElement]);
168 expect(log[4]).toEqual(['bubble', divElement]);
169 expect(log[5]).toEqual(['bubble', buttonElement]);
170 });
171
172 it('handle propagation of click events combined with sync clicks', async () => {
173 const buttonRef = React.createRef();
174 let clicks = 0;
175
176 function Test() {
177 const inputRef = React.useRef(null);
178 return (
179 <div>
180 <button
181 ref={buttonRef}
182 onClick={() => {
183 // Sync click
184 inputRef.current.click();
185 }}
186 />
187 <input
188 ref={inputRef}
189 onClick={() => {
190 clicks++;
191 }}
192 />
193 </div>
194 );
195 }
196
197 const root = ReactDOMClient.createRoot(container);
198 await act(() => {
199 root.render(<Test />);
200 });
201
202 const buttonElement = buttonRef.current;
203 dispatchClickEvent(buttonElement);
204
205 expect(clicks).toBe(1);
206 });
207
208 it('handle propagation of click events between roots', async () => {
209 const buttonRef = React.createRef();
210 const divRef = React.createRef();
211 const childRef = React.createRef();
212 const log = [];
213 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
214 const onClickCapture = jest.fn(e =>
215 log.push(['capture', e.currentTarget]),
216 );
217
218 function Child() {
219 return (
220 <div
221 ref={divRef}
222 onClick={onClick}
223 onClickCapture={onClickCapture}>
224 Click me!
225 </div>
226 );
227 }
228
229 function Parent() {
230 return (
231 <button
232 ref={buttonRef}
233 onClick={onClick}
234 onClickCapture={onClickCapture}>
235 <div ref={childRef} />
236 </button>
237 );
238 }
239
240 const root = ReactDOMClient.createRoot(container);
241 await act(() => {
242 root.render(<Parent />);
243 });
244 const childRoot = ReactDOMClient.createRoot(childRef.current);
245 await act(() => {
246 childRoot.render(<Child />);
247 });
248
249 const buttonElement = buttonRef.current;
250 dispatchClickEvent(buttonElement);
251 expect(onClick).toHaveBeenCalledTimes(1);
252 expect(onClickCapture).toHaveBeenCalledTimes(1);
253 expect(log[0]).toEqual(['capture', buttonElement]);
254 expect(log[1]).toEqual(['bubble', buttonElement]);
255
256 const divElement = divRef.current;
257 dispatchClickEvent(divElement);
258 expect(onClick).toHaveBeenCalledTimes(3);
259 expect(onClickCapture).toHaveBeenCalledTimes(3);
260 expect(log[2]).toEqual(['capture', buttonElement]);
261 expect(log[3]).toEqual(['capture', divElement]);
262 expect(log[4]).toEqual(['bubble', divElement]);
263 expect(log[5]).toEqual(['bubble', buttonElement]);
264 });
265
266 it('handle propagation of click events between disjointed roots', async () => {
267 const buttonRef = React.createRef();
268 const divRef = React.createRef();
269 const log = [];
270 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
271 const onClickCapture = jest.fn(e =>
272 log.push(['capture', e.currentTarget]),
273 );
274
275 function Child() {
276 return (
277 <div
278 ref={divRef}
279 onClick={onClick}
280 onClickCapture={onClickCapture}>
281 Click me!
282 </div>
283 );
284 }
285
286 function Parent() {
287 return (
288 <button
289 ref={buttonRef}
290 onClick={onClick}
291 onClickCapture={onClickCapture}
292 />
293 );
294 }
295
296 const disjointedNode = document.createElement('div');
297 const root = ReactDOMClient.createRoot(container);
298 await act(() => {
299 root.render(<Parent />);
300 });
301
302 buttonRef.current.appendChild(disjointedNode);
303 const disjointedNodeRoot = ReactDOMClient.createRoot(disjointedNode);
304 await act(() => {
305 disjointedNodeRoot.render(<Child />);
306 });
307
308 const buttonElement = buttonRef.current;
309 dispatchClickEvent(buttonElement);
310 expect(onClick).toHaveBeenCalledTimes(1);
311 expect(onClickCapture).toHaveBeenCalledTimes(1);
312 expect(log[0]).toEqual(['capture', buttonElement]);
313 expect(log[1]).toEqual(['bubble', buttonElement]);
314
315 const divElement = divRef.current;
316 dispatchClickEvent(divElement);
317 expect(onClick).toHaveBeenCalledTimes(3);
318 expect(onClickCapture).toHaveBeenCalledTimes(3);
319 expect(log[2]).toEqual(['capture', buttonElement]);
320 expect(log[3]).toEqual(['capture', divElement]);
321 expect(log[4]).toEqual(['bubble', divElement]);
322 expect(log[5]).toEqual(['bubble', buttonElement]);
323 });
324
325 it('handle propagation of click events between disjointed roots #2', async () => {
326 const buttonRef = React.createRef();
327 const button2Ref = React.createRef();
328 const divRef = React.createRef();
329 const spanRef = React.createRef();
330 const log = [];
331 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
332 const onClickCapture = jest.fn(e =>
333 log.push(['capture', e.currentTarget]),
334 );
335
336 function Child() {
337 return (
338 <div
339 ref={divRef}
340 onClick={onClick}
341 onClickCapture={onClickCapture}>
342 Click me!
343 </div>
344 );
345 }
346
347 function Parent() {
348 return (
349 <button
350 ref={button2Ref}
351 onClick={onClick}
352 onClickCapture={onClickCapture}
353 />
354 );
355 }
356
357 function GrandParent() {
358 return (
359 <button
360 ref={buttonRef}
361 onClick={onClick}
362 onClickCapture={onClickCapture}>
363 <span ref={spanRef} />
364 </button>
365 );
366 }
367
368 // We make a wrapper with an inner container that we
369 // render to. So it looks like <div><span></span></div>
370 // We then render to all three:
371 // - container
372 // - parentContainer
373 // - childContainer
374
375 const parentContainer = document.createElement('div');
376 const childContainer = document.createElement('div');
377
378 const root = ReactDOMClient.createRoot(container);
379 await act(() => {
380 root.render(<GrandParent />);
381 });
382 const parentRoot = ReactDOMClient.createRoot(parentContainer);
383 await act(() => {
384 parentRoot.render(<Parent />);
385 });
386 const childRoot = ReactDOMClient.createRoot(childContainer);
387 await act(() => {
388 childRoot.render(<Child />);
389 });
390
391 parentContainer.appendChild(childContainer);
392 spanRef.current.appendChild(parentContainer);
393
394 // Inside <GrandParent />
395 const buttonElement = buttonRef.current;
396 dispatchClickEvent(buttonElement);
397 expect(onClick).toHaveBeenCalledTimes(1);
398 expect(onClickCapture).toHaveBeenCalledTimes(1);
399 expect(log[0]).toEqual(['capture', buttonElement]);
400 expect(log[1]).toEqual(['bubble', buttonElement]);
401
402 // Inside <Child />
403 const divElement = divRef.current;
404 dispatchClickEvent(divElement);
405 expect(onClick).toHaveBeenCalledTimes(3);
406 expect(onClickCapture).toHaveBeenCalledTimes(3);
407 expect(log[2]).toEqual(['capture', buttonElement]);
408 expect(log[3]).toEqual(['capture', divElement]);
409 expect(log[4]).toEqual(['bubble', divElement]);
410 expect(log[5]).toEqual(['bubble', buttonElement]);
411
412 // Inside <Parent />
413 const buttonElement2 = button2Ref.current;
414 dispatchClickEvent(buttonElement2);
415 expect(onClick).toHaveBeenCalledTimes(5);
416 expect(onClickCapture).toHaveBeenCalledTimes(5);
417 expect(log[6]).toEqual(['capture', buttonElement]);
418 expect(log[7]).toEqual(['capture', buttonElement2]);
419 expect(log[8]).toEqual(['bubble', buttonElement2]);
420 expect(log[9]).toEqual(['bubble', buttonElement]);
421 });
422
423 // @gate !disableCommentsAsDOMContainers
424 it('handle propagation of click events between disjointed comment roots', async () => {
425 const buttonRef = React.createRef();
426 const divRef = React.createRef();
427 const log = [];
428 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
429 const onClickCapture = jest.fn(e =>
430 log.push(['capture', e.currentTarget]),
431 );
432
433 function Child() {
434 return (
435 <div
436 ref={divRef}
437 onClick={onClick}
438 onClickCapture={onClickCapture}>
439 Click me!
440 </div>
441 );
442 }
443
444 function Parent() {
445 return (
446 <button
447 ref={buttonRef}
448 onClick={onClick}
449 onClickCapture={onClickCapture}
450 />
451 );
452 }
453
454 // We use a comment node here, then mount to it
455 const disjointedNode = document.createComment(
456 ' react-mount-point-unstable ',
457 );
458 const root = ReactDOMClient.createRoot(container);
459 await act(() => {
460 root.render(<Parent />);
461 });
462 buttonRef.current.appendChild(disjointedNode);
463 const disjointedNodeRoot = ReactDOMClient.createRoot(disjointedNode);
464 await act(() => {
465 disjointedNodeRoot.render(<Child />);
466 });
467
468 const buttonElement = buttonRef.current;
469 await act(() => {
470 dispatchClickEvent(buttonElement);
471 });
472 expect(onClick).toHaveBeenCalledTimes(1);
473 expect(onClickCapture).toHaveBeenCalledTimes(1);
474 expect(log[0]).toEqual(['capture', buttonElement]);
475 expect(log[1]).toEqual(['bubble', buttonElement]);
476
477 const divElement = divRef.current;
478 await act(() => {
479 dispatchClickEvent(divElement);
480 });
481 expect(onClick).toHaveBeenCalledTimes(3);
482 expect(onClickCapture).toHaveBeenCalledTimes(3);
483 expect(log[2]).toEqual(['capture', buttonElement]);
484 expect(log[3]).toEqual(['capture', divElement]);
485 expect(log[4]).toEqual(['bubble', divElement]);
486 expect(log[5]).toEqual(['bubble', buttonElement]);
487 });
488
489 // @gate !disableCommentsAsDOMContainers
490 it('handle propagation of click events between disjointed comment roots #2', async () => {
491 const buttonRef = React.createRef();
492 const divRef = React.createRef();
493 const spanRef = React.createRef();
494 const log = [];
495 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
496 const onClickCapture = jest.fn(e =>
497 log.push(['capture', e.currentTarget]),
498 );
499
500 function Child() {
501 return (
502 <div
503 ref={divRef}
504 onClick={onClick}
505 onClickCapture={onClickCapture}>
506 Click me!
507 </div>
508 );
509 }
510
511 function Parent() {
512 return (
513 <button
514 ref={buttonRef}
515 onClick={onClick}
516 onClickCapture={onClickCapture}>
517 <span ref={spanRef} />
518 </button>
519 );
520 }
521
522 // We use a comment node here, then mount to it
523 const disjointedNode = document.createComment(
524 ' react-mount-point-unstable ',
525 );
526 const root = ReactDOMClient.createRoot(container);
527 await act(() => {
528 root.render(<Parent />);
529 });
530 spanRef.current.appendChild(disjointedNode);
531 const disjointedNodeRoot = ReactDOMClient.createRoot(disjointedNode);
532 await act(() => {
533 disjointedNodeRoot.render(<Child />);
534 });
535
536 const buttonElement = buttonRef.current;
537 await act(() => {
538 dispatchClickEvent(buttonElement);
539 });
540 expect(onClick).toHaveBeenCalledTimes(1);
541 expect(onClickCapture).toHaveBeenCalledTimes(1);
542 expect(log[0]).toEqual(['capture', buttonElement]);
543 expect(log[1]).toEqual(['bubble', buttonElement]);
544
545 const divElement = divRef.current;
546 await act(() => {
547 dispatchClickEvent(divElement);
548 });
549 expect(onClick).toHaveBeenCalledTimes(3);
550 expect(onClickCapture).toHaveBeenCalledTimes(3);
551 expect(log[2]).toEqual(['capture', buttonElement]);
552 expect(log[3]).toEqual(['capture', divElement]);
553 expect(log[4]).toEqual(['bubble', divElement]);
554 expect(log[5]).toEqual(['bubble', buttonElement]);
555 });
556
557 it('handle propagation of click events between portals', async () => {
558 const buttonRef = React.createRef();
559 const divRef = React.createRef();
560 const log = [];
561 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
562 const onClickCapture = jest.fn(e =>
563 log.push(['capture', e.currentTarget]),
564 );
565
566 const portalElement = document.createElement('div');
567 document.body.appendChild(portalElement);
568
569 function Child() {
570 return (
571 <div
572 ref={divRef}
573 onClick={onClick}
574 onClickCapture={onClickCapture}>
575 Click me!
576 </div>
577 );
578 }
579
580 function Parent() {
581 return (
582 <button
583 ref={buttonRef}
584 onClick={onClick}
585 onClickCapture={onClickCapture}>
586 {ReactDOM.createPortal(<Child />, portalElement)}
587 </button>
588 );
589 }
590
591 const root = ReactDOMClient.createRoot(container);
592 await act(() => {
593 root.render(<Parent />);
594 });
595
596 const buttonElement = buttonRef.current;
597 dispatchClickEvent(buttonElement);
598 expect(onClick).toHaveBeenCalledTimes(1);
599 expect(onClickCapture).toHaveBeenCalledTimes(1);
600 expect(log[0]).toEqual(['capture', buttonElement]);
601 expect(log[1]).toEqual(['bubble', buttonElement]);
602
603 const divElement = divRef.current;
604 dispatchClickEvent(divElement);
605 expect(onClick).toHaveBeenCalledTimes(3);
606 expect(onClickCapture).toHaveBeenCalledTimes(3);
607 expect(log[2]).toEqual(['capture', buttonElement]);
608 expect(log[3]).toEqual(['capture', divElement]);
609 expect(log[4]).toEqual(['bubble', divElement]);
610 expect(log[5]).toEqual(['bubble', buttonElement]);
611
612 document.body.removeChild(portalElement);
613 });
614
615 it('handle click events on document.body portals', async () => {
616 const log = [];
617
618 function Child({label}) {
619 return <div onClick={() => log.push(label)}>{label}</div>;
620 }
621
622 function Parent() {
623 return (
624 <>
625 {ReactDOM.createPortal(
626 <Child label={'first'} />,
627 document.body,
628 )}
629 {ReactDOM.createPortal(
630 <Child label={'second'} />,
631 document.body,
632 )}
633 </>
634 );
635 }
636
637 const root = ReactDOMClient.createRoot(container);
638 await act(() => {
639 root.render(<Parent />);
640 });
641
642 const second = document.body.lastChild;
643 expect(second.textContent).toEqual('second');
644 dispatchClickEvent(second);
645
646 expect(log).toEqual(['second']);
647
648 const first = second.previousSibling;
649 expect(first.textContent).toEqual('first');
650 dispatchClickEvent(first);
651
652 expect(log).toEqual(['second', 'first']);
653 });
654
655 it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {
656 let suspend = false;
657 let resolve;
658 const promise = new Promise(
659 resolvePromise => (resolve = resolvePromise),
660 );
661
662 let clicks = 0;
663 const childSlotRef = React.createRef();
664
665 function Parent() {
666 return <div onClick={() => clicks++} ref={childSlotRef} />;
667 }
668
669 function Child({text}) {
670 if (suspend) {
671 throw promise;
672 } else {
673 return <a>Click me</a>;
674 }
675 }
676
677 function App() {
678 // The root is a Suspense boundary.
679 return (
680 <React.Suspense fallback="Loading...">
681 <Child />
682 </React.Suspense>
683 );
684 }
685
686 suspend = false;
687 const finalHTML = ReactDOMServer.renderToString(<App />);
688
689 const parentContainer = document.createElement('div');
690 const childContainer = document.createElement('div');
691
692 // We need this to be in the document since we'll dispatch events on it.
693 document.body.appendChild(parentContainer);
694
695 // We're going to use a different root as a parent.
696 // This lets us detect whether an event goes through React's event system.
697 const parentRoot = ReactDOMClient.createRoot(parentContainer);
698 await act(() => {
699 parentRoot.render(<Parent />);
700 });
701
702 childSlotRef.current.appendChild(childContainer);
703
704 childContainer.innerHTML = finalHTML;
705
706 const a = childContainer.getElementsByTagName('a')[0];
707
708 suspend = true;
709
710 // Hydrate asynchronously.
711 await act(() => {
712 ReactDOMClient.hydrateRoot(childContainer, <App />);
713 });
714
715 // The Suspense boundary is not yet hydrated.
716 await act(() => {
717 a.click();
718 });
719 expect(clicks).toBe(0);
720
721 // Resolving the promise so that rendering can complete.
722 await act(async () => {
723 suspend = false;
724 resolve();
725 await promise;
726 });
727
728 // We're now full hydrated.
729 expect(clicks).toBe(0);
730 document.body.removeChild(parentContainer);
731 });
732
733 it('handle click events on dynamic portals', async () => {
734 const log = [];
735
736 function Parent() {
737 const ref = React.useRef(null);
738 const [portal, setPortal] = React.useState(null);
739
740 React.useEffect(() => {
741 setPortal(
742 ReactDOM.createPortal(
743 <span onClick={() => log.push('child')} id="child" />,
744 ref.current,
745 ),
746 );
747 }, []);
748
749 return (
750 <div ref={ref} onClick={() => log.push('parent')} id="parent">
751 {portal}
752 </div>
753 );
754 }
755
756 const root = ReactDOMClient.createRoot(container);
757 await act(() => {
758 root.render(<Parent />);
759 });
760
761 const parent = container.lastChild;
762 expect(parent.id).toEqual('parent');
763
764 await act(() => {
765 dispatchClickEvent(parent);
766 });
767
768 expect(log).toEqual(['parent']);
769
770 const child = parent.lastChild;
771 expect(child.id).toEqual('child');
772
773 await act(() => {
774 dispatchClickEvent(child);
775 });
776
777 // we add both 'child' and 'parent' due to bubbling
778 expect(log).toEqual(['parent', 'child', 'parent']);
779 });
780
781 // Slight alteration to the last test, to catch
782 // a subtle difference in traversal.
783 it('handle click events on dynamic portals #2', async () => {
784 const log = [];
785
786 function Parent() {
787 const ref = React.useRef(null);
788 const [portal, setPortal] = React.useState(null);
789
790 React.useEffect(() => {
791 setPortal(
792 ReactDOM.createPortal(
793 <span onClick={() => log.push('child')} id="child" />,
794 ref.current,
795 ),
796 );
797 }, []);
798
799 return (
800 <div ref={ref} onClick={() => log.push('parent')} id="parent">
801 <div>{portal}</div>
802 </div>
803 );
804 }
805
806 const root = ReactDOMClient.createRoot(container);
807 await act(() => {
808 root.render(<Parent />);
809 });
810
811 const parent = container.lastChild;
812 expect(parent.id).toEqual('parent');
813
814 await act(() => {
815 dispatchClickEvent(parent);
816 });
817
818 expect(log).toEqual(['parent']);
819
820 const child = parent.lastChild;
821 expect(child.id).toEqual('child');
822
823 await act(() => {
824 dispatchClickEvent(child);
825 });
826
827 // we add both 'child' and 'parent' due to bubbling
828 expect(log).toEqual(['parent', 'child', 'parent']);
829 });
830
831 it('native stopPropagation on click events between portals', async () => {
832 const buttonRef = React.createRef();
833 const divRef = React.createRef();
834 const middleDivRef = React.createRef();
835 const log = [];
836 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
837 const onClickCapture = jest.fn(e =>
838 log.push(['capture', e.currentTarget]),
839 );
840
841 const portalElement = document.createElement('div');
842 document.body.appendChild(portalElement);
843
844 function Child() {
845 return (
846 <div ref={middleDivRef}>
847 <div
848 ref={divRef}
849 onClick={onClick}
850 onClickCapture={onClickCapture}>
851 Click me!
852 </div>
853 </div>
854 );
855 }
856
857 function Parent() {
858 React.useLayoutEffect(() => {
859 // This should prevent the portalElement listeners from
860 // capturing the events in the bubble phase.
861 middleDivRef.current.addEventListener('click', e => {
862 e.stopPropagation();
863 });
864 });
865
866 return (
867 <button
868 ref={buttonRef}
869 onClick={onClick}
870 onClickCapture={onClickCapture}>
871 {ReactDOM.createPortal(<Child />, portalElement)}
872 </button>
873 );
874 }
875
876 const root = ReactDOMClient.createRoot(container);
877 await act(() => {
878 root.render(<Parent />);
879 });
880
881 const buttonElement = buttonRef.current;
882 dispatchClickEvent(buttonElement);
883 expect(onClick).toHaveBeenCalledTimes(1);
884 expect(onClickCapture).toHaveBeenCalledTimes(1);
885 expect(log[0]).toEqual(['capture', buttonElement]);
886 expect(log[1]).toEqual(['bubble', buttonElement]);
887
888 const divElement = divRef.current;
889 dispatchClickEvent(divElement);
890 expect(onClick).toHaveBeenCalledTimes(1);
891 expect(onClickCapture).toHaveBeenCalledTimes(3);
892
893 document.body.removeChild(portalElement);
894 });
895
896 it('handle propagation of focus events', async () => {
897 const buttonRef = React.createRef();
898 const divRef = React.createRef();
899 const log = [];
900 const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
901 const onFocusCapture = jest.fn(e =>
902 log.push(['capture', e.currentTarget]),
903 );
904
905 function Test() {
906 return (
907 <button
908 ref={buttonRef}
909 onFocus={onFocus}
910 onFocusCapture={onFocusCapture}>
911 <div
912 ref={divRef}
913 onFocus={onFocus}
914 onFocusCapture={onFocusCapture}
915 tabIndex={0}>
916 Click me!
917 </div>
918 </button>
919 );
920 }
921
922 const root = ReactDOMClient.createRoot(container);
923 await act(() => {
924 root.render(<Test />);
925 });
926
927 const buttonElement = buttonRef.current;
928 buttonElement.focus();
929 expect(onFocus).toHaveBeenCalledTimes(1);
930 expect(onFocusCapture).toHaveBeenCalledTimes(1);
931 expect(log[0]).toEqual(['capture', buttonElement]);
932 expect(log[1]).toEqual(['bubble', buttonElement]);
933
934 const divElement = divRef.current;
935 divElement.focus();
936 expect(onFocus).toHaveBeenCalledTimes(3);
937 expect(onFocusCapture).toHaveBeenCalledTimes(3);
938 expect(log[2]).toEqual(['capture', buttonElement]);
939 expect(log[3]).toEqual(['capture', divElement]);
940 expect(log[4]).toEqual(['bubble', divElement]);
941 expect(log[5]).toEqual(['bubble', buttonElement]);
942 });
943
944 it('handle propagation of focus events between roots', async () => {
945 const buttonRef = React.createRef();
946 const divRef = React.createRef();
947 const childRef = React.createRef();
948 const log = [];
949 const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
950 const onFocusCapture = jest.fn(e =>
951 log.push(['capture', e.currentTarget]),
952 );
953
954 function Child() {
955 return (
956 <div
957 ref={divRef}
958 onFocus={onFocus}
959 onFocusCapture={onFocusCapture}
960 tabIndex={0}>
961 Click me!
962 </div>
963 );
964 }
965
966 function Parent() {
967 return (
968 <button
969 ref={buttonRef}
970 onFocus={onFocus}
971 onFocusCapture={onFocusCapture}>
972 <div ref={childRef} />
973 </button>
974 );
975 }
976
977 const root = ReactDOMClient.createRoot(container);
978 await act(() => {
979 root.render(<Parent />);
980 });
981 const childRoot = ReactDOMClient.createRoot(childRef.current);
982 await act(() => {
983 childRoot.render(<Child />);
984 });
985
986 const buttonElement = buttonRef.current;
987 buttonElement.focus();
988 expect(onFocus).toHaveBeenCalledTimes(1);
989 expect(onFocusCapture).toHaveBeenCalledTimes(1);
990 expect(log[0]).toEqual(['capture', buttonElement]);
991 expect(log[1]).toEqual(['bubble', buttonElement]);
992
993 const divElement = divRef.current;
994 divElement.focus();
995 expect(onFocus).toHaveBeenCalledTimes(3);
996 expect(onFocusCapture).toHaveBeenCalledTimes(3);
997 expect(log[2]).toEqual(['capture', buttonElement]);
998 expect(log[3]).toEqual(['capture', divElement]);
999 expect(log[4]).toEqual(['bubble', divElement]);
1000 expect(log[5]).toEqual(['bubble', buttonElement]);
1001 });
1002
1003 it('handle propagation of focus events between portals', async () => {
1004 const buttonRef = React.createRef();
1005 const divRef = React.createRef();
1006 const log = [];
1007 const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
1008 const onFocusCapture = jest.fn(e =>
1009 log.push(['capture', e.currentTarget]),
1010 );
1011
1012 const portalElement = document.createElement('div');
1013 document.body.appendChild(portalElement);
1014
1015 function Child() {
1016 return (
1017 <div
1018 ref={divRef}
1019 onFocus={onFocus}
1020 onFocusCapture={onFocusCapture}
1021 tabIndex={0}>
1022 Click me!
1023 </div>
1024 );
1025 }
1026
1027 function Parent() {
1028 return (
1029 <button
1030 ref={buttonRef}
1031 onFocus={onFocus}
1032 onFocusCapture={onFocusCapture}>
1033 {ReactDOM.createPortal(<Child />, portalElement)}
1034 </button>
1035 );
1036 }
1037
1038 const root = ReactDOMClient.createRoot(container);
1039 await act(() => {
1040 root.render(<Parent />);
1041 });
1042
1043 const buttonElement = buttonRef.current;
1044 buttonElement.focus();
1045 expect(onFocus).toHaveBeenCalledTimes(1);
1046 expect(onFocusCapture).toHaveBeenCalledTimes(1);
1047 expect(log[0]).toEqual(['capture', buttonElement]);
1048 expect(log[1]).toEqual(['bubble', buttonElement]);
1049
1050 const divElement = divRef.current;
1051 divElement.focus();
1052 expect(onFocus).toHaveBeenCalledTimes(3);
1053 expect(onFocusCapture).toHaveBeenCalledTimes(3);
1054 expect(log[2]).toEqual(['capture', buttonElement]);
1055 expect(log[3]).toEqual(['capture', divElement]);
1056 expect(log[4]).toEqual(['bubble', divElement]);
1057 expect(log[5]).toEqual(['bubble', buttonElement]);
1058
1059 document.body.removeChild(portalElement);
1060 });
1061
1062 it('native stopPropagation on focus events between portals', async () => {
1063 const buttonRef = React.createRef();
1064 const divRef = React.createRef();
1065 const middleDivRef = React.createRef();
1066 const log = [];
1067 const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
1068 const onFocusCapture = jest.fn(e =>
1069 log.push(['capture', e.currentTarget]),
1070 );
1071
1072 const portalElement = document.createElement('div');
1073 document.body.appendChild(portalElement);
1074
1075 function Child() {
1076 return (
1077 <div ref={middleDivRef}>
1078 <div
1079 ref={divRef}
1080 onFocus={onFocus}
1081 onFocusCapture={onFocusCapture}
1082 tabIndex={0}>
1083 Click me!
1084 </div>
1085 </div>
1086 );
1087 }
1088
1089 function Parent() {
1090 React.useLayoutEffect(() => {
1091 // This should prevent the portalElement listeners from
1092 // capturing the events in the bubble phase.
1093 middleDivRef.current.addEventListener('focusin', e => {
1094 e.stopPropagation();
1095 });
1096 });
1097
1098 return (
1099 <button
1100 ref={buttonRef}
1101 onFocus={onFocus}
1102 onFocusCapture={onFocusCapture}>
1103 {ReactDOM.createPortal(<Child />, portalElement)}
1104 </button>
1105 );
1106 }
1107
1108 const root = ReactDOMClient.createRoot(container);
1109 await act(() => {
1110 root.render(<Parent />);
1111 });
1112
1113 const buttonElement = buttonRef.current;
1114 buttonElement.focus();
1115 expect(onFocus).toHaveBeenCalledTimes(1);
1116 expect(onFocusCapture).toHaveBeenCalledTimes(1);
1117 expect(log[0]).toEqual(['capture', buttonElement]);
1118 expect(log[1]).toEqual(['bubble', buttonElement]);
1119
1120 const divElement = divRef.current;
1121 divElement.focus();
1122 expect(onFocus).toHaveBeenCalledTimes(1);
1123 expect(onFocusCapture).toHaveBeenCalledTimes(3);
1124
1125 document.body.removeChild(portalElement);
1126 });
1127
1128 it('handle propagation of enter and leave events between portals', async () => {
1129 const buttonRef = React.createRef();
1130 const divRef = React.createRef();
1131 const log = [];
1132 const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
1133 const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
1134
1135 const portalElement = document.createElement('div');
1136 document.body.appendChild(portalElement);
1137
1138 function Child() {
1139 return (
1140 <div
1141 ref={divRef}
1142 onMouseEnter={onMouseEnter}
1143 onMouseLeave={onMouseLeave}
1144 />
1145 );
1146 }
1147
1148 function Parent() {
1149 return (
1150 <button
1151 ref={buttonRef}
1152 onMouseEnter={onMouseEnter}
1153 onMouseLeave={onMouseLeave}>
1154 {ReactDOM.createPortal(<Child />, portalElement)}
1155 </button>
1156 );
1157 }
1158
1159 const root = ReactDOMClient.createRoot(container);
1160 await act(() => {
1161 root.render(<Parent />);
1162 });
1163
1164 const buttonElement = buttonRef.current;
1165 buttonElement.dispatchEvent(
1166 new MouseEvent('mouseover', {
1167 bubbles: true,
1168 cancelable: true,
1169 relatedTarget: null,
1170 }),
1171 );
1172 expect(onMouseEnter).toHaveBeenCalledTimes(1);
1173 expect(onMouseLeave).toHaveBeenCalledTimes(0);
1174 expect(log[0]).toEqual(buttonElement);
1175
1176 const divElement = divRef.current;
1177 buttonElement.dispatchEvent(
1178 new MouseEvent('mouseout', {
1179 bubbles: true,
1180 cancelable: true,
1181 relatedTarget: divElement,
1182 }),
1183 );
1184 divElement.dispatchEvent(
1185 new MouseEvent('mouseover', {
1186 bubbles: true,
1187 cancelable: true,
1188 relatedTarget: buttonElement,
1189 }),
1190 );
1191 expect(onMouseEnter).toHaveBeenCalledTimes(2);
1192 expect(onMouseLeave).toHaveBeenCalledTimes(0);
1193 expect(log[1]).toEqual(divElement);
1194
1195 document.body.removeChild(portalElement);
1196 });
1197
1198 it('handle propagation of enter and leave events between portals #2', async () => {
1199 const buttonRef = React.createRef();
1200 const divRef = React.createRef();
1201 const portalRef = React.createRef();
1202 const log = [];
1203 const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
1204 const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
1205
1206 function Child() {
1207 return (
1208 <div
1209 ref={divRef}
1210 onMouseEnter={onMouseEnter}
1211 onMouseLeave={onMouseLeave}
1212 />
1213 );
1214 }
1215
1216 function Parent() {
1217 const [portal, setPortal] = React.useState(null);
1218
1219 React.useLayoutEffect(() => {
1220 setPortal(ReactDOM.createPortal(<Child />, portalRef.current));
1221 }, []);
1222
1223 return (
1224 <button
1225 ref={buttonRef}
1226 onMouseEnter={onMouseEnter}
1227 onMouseLeave={onMouseLeave}>
1228 <div ref={portalRef}>{portal}</div>
1229 </button>
1230 );
1231 }
1232
1233 const root = ReactDOMClient.createRoot(container);
1234 await act(() => {
1235 root.render(<Parent />);
1236 });
1237
1238 const buttonElement = buttonRef.current;
1239 buttonElement.dispatchEvent(
1240 new MouseEvent('mouseover', {
1241 bubbles: true,
1242 cancelable: true,
1243 relatedTarget: null,
1244 }),
1245 );
1246 expect(onMouseEnter).toHaveBeenCalledTimes(1);
1247 expect(onMouseLeave).toHaveBeenCalledTimes(0);
1248 expect(log[0]).toEqual(buttonElement);
1249
1250 const divElement = divRef.current;
1251 buttonElement.dispatchEvent(
1252 new MouseEvent('mouseout', {
1253 bubbles: true,
1254 cancelable: true,
1255 relatedTarget: divElement,
1256 }),
1257 );
1258 divElement.dispatchEvent(
1259 new MouseEvent('mouseover', {
1260 bubbles: true,
1261 cancelable: true,
1262 relatedTarget: buttonElement,
1263 }),
1264 );
1265 expect(onMouseEnter).toHaveBeenCalledTimes(2);
1266 expect(onMouseLeave).toHaveBeenCalledTimes(0);
1267 expect(log[1]).toEqual(divElement);
1268 });
1269
1270 it('should preserve bubble/capture order between roots and nested portals', async () => {
1271 const targetRef = React.createRef();
1272 let log = [];
1273 const onClickRoot = jest.fn(e => log.push('bubble root'));
1274 const onClickCaptureRoot = jest.fn(e => log.push('capture root'));
1275 const onClickPortal = jest.fn(e => log.push('bubble portal'));
1276 const onClickCapturePortal = jest.fn(e => log.push('capture portal'));
1277
1278 function Portal() {
1279 return (
1280 <div
1281 onClick={onClickPortal}
1282 onClickCapture={onClickCapturePortal}
1283 ref={targetRef}>
1284 Click me!
1285 </div>
1286 );
1287 }
1288
1289 const portalContainer = document.createElement('div');
1290
1291 let shouldStopPropagation = false;
1292 portalContainer.addEventListener(
1293 'click',
1294 e => {
1295 if (shouldStopPropagation) {
1296 e.stopPropagation();
1297 }
1298 },
1299 false,
1300 );
1301
1302 function Root() {
1303 const portalTargetRef = React.useRef(null);
1304 React.useLayoutEffect(() => {
1305 portalTargetRef.current.appendChild(portalContainer);
1306 });
1307 return (
1308 <div onClick={onClickRoot} onClickCapture={onClickCaptureRoot}>
1309 <div ref={portalTargetRef} />
1310 {ReactDOM.createPortal(<Portal />, portalContainer)}
1311 </div>
1312 );
1313 }
1314
1315 const root = ReactDOMClient.createRoot(container);
1316 await act(() => {
1317 root.render(<Root />);
1318 });
1319
1320 const divElement = targetRef.current;
1321 dispatchClickEvent(divElement);
1322 expect(log).toEqual([
1323 'capture root',
1324 'capture portal',
1325 'bubble portal',
1326 'bubble root',
1327 ]);
1328
1329 log = [];
1330
1331 shouldStopPropagation = true;
1332 dispatchClickEvent(divElement);
1333
1334 if (enableLegacyFBSupport) {
1335 // We aren't using roots with legacyFBSupport, we put clicks on the document, so we exbit the previous
1336 // behavior.
1337 expect(log).toEqual(['capture root', 'capture portal']);
1338 } else {
1339 expect(log).toEqual([
1340 // The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
1341 'capture root',
1342 'capture portal',
1343 'bubble portal',
1344 'bubble root',
1345 ]);
1346 }
1347 });
1348
1349 describe('ReactDOM.createEventHandle', () => {
1350 beforeEach(() => {
1351 jest.resetModules();
1352 ReactFeatureFlags = require('shared/ReactFeatureFlags');
1353 ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
1354 ReactFeatureFlags.enableCreateEventHandleAPI = true;
1355
1356 React = require('react');
1357 ReactDOM = require('react-dom');
1358 ReactDOMClient = require('react-dom/client');
1359 Scheduler = require('scheduler');
1360 ReactDOMServer = require('react-dom/server');
1361 act = require('internal-test-utils').act;
1362
1363 const InternalTestUtils = require('internal-test-utils');
1364 waitForAll = InternalTestUtils.waitForAll;
1365 waitFor = InternalTestUtils.waitFor;
1366 });
1367
1368 // @gate www
1369 it('can render correctly with the ReactDOMServer', () => {
1370 const clickEvent = jest.fn();
1371 const setClick = ReactDOM.unstable_createEventHandle('click');
1372
1373 function Test() {
1374 const divRef = React.useRef(null);
1375
1376 React.useEffect(() => {
1377 return setClick(divRef.current, clickEvent);
1378 });
1379
1380 return <div ref={divRef}>Hello world</div>;
1381 }
1382 const output = ReactDOMServer.renderToString(<Test />);
1383 expect(output).toBe(`<div>Hello world</div>`);
1384 });
1385
1386 // @gate www
1387 it('can render correctly with the ReactDOMServer hydration', async () => {
1388 const clickEvent = jest.fn();
1389 const spanRef = React.createRef();
1390 const setClick = ReactDOM.unstable_createEventHandle('click');
1391
1392 function Test() {
1393 React.useEffect(() => {
1394 return setClick(spanRef.current, clickEvent);
1395 });
1396
1397 return (
1398 <div>
1399 <span ref={spanRef}>Hello world</span>
1400 </div>
1401 );
1402 }
1403 const output = ReactDOMServer.renderToString(<Test />);
1404 expect(output).toBe(`<div><span>Hello world</span></div>`);
1405 container.innerHTML = output;
1406 await act(() => {
1407 ReactDOMClient.hydrateRoot(container, <Test />);
1408 });
1409 dispatchClickEvent(spanRef.current);
1410 expect(clickEvent).toHaveBeenCalledTimes(1);
1411 });
1412
1413 // @gate www
1414 it('should correctly work for a basic "click" listener', async () => {
1415 let log = [];
1416 const clickEvent = jest.fn(event => {
1417 log.push({
1418 eventPhase: event.eventPhase,
1419 type: event.type,
1420 currentTarget: event.currentTarget,
1421 target: event.target,
1422 });
1423 });
1424 const divRef = React.createRef();
1425 const buttonRef = React.createRef();
1426 const setClick = ReactDOM.unstable_createEventHandle('click');
1427
1428 function Test() {
1429 React.useEffect(() => {
1430 return setClick(buttonRef.current, clickEvent);
1431 });
1432
1433 return (
1434 <button ref={buttonRef}>
1435 <div ref={divRef}>Click me!</div>
1436 </button>
1437 );
1438 }
1439
1440 const root = ReactDOMClient.createRoot(container);
1441 await act(() => {
1442 root.render(<Test />);
1443 });
1444
1445 expect(container.innerHTML).toBe(
1446 '<button><div>Click me!</div></button>',
1447 );
1448
1449 // Clicking the button should trigger the event callback
1450 let divElement = divRef.current;
1451 dispatchClickEvent(divElement);
1452 expect(log).toEqual([
1453 {
1454 eventPhase: 3,
1455 type: 'click',
1456 currentTarget: buttonRef.current,
1457 target: divRef.current,
1458 },
1459 ]);
1460 expect(clickEvent).toHaveBeenCalledTimes(1);
1461
1462 // Unmounting the container and clicking should not work
1463 await act(() => {
1464 root.render(null);
1465 });
1466
1467 dispatchClickEvent(divElement);
1468 expect(clickEvent).toHaveBeenCalledTimes(1);
1469
1470 // Re-rendering the container and clicking should work
1471 await act(() => {
1472 root.render(<Test />);
1473 });
1474
1475 divElement = divRef.current;
1476 dispatchClickEvent(divElement);
1477 expect(clickEvent).toHaveBeenCalledTimes(2);
1478
1479 log = [];
1480
1481 // Clicking the button should also work
1482 const buttonElement = buttonRef.current;
1483 dispatchClickEvent(buttonElement);
1484 expect(log).toEqual([
1485 {
1486 eventPhase: 3,
1487 type: 'click',
1488 currentTarget: buttonRef.current,
1489 target: buttonRef.current,
1490 },
1491 ]);
1492
1493 const setClick2 = ReactDOM.unstable_createEventHandle('click');
1494
1495 function Test2({clickEvent2}) {
1496 React.useEffect(() => {
1497 return setClick2(buttonRef.current, clickEvent2);
1498 });
1499
1500 return (
1501 <button ref={buttonRef}>
1502 <div ref={divRef}>Click me!</div>
1503 </button>
1504 );
1505 }
1506
1507 let clickEvent2 = jest.fn();
1508 await act(() => {
1509 root.render(<Test2 clickEvent2={clickEvent2} />);
1510 });
1511
1512 divElement = divRef.current;
1513 dispatchClickEvent(divElement);
1514 expect(clickEvent2).toHaveBeenCalledTimes(1);
1515
1516 // Reset the function we pass in, so it's different
1517 clickEvent2 = jest.fn();
1518 await act(() => {
1519 root.render(<Test2 clickEvent2={clickEvent2} />);
1520 });
1521
1522 divElement = divRef.current;
1523 dispatchClickEvent(divElement);
1524 expect(clickEvent2).toHaveBeenCalledTimes(1);
1525 });
1526
1527 // @gate www
1528 it('should correctly work for setting and clearing a basic "click" listener', async () => {
1529 const clickEvent = jest.fn();
1530 const divRef = React.createRef();
1531 const buttonRef = React.createRef();
1532 const setClick = ReactDOM.unstable_createEventHandle('click');
1533
1534 function Test({off}) {
1535 React.useEffect(() => {
1536 const clear = setClick(buttonRef.current, clickEvent);
1537 if (off) {
1538 clear();
1539 }
1540 return clear;
1541 });
1542
1543 return (
1544 <button ref={buttonRef}>
1545 <div ref={divRef}>Click me!</div>
1546 </button>
1547 );
1548 }
1549
1550 const root = ReactDOMClient.createRoot(container);
1551
1552 await act(() => {
1553 root.render(<Test off={false} />);
1554 });
1555
1556 let divElement = divRef.current;
1557 dispatchClickEvent(divElement);
1558 expect(clickEvent).toHaveBeenCalledTimes(1);
1559
1560 // The listener should get unmounted
1561 await act(() => {
1562 root.render(<Test off={true} />);
1563 });
1564
1565 clickEvent.mockClear();
1566
1567 divElement = divRef.current;
1568 dispatchClickEvent(divElement);
1569 expect(clickEvent).toHaveBeenCalledTimes(0);
1570 });
1571
1572 // @gate www
1573 it('should handle the target being a text node', async () => {
1574 const clickEvent = jest.fn();
1575 const buttonRef = React.createRef();
1576 const setClick = ReactDOM.unstable_createEventHandle('click');
1577
1578 function Test() {
1579 React.useEffect(() => {
1580 return setClick(buttonRef.current, clickEvent);
1581 });
1582
1583 return <button ref={buttonRef}>Click me!</button>;
1584 }
1585
1586 const root = ReactDOMClient.createRoot(container);
1587 await act(() => {
1588 root.render(<Test />);
1589 });
1590
1591 const textNode = buttonRef.current.firstChild;
1592 dispatchClickEvent(textNode);
1593 expect(clickEvent).toHaveBeenCalledTimes(1);
1594 });
1595
1596 // @gate www
1597 it('handle propagation of click events', async () => {
1598 const buttonRef = React.createRef();
1599 const divRef = React.createRef();
1600 const log = [];
1601 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1602 const onClickCapture = jest.fn(e =>
1603 log.push(['capture', e.currentTarget]),
1604 );
1605 const setClick = ReactDOM.unstable_createEventHandle('click');
1606 const setCaptureClick = ReactDOM.unstable_createEventHandle(
1607 'click',
1608 {
1609 capture: true,
1610 },
1611 );
1612
1613 function Test() {
1614 React.useEffect(() => {
1615 const clearClick1 = setClick(buttonRef.current, onClick);
1616 const clearCaptureClick1 = setCaptureClick(
1617 buttonRef.current,
1618 onClickCapture,
1619 );
1620 const clearClick2 = setClick(divRef.current, onClick);
1621 const clearCaptureClick2 = setCaptureClick(
1622 divRef.current,
1623 onClickCapture,
1624 );
1625
1626 return () => {
1627 clearClick1();
1628 clearCaptureClick1();
1629 clearClick2();
1630 clearCaptureClick2();
1631 };
1632 });
1633
1634 return (
1635 <button ref={buttonRef}>
1636 <div ref={divRef}>Click me!</div>
1637 </button>
1638 );
1639 }
1640
1641 const root = ReactDOMClient.createRoot(container);
1642 await act(() => {
1643 root.render(<Test />);
1644 });
1645
1646 const buttonElement = buttonRef.current;
1647 dispatchClickEvent(buttonElement);
1648 expect(onClick).toHaveBeenCalledTimes(1);
1649 expect(onClickCapture).toHaveBeenCalledTimes(1);
1650 expect(log[0]).toEqual(['capture', buttonElement]);
1651 expect(log[1]).toEqual(['bubble', buttonElement]);
1652
1653 log.length = 0;
1654 onClick.mockClear();
1655 onClickCapture.mockClear();
1656
1657 const divElement = divRef.current;
1658 dispatchClickEvent(divElement);
1659 expect(onClick).toHaveBeenCalledTimes(2);
1660 expect(onClickCapture).toHaveBeenCalledTimes(2);
1661 expect(log[0]).toEqual(['capture', buttonElement]);
1662 expect(log[1]).toEqual(['capture', divElement]);
1663 expect(log[2]).toEqual(['bubble', divElement]);
1664 expect(log[3]).toEqual(['bubble', buttonElement]);
1665 });
1666
1667 // @gate www
1668 it('handle propagation of click events mixed with onClick events', async () => {
1669 const buttonRef = React.createRef();
1670 const divRef = React.createRef();
1671 const log = [];
1672 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1673 const onClickCapture = jest.fn(e =>
1674 log.push(['capture', e.currentTarget]),
1675 );
1676 const setClick = ReactDOM.unstable_createEventHandle('click');
1677 const setClickCapture = ReactDOM.unstable_createEventHandle(
1678 'click',
1679 {
1680 capture: true,
1681 },
1682 );
1683
1684 function Test() {
1685 React.useEffect(() => {
1686 setClick(buttonRef.current, onClick);
1687 setClickCapture(buttonRef.current, onClickCapture);
1688
1689 return () => {
1690 setClick();
1691 setClickCapture();
1692 };
1693 });
1694
1695 return (
1696 <button ref={buttonRef}>
1697 <div
1698 ref={divRef}
1699 onClick={onClick}
1700 onClickCapture={onClickCapture}>
1701 Click me!
1702 </div>
1703 </button>
1704 );
1705 }
1706
1707 const root = ReactDOMClient.createRoot(container);
1708 await act(() => {
1709 root.render(<Test />);
1710 });
1711
1712 const buttonElement = buttonRef.current;
1713 dispatchClickEvent(buttonElement);
1714 expect(onClick).toHaveBeenCalledTimes(1);
1715 expect(onClickCapture).toHaveBeenCalledTimes(1);
1716 expect(log[0]).toEqual(['capture', buttonElement]);
1717 expect(log[1]).toEqual(['bubble', buttonElement]);
1718
1719 const divElement = divRef.current;
1720 dispatchClickEvent(divElement);
1721 expect(onClick).toHaveBeenCalledTimes(3);
1722 expect(onClickCapture).toHaveBeenCalledTimes(3);
1723 expect(log[2]).toEqual(['capture', buttonElement]);
1724 expect(log[3]).toEqual(['capture', divElement]);
1725 expect(log[4]).toEqual(['bubble', divElement]);
1726 expect(log[5]).toEqual(['bubble', buttonElement]);
1727 });
1728
1729 // @gate www
1730 it('should correctly work for a basic "click" listener on the outer target', async () => {
1731 const log = [];
1732 const clickEvent = jest.fn(event => {
1733 log.push({
1734 eventPhase: event.eventPhase,
1735 type: event.type,
1736 currentTarget: event.currentTarget,
1737 target: event.target,
1738 });
1739 });
1740 const divRef = React.createRef();
1741 const buttonRef = React.createRef();
1742 const setClick = ReactDOM.unstable_createEventHandle('click');
1743
1744 function Test() {
1745 React.useEffect(() => {
1746 return setClick(divRef.current, clickEvent);
1747 });
1748
1749 return (
1750 <button ref={buttonRef}>
1751 <div ref={divRef}>Click me!</div>
1752 </button>
1753 );
1754 }
1755
1756 const root = ReactDOMClient.createRoot(container);
1757 await act(() => {
1758 root.render(<Test />);
1759 });
1760
1761 expect(container.innerHTML).toBe(
1762 '<button><div>Click me!</div></button>',
1763 );
1764
1765 // Clicking the button should trigger the event callback
1766 let divElement = divRef.current;
1767 dispatchClickEvent(divElement);
1768 expect(log).toEqual([
1769 {
1770 eventPhase: 3,
1771 type: 'click',
1772 currentTarget: divRef.current,
1773 target: divRef.current,
1774 },
1775 ]);
1776
1777 // Unmounting the container and clicking should not work
1778 await act(() => {
1779 root.render(null);
1780 });
1781
1782 dispatchClickEvent(divElement);
1783 expect(clickEvent).toHaveBeenCalledTimes(1);
1784
1785 // Re-rendering the container and clicking should work
1786 await act(() => {
1787 root.render(<Test />);
1788 });
1789
1790 divElement = divRef.current;
1791 dispatchClickEvent(divElement);
1792 expect(clickEvent).toHaveBeenCalledTimes(2);
1793
1794 // Clicking the button should not work
1795 const buttonElement = buttonRef.current;
1796 dispatchClickEvent(buttonElement);
1797 expect(clickEvent).toHaveBeenCalledTimes(2);
1798 });
1799
1800 // @gate www
1801 it('should correctly handle many nested target listeners', async () => {
1802 const buttonRef = React.createRef();
1803 const targetListener1 = jest.fn();
1804 const targetListener2 = jest.fn();
1805 const targetListener3 = jest.fn();
1806 const targetListener4 = jest.fn();
1807 let setClick1 = ReactDOM.unstable_createEventHandle('click', {
1808 capture: true,
1809 });
1810 let setClick2 = ReactDOM.unstable_createEventHandle('click', {
1811 capture: true,
1812 });
1813 let setClick3 = ReactDOM.unstable_createEventHandle('click');
1814 let setClick4 = ReactDOM.unstable_createEventHandle('click');
1815
1816 function Test() {
1817 React.useEffect(() => {
1818 const clearClick1 = setClick1(
1819 buttonRef.current,
1820 targetListener1,
1821 );
1822 const clearClick2 = setClick2(
1823 buttonRef.current,
1824 targetListener2,
1825 );
1826 const clearClick3 = setClick3(
1827 buttonRef.current,
1828 targetListener3,
1829 );
1830 const clearClick4 = setClick4(
1831 buttonRef.current,
1832 targetListener4,
1833 );
1834
1835 return () => {
1836 clearClick1();
1837 clearClick2();
1838 clearClick3();
1839 clearClick4();
1840 };
1841 });
1842
1843 return <button ref={buttonRef}>Click me!</button>;
1844 }
1845
1846 const root = ReactDOMClient.createRoot(container);
1847 await act(() => {
1848 root.render(<Test />);
1849 });
1850
1851 let buttonElement = buttonRef.current;
1852 dispatchClickEvent(buttonElement);
1853
1854 expect(targetListener1).toHaveBeenCalledTimes(1);
1855 expect(targetListener2).toHaveBeenCalledTimes(1);
1856 expect(targetListener3).toHaveBeenCalledTimes(1);
1857 expect(targetListener4).toHaveBeenCalledTimes(1);
1858
1859 setClick1 = ReactDOM.unstable_createEventHandle('click');
1860 setClick2 = ReactDOM.unstable_createEventHandle('click');
1861 setClick3 = ReactDOM.unstable_createEventHandle('click');
1862 setClick4 = ReactDOM.unstable_createEventHandle('click');
1863
1864 function Test2() {
1865 React.useEffect(() => {
1866 const clearClick1 = setClick1(
1867 buttonRef.current,
1868 targetListener1,
1869 );
1870 const clearClick2 = setClick2(
1871 buttonRef.current,
1872 targetListener2,
1873 );
1874 const clearClick3 = setClick3(
1875 buttonRef.current,
1876 targetListener3,
1877 );
1878 const clearClick4 = setClick4(
1879 buttonRef.current,
1880 targetListener4,
1881 );
1882
1883 return () => {
1884 clearClick1();
1885 clearClick2();
1886 clearClick3();
1887 clearClick4();
1888 };
1889 });
1890
1891 return <button ref={buttonRef}>Click me!</button>;
1892 }
1893
1894 await act(() => {
1895 root.render(<Test2 />);
1896 });
1897
1898 buttonElement = buttonRef.current;
1899 dispatchClickEvent(buttonElement);
1900 expect(targetListener1).toHaveBeenCalledTimes(2);
1901 expect(targetListener2).toHaveBeenCalledTimes(2);
1902 expect(targetListener3).toHaveBeenCalledTimes(2);
1903 expect(targetListener4).toHaveBeenCalledTimes(2);
1904 });
1905
1906 // @gate www
1907 it('should correctly handle stopPropagation correctly for target events', async () => {
1908 const buttonRef = React.createRef();
1909 const divRef = React.createRef();
1910 const clickEvent = jest.fn();
1911 const setClick1 = ReactDOM.unstable_createEventHandle('click', {
1912 bind: buttonRef,
1913 });
1914 const setClick2 = ReactDOM.unstable_createEventHandle('click');
1915
1916 function Test() {
1917 React.useEffect(() => {
1918 const clearClick1 = setClick1(buttonRef.current, clickEvent);
1919 const clearClick2 = setClick2(divRef.current, e => {
1920 e.stopPropagation();
1921 });
1922
1923 return () => {
1924 clearClick1();
1925 clearClick2();
1926 };
1927 });
1928
1929 return (
1930 <button ref={buttonRef}>
1931 <div ref={divRef}>Click me!</div>
1932 </button>
1933 );
1934 }
1935
1936 const root = ReactDOMClient.createRoot(container);
1937 await act(() => {
1938 root.render(<Test />);
1939 });
1940
1941 const divElement = divRef.current;
1942 dispatchClickEvent(divElement);
1943 expect(clickEvent).toHaveBeenCalledTimes(0);
1944 });
1945
1946 // @gate www
1947 it('should correctly handle stopPropagation correctly for many target events', async () => {
1948 const buttonRef = React.createRef();
1949 const targetListener1 = jest.fn(e => e.stopPropagation());
1950 const targetListener2 = jest.fn(e => e.stopPropagation());
1951 const targetListener3 = jest.fn(e => e.stopPropagation());
1952 const targetListener4 = jest.fn(e => e.stopPropagation());
1953 const setClick1 = ReactDOM.unstable_createEventHandle('click');
1954 const setClick2 = ReactDOM.unstable_createEventHandle('click');
1955 const setClick3 = ReactDOM.unstable_createEventHandle('click');
1956 const setClick4 = ReactDOM.unstable_createEventHandle('click');
1957
1958 function Test() {
1959 React.useEffect(() => {
1960 const clearClick1 = setClick1(
1961 buttonRef.current,
1962 targetListener1,
1963 );
1964 const clearClick2 = setClick2(
1965 buttonRef.current,
1966 targetListener2,
1967 );
1968 const clearClick3 = setClick3(
1969 buttonRef.current,
1970 targetListener3,
1971 );
1972 const clearClick4 = setClick4(
1973 buttonRef.current,
1974 targetListener4,
1975 );
1976
1977 return () => {
1978 clearClick1();
1979 clearClick2();
1980 clearClick3();
1981 clearClick4();
1982 };
1983 });
1984
1985 return <button ref={buttonRef}>Click me!</button>;
1986 }
1987
1988 const root = ReactDOMClient.createRoot(container);
1989 await act(() => {
1990 root.render(<Test />);
1991 });
1992
1993 const buttonElement = buttonRef.current;
1994 dispatchClickEvent(buttonElement);
1995 expect(targetListener1).toHaveBeenCalledTimes(1);
1996 expect(targetListener2).toHaveBeenCalledTimes(1);
1997 expect(targetListener3).toHaveBeenCalledTimes(1);
1998 expect(targetListener4).toHaveBeenCalledTimes(1);
1999 });
2000
2001 // @gate www
2002 it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', async () => {
2003 const buttonRef = React.createRef();
2004 const targetListener1 = jest.fn(e => e.stopPropagation());
2005 const targetListener2 = jest.fn(e => e.stopPropagation());
2006 const targetListener3 = jest.fn(e => e.stopPropagation());
2007 const targetListener4 = jest.fn(e => e.stopPropagation());
2008 const setClick1 = ReactDOM.unstable_createEventHandle('click', {
2009 capture: true,
2010 });
2011 const setClick2 = ReactDOM.unstable_createEventHandle('click', {
2012 capture: true,
2013 });
2014 const setClick3 = ReactDOM.unstable_createEventHandle('click');
2015 const setClick4 = ReactDOM.unstable_createEventHandle('click');
2016
2017 function Test() {
2018 React.useEffect(() => {
2019 const clearClick1 = setClick1(
2020 buttonRef.current,
2021 targetListener1,
2022 );
2023 const clearClick2 = setClick2(
2024 buttonRef.current,
2025 targetListener2,
2026 );
2027 const clearClick3 = setClick3(
2028 buttonRef.current,
2029 targetListener3,
2030 );
2031 const clearClick4 = setClick4(
2032 buttonRef.current,
2033 targetListener4,
2034 );
2035
2036 return () => {
2037 clearClick1();
2038 clearClick2();
2039 clearClick3();
2040 clearClick4();
2041 };
2042 });
2043
2044 return <button ref={buttonRef}>Click me!</button>;
2045 }
2046
2047 const root = ReactDOMClient.createRoot(container);
2048 await act(() => {
2049 root.render(<Test />);
2050 });
2051
2052 const buttonElement = buttonRef.current;
2053 dispatchClickEvent(buttonElement);
2054 expect(targetListener1).toHaveBeenCalledTimes(1);
2055 expect(targetListener2).toHaveBeenCalledTimes(1);
2056 expect(targetListener3).toHaveBeenCalledTimes(0);
2057 expect(targetListener4).toHaveBeenCalledTimes(0);
2058 });
2059
2060 // @gate www
2061 it('should work with concurrent mode updates', async () => {
2062 const log = [];
2063 const ref = React.createRef();
2064 const setClick1 = ReactDOM.unstable_createEventHandle('click');
2065
2066 function Test({counter}) {
2067 React.useLayoutEffect(() => {
2068 return setClick1(ref.current, () => {
2069 log.push({counter});
2070 });
2071 });
2072
2073 Scheduler.log('Test');
2074 return <button ref={ref}>Press me</button>;
2075 }
2076
2077 const root = ReactDOMClient.createRoot(container);
2078 root.render(<Test counter={0} />);
2079
2080 await waitForAll(['Test']);
2081
2082 // Click the button
2083 dispatchClickEvent(ref.current);
2084 expect(log).toEqual([{counter: 0}]);
2085
2086 // Clear log
2087 log.length = 0;
2088
2089 // Increase counter
2090 React.startTransition(() => {
2091 root.render(<Test counter={1} />);
2092 });
2093 // Yield before committing
2094 await waitFor(['Test']);
2095
2096 // Click the button again
2097 dispatchClickEvent(ref.current);
2098 expect(log).toEqual([{counter: 0}]);
2099
2100 // Clear log
2101 log.length = 0;
2102
2103 // Commit
2104 await waitForAll([]);
2105 dispatchClickEvent(ref.current);
2106 expect(log).toEqual([{counter: 1}]);
2107 });
2108
2109 // @gate www
2110 it('should correctly work for a basic "click" window listener', async () => {
2111 const log = [];
2112 const clickEvent = jest.fn(event => {
2113 log.push({
2114 eventPhase: event.eventPhase,
2115 type: event.type,
2116 currentTarget: event.currentTarget,
2117 target: event.target,
2118 });
2119 });
2120 const setClick1 = ReactDOM.unstable_createEventHandle('click');
2121
2122 function Test() {
2123 React.useEffect(() => {
2124 return setClick1(window, clickEvent);
2125 });
2126
2127 return <button>Click anything!</button>;
2128 }
2129 const root = ReactDOMClient.createRoot(container);
2130 await act(() => {
2131 root.render(<Test />);
2132 });
2133
2134 expect(container.innerHTML).toBe(
2135 '<button>Click anything!</button>',
2136 );
2137
2138 // Clicking outside the button should trigger the event callback
2139 dispatchClickEvent(document.body);
2140 expect(log[0]).toEqual({
2141 eventPhase: 3,
2142 type: 'click',
2143 currentTarget: window,
2144 target: document.body,
2145 });
2146
2147 // Unmounting the container and clicking should not work
2148
2149 await act(() => {
2150 root.render(null);
2151 });
2152
2153 dispatchClickEvent(document.body);
2154 expect(clickEvent).toHaveBeenCalledTimes(1);
2155
2156 // Re-rendering and clicking the body should work again
2157 await act(() => {
2158 root.render(<Test />);
2159 });
2160
2161 dispatchClickEvent(document.body);
2162 expect(clickEvent).toHaveBeenCalledTimes(2);
2163 });
2164
2165 // @gate www
2166 it('handle propagation of click events on the window', async () => {
2167 const buttonRef = React.createRef();
2168 const divRef = React.createRef();
2169 const log = [];
2170 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2171 const onClickCapture = jest.fn(e =>
2172 log.push(['capture', e.currentTarget]),
2173 );
2174 const setClick = ReactDOM.unstable_createEventHandle('click');
2175 const setClickCapture = ReactDOM.unstable_createEventHandle(
2176 'click',
2177 {
2178 capture: true,
2179 },
2180 );
2181
2182 function Test() {
2183 React.useEffect(() => {
2184 const clearClick1 = setClick(window, onClick);
2185 const clearClickCapture1 = setClickCapture(
2186 window,
2187 onClickCapture,
2188 );
2189 const clearClick2 = setClick(buttonRef.current, onClick);
2190 const clearClickCapture2 = setClickCapture(
2191 buttonRef.current,
2192 onClickCapture,
2193 );
2194 const clearClick3 = setClick(divRef.current, onClick);
2195 const clearClickCapture3 = setClickCapture(
2196 divRef.current,
2197 onClickCapture,
2198 );
2199
2200 return () => {
2201 clearClick1();
2202 clearClickCapture1();
2203 clearClick2();
2204 clearClickCapture2();
2205 clearClick3();
2206 clearClickCapture3();
2207 };
2208 });
2209
2210 return (
2211 <button ref={buttonRef}>
2212 <div ref={divRef}>Click me!</div>
2213 </button>
2214 );
2215 }
2216
2217 const root = ReactDOMClient.createRoot(container);
2218 await act(() => {
2219 root.render(<Test />);
2220 });
2221
2222 const buttonElement = buttonRef.current;
2223 dispatchClickEvent(buttonElement);
2224 expect(onClick).toHaveBeenCalledTimes(2);
2225 expect(onClickCapture).toHaveBeenCalledTimes(2);
2226 expect(log[0]).toEqual(['capture', window]);
2227 expect(log[1]).toEqual(['capture', buttonElement]);
2228 expect(log[2]).toEqual(['bubble', buttonElement]);
2229 expect(log[3]).toEqual(['bubble', window]);
2230
2231 log.length = 0;
2232 onClick.mockClear();
2233 onClickCapture.mockClear();
2234
2235 const divElement = divRef.current;
2236 dispatchClickEvent(divElement);
2237 expect(onClick).toHaveBeenCalledTimes(3);
2238 expect(onClickCapture).toHaveBeenCalledTimes(3);
2239 expect(log[0]).toEqual(['capture', window]);
2240 expect(log[1]).toEqual(['capture', buttonElement]);
2241 expect(log[2]).toEqual(['capture', divElement]);
2242 expect(log[3]).toEqual(['bubble', divElement]);
2243 expect(log[4]).toEqual(['bubble', buttonElement]);
2244 expect(log[5]).toEqual(['bubble', window]);
2245 });
2246
2247 // @gate www
2248 it('should correctly handle stopPropagation for mixed listeners', async () => {
2249 const buttonRef = React.createRef();
2250 const rootListener1 = jest.fn(e => e.stopPropagation());
2251 const rootListener2 = jest.fn();
2252 const targetListener1 = jest.fn();
2253 const targetListener2 = jest.fn();
2254 const setClick1 = ReactDOM.unstable_createEventHandle('click', {
2255 capture: true,
2256 });
2257 const setClick2 = ReactDOM.unstable_createEventHandle('click', {
2258 capture: true,
2259 });
2260 const setClick3 = ReactDOM.unstable_createEventHandle('click');
2261 const setClick4 = ReactDOM.unstable_createEventHandle('click');
2262
2263 function Test() {
2264 React.useEffect(() => {
2265 const clearClick1 = setClick1(window, rootListener1);
2266 const clearClick2 = setClick2(
2267 buttonRef.current,
2268 targetListener1,
2269 );
2270 const clearClick3 = setClick3(window, rootListener2);
2271 const clearClick4 = setClick4(
2272 buttonRef.current,
2273 targetListener2,
2274 );
2275
2276 return () => {
2277 clearClick1();
2278 clearClick2();
2279 clearClick3();
2280 clearClick4();
2281 };
2282 });
2283
2284 return <button ref={buttonRef}>Click me!</button>;
2285 }
2286
2287 const root = ReactDOMClient.createRoot(container);
2288 await act(() => {
2289 root.render(<Test />);
2290 });
2291
2292 const buttonElement = buttonRef.current;
2293 dispatchClickEvent(buttonElement);
2294 expect(rootListener1).toHaveBeenCalledTimes(1);
2295 expect(targetListener1).toHaveBeenCalledTimes(0);
2296 expect(targetListener2).toHaveBeenCalledTimes(0);
2297 expect(rootListener2).toHaveBeenCalledTimes(0);
2298 });
2299
2300 // @gate www
2301 it('should correctly handle stopPropagation for delegated listeners', async () => {
2302 const buttonRef = React.createRef();
2303 const rootListener1 = jest.fn(e => e.stopPropagation());
2304 const rootListener2 = jest.fn();
2305 const rootListener3 = jest.fn(e => e.stopPropagation());
2306 const rootListener4 = jest.fn();
2307 const setClick1 = ReactDOM.unstable_createEventHandle('click', {
2308 capture: true,
2309 });
2310 const setClick2 = ReactDOM.unstable_createEventHandle('click', {
2311 capture: true,
2312 });
2313 const setClick3 = ReactDOM.unstable_createEventHandle('click');
2314 const setClick4 = ReactDOM.unstable_createEventHandle('click');
2315
2316 function Test() {
2317 React.useEffect(() => {
2318 const clearClick1 = setClick1(window, rootListener1);
2319 const clearClick2 = setClick2(window, rootListener2);
2320 const clearClick3 = setClick3(window, rootListener3);
2321 const clearClick4 = setClick4(window, rootListener4);
2322
2323 return () => {
2324 clearClick1();
2325 clearClick2();
2326 clearClick3();
2327 clearClick4();
2328 };
2329 });
2330
2331 return <button ref={buttonRef}>Click me!</button>;
2332 }
2333
2334 const root = ReactDOMClient.createRoot(container);
2335 await act(() => {
2336 root.render(<Test />);
2337 });
2338
2339 const buttonElement = buttonRef.current;
2340 dispatchClickEvent(buttonElement);
2341 expect(rootListener1).toHaveBeenCalledTimes(1);
2342 expect(rootListener2).toHaveBeenCalledTimes(1);
2343 expect(rootListener3).toHaveBeenCalledTimes(0);
2344 expect(rootListener4).toHaveBeenCalledTimes(0);
2345 });
2346
2347 // @gate www
2348 it('handle propagation of click events on the window and document', async () => {
2349 const buttonRef = React.createRef();
2350 const divRef = React.createRef();
2351 const log = [];
2352 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2353 const onClickCapture = jest.fn(e =>
2354 log.push(['capture', e.currentTarget]),
2355 );
2356 const setClick = ReactDOM.unstable_createEventHandle('click');
2357 const setClickCapture = ReactDOM.unstable_createEventHandle(
2358 'click',
2359 {
2360 capture: true,
2361 },
2362 );
2363
2364 function Test() {
2365 React.useEffect(() => {
2366 const clearClick1 = setClick(window, onClick);
2367 const clearClickCapture1 = setClickCapture(
2368 window,
2369 onClickCapture,
2370 );
2371 const clearClick2 = setClick(document, onClick);
2372 const clearClickCapture2 = setClickCapture(
2373 document,
2374 onClickCapture,
2375 );
2376 const clearClick3 = setClick(buttonRef.current, onClick);
2377 const clearClickCapture3 = setClickCapture(
2378 buttonRef.current,
2379 onClickCapture,
2380 );
2381 const clearClick4 = setClick(divRef.current, onClick);
2382 const clearClickCapture4 = setClickCapture(
2383 divRef.current,
2384 onClickCapture,
2385 );
2386
2387 return () => {
2388 clearClick1();
2389 clearClickCapture1();
2390 clearClick2();
2391 clearClickCapture2();
2392 clearClick3();
2393 clearClickCapture3();
2394 clearClick4();
2395 clearClickCapture4();
2396 };
2397 });
2398
2399 return (
2400 <button ref={buttonRef}>
2401 <div ref={divRef}>Click me!</div>
2402 </button>
2403 );
2404 }
2405
2406 const root = ReactDOMClient.createRoot(container);
2407 await act(() => {
2408 root.render(<Test />);
2409 });
2410
2411 const buttonElement = buttonRef.current;
2412 dispatchClickEvent(buttonElement);
2413 expect(onClick).toHaveBeenCalledTimes(3);
2414 expect(onClickCapture).toHaveBeenCalledTimes(3);
2415
2416 if (enableLegacyFBSupport) {
2417 expect(log[0]).toEqual(['capture', window]);
2418 expect(log[1]).toEqual(['capture', document]);
2419 expect(log[2]).toEqual(['capture', buttonElement]);
2420 expect(log[3]).toEqual(['bubble', document]);
2421 expect(log[4]).toEqual(['bubble', buttonElement]);
2422 expect(log[5]).toEqual(['bubble', window]);
2423 } else {
2424 expect(log[0]).toEqual(['capture', window]);
2425 expect(log[1]).toEqual(['capture', document]);
2426 expect(log[2]).toEqual(['capture', buttonElement]);
2427 expect(log[3]).toEqual(['bubble', buttonElement]);
2428 expect(log[4]).toEqual(['bubble', document]);
2429 expect(log[5]).toEqual(['bubble', window]);
2430 }
2431
2432 log.length = 0;
2433 onClick.mockClear();
2434 onClickCapture.mockClear();
2435
2436 const divElement = divRef.current;
2437 dispatchClickEvent(divElement);
2438 expect(onClick).toHaveBeenCalledTimes(4);
2439 expect(onClickCapture).toHaveBeenCalledTimes(4);
2440
2441 if (enableLegacyFBSupport) {
2442 expect(log[0]).toEqual(['capture', window]);
2443 expect(log[1]).toEqual(['capture', document]);
2444 expect(log[2]).toEqual(['capture', buttonElement]);
2445 expect(log[3]).toEqual(['capture', divElement]);
2446 expect(log[4]).toEqual(['bubble', document]);
2447 expect(log[5]).toEqual(['bubble', divElement]);
2448 expect(log[6]).toEqual(['bubble', buttonElement]);
2449 expect(log[7]).toEqual(['bubble', window]);
2450 } else {
2451 expect(log[0]).toEqual(['capture', window]);
2452 expect(log[1]).toEqual(['capture', document]);
2453 expect(log[2]).toEqual(['capture', buttonElement]);
2454 expect(log[3]).toEqual(['capture', divElement]);
2455 expect(log[4]).toEqual(['bubble', divElement]);
2456 expect(log[5]).toEqual(['bubble', buttonElement]);
2457 expect(log[6]).toEqual(['bubble', document]);
2458 expect(log[7]).toEqual(['bubble', window]);
2459 }
2460 });
2461
2462 // @gate www
2463 it('does not support custom user events', () => {
2464 // With eager listeners, supporting custom events via this API doesn't make sense
2465 // because we can't know a full list of them ahead of time. Let's check we throw
2466 // since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
2467 expect(() => {
2468 ReactDOM.unstable_createEventHandle('custom-event');
2469 }).toThrow(
2470 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
2471 );
2472 });
2473
2474 // @gate www
2475 it('beforeblur and afterblur are called after a focused element is unmounted', async () => {
2476 const log = [];
2477 // We have to persist here because we want to read relatedTarget later.
2478 const onAfterBlur = jest.fn(e => {
2479 e.persist();
2480 log.push(e.type);
2481 });
2482 const onBeforeBlur = jest.fn(e => log.push(e.type));
2483 const innerRef = React.createRef();
2484 const innerRef2 = React.createRef();
2485 const setAfterBlurHandle =
2486 ReactDOM.unstable_createEventHandle('afterblur');
2487 const setBeforeBlurHandle =
2488 ReactDOM.unstable_createEventHandle('beforeblur');
2489
2490 const Component = ({show}) => {
2491 const ref = React.useRef(null);
2492
2493 React.useEffect(() => {
2494 const clear1 = setAfterBlurHandle(document, onAfterBlur);
2495 const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2496
2497 return () => {
2498 clear1();
2499 clear2();
2500 };
2501 });
2502
2503 return (
2504 <div ref={ref}>
2505 {show && <input ref={innerRef} />}
2506 <div ref={innerRef2} />
2507 </div>
2508 );
2509 };
2510
2511 const root = ReactDOMClient.createRoot(container);
2512 await act(() => {
2513 root.render(<Component show={true} />);
2514 });
2515
2516 const inner = innerRef.current;
2517 const target = createEventTarget(inner);
2518 target.focus();
2519 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2520 expect(onAfterBlur).toHaveBeenCalledTimes(0);
2521
2522 await act(() => {
2523 root.render(<Component show={false} />);
2524 });
2525
2526 expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2527 expect(onAfterBlur).toHaveBeenCalledTimes(1);
2528 expect(onAfterBlur).toHaveBeenCalledWith(
2529 expect.objectContaining({relatedTarget: inner}),
2530 );
2531 expect(log).toEqual(['beforeblur', 'afterblur']);
2532 });
2533
2534 // @gate www
2535 it('beforeblur and afterblur are called after a nested focused element is unmounted', async () => {
2536 const log = [];
2537 // We have to persist here because we want to read relatedTarget later.
2538 const onAfterBlur = jest.fn(e => {
2539 e.persist();
2540 log.push(e.type);
2541 });
2542 const onBeforeBlur = jest.fn(e => log.push(e.type));
2543 const innerRef = React.createRef();
2544 const innerRef2 = React.createRef();
2545 const setAfterBlurHandle =
2546 ReactDOM.unstable_createEventHandle('afterblur');
2547 const setBeforeBlurHandle =
2548 ReactDOM.unstable_createEventHandle('beforeblur');
2549
2550 const Component = ({show}) => {
2551 const ref = React.useRef(null);
2552
2553 React.useEffect(() => {
2554 const clear1 = setAfterBlurHandle(document, onAfterBlur);
2555 const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2556
2557 return () => {
2558 clear1();
2559 clear2();
2560 };
2561 });
2562
2563 return (
2564 <div ref={ref}>
2565 {show && (
2566 <div>
2567 <input ref={innerRef} />
2568 </div>
2569 )}
2570 <div ref={innerRef2} />
2571 </div>
2572 );
2573 };
2574
2575 const root = ReactDOMClient.createRoot(container);
2576 await act(() => {
2577 root.render(<Component show={true} />);
2578 });
2579
2580 const inner = innerRef.current;
2581 const target = createEventTarget(inner);
2582 target.focus();
2583 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2584 expect(onAfterBlur).toHaveBeenCalledTimes(0);
2585
2586 await act(() => {
2587 root.render(<Component show={false} />);
2588 });
2589
2590 expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2591 expect(onAfterBlur).toHaveBeenCalledTimes(1);
2592 expect(onAfterBlur).toHaveBeenCalledWith(
2593 expect.objectContaining({relatedTarget: inner}),
2594 );
2595 expect(log).toEqual(['beforeblur', 'afterblur']);
2596 });
2597
2598 // @gate www
2599 it('beforeblur should skip handlers from a deleted subtree after the focused element is unmounted', async () => {
2600 const onBeforeBlur = jest.fn();
2601 const innerRef = React.createRef();
2602 const innerRef2 = React.createRef();
2603 const setBeforeBlurHandle =
2604 ReactDOM.unstable_createEventHandle('beforeblur');
2605 const ref2 = React.createRef();
2606
2607 const Component = ({show}) => {
2608 const ref = React.useRef(null);
2609
2610 React.useEffect(() => {
2611 const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2612 let clear2;
2613 if (ref2.current) {
2614 clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur);
2615 }
2616
2617 return () => {
2618 clear1();
2619 if (clear2) {
2620 clear2();
2621 }
2622 };
2623 });
2624
2625 return (
2626 <div ref={ref}>
2627 {show && (
2628 <div ref={ref2}>
2629 <input ref={innerRef} />
2630 </div>
2631 )}
2632 <div ref={innerRef2} />
2633 </div>
2634 );
2635 };
2636
2637 const root = ReactDOMClient.createRoot(container);
2638 await act(() => {
2639 root.render(<Component show={true} />);
2640 });
2641
2642 const inner = innerRef.current;
2643 const target = createEventTarget(inner);
2644 target.focus();
2645 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2646
2647 await act(() => {
2648 root.render(<Component show={false} />);
2649 });
2650
2651 expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2652 });
2653
2654 // @gate www
2655 it('beforeblur and afterblur are called after a focused element is suspended', async () => {
2656 const log = [];
2657 // We have to persist here because we want to read relatedTarget later.
2658 const onAfterBlur = jest.fn(e => {
2659 e.persist();
2660 log.push(e.type);
2661 });
2662 const onBeforeBlur = jest.fn(e => log.push(e.type));
2663 const innerRef = React.createRef();
2664 const Suspense = React.Suspense;
2665 let suspend = false;
2666 let resolve;
2667 const promise = new Promise(
2668 resolvePromise => (resolve = resolvePromise),
2669 );
2670 const setAfterBlurHandle =
2671 ReactDOM.unstable_createEventHandle('afterblur');
2672 const setBeforeBlurHandle =
2673 ReactDOM.unstable_createEventHandle('beforeblur');
2674
2675 function Child() {
2676 if (suspend) {
2677 throw promise;
2678 } else {
2679 return <input ref={innerRef} />;
2680 }
2681 }
2682
2683 const Component = () => {
2684 const ref = React.useRef(null);
2685
2686 React.useEffect(() => {
2687 const clear1 = setAfterBlurHandle(document, onAfterBlur);
2688 const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2689
2690 return () => {
2691 clear1();
2692 clear2();
2693 };
2694 });
2695
2696 return (
2697 <div ref={ref}>
2698 <Suspense fallback="Loading...">
2699 <Child />
2700 </Suspense>
2701 </div>
2702 );
2703 };
2704
2705 const container2 = document.createElement('div');
2706 document.body.appendChild(container2);
2707
2708 const root = ReactDOMClient.createRoot(container2);
2709
2710 await act(() => {
2711 root.render(<Component />);
2712 });
2713 jest.runAllTimers();
2714
2715 const inner = innerRef.current;
2716 const target = createEventTarget(inner);
2717 target.focus();
2718 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2719 expect(onAfterBlur).toHaveBeenCalledTimes(0);
2720
2721 suspend = true;
2722 await act(() => {
2723 root.render(<Component />);
2724 });
2725 jest.runAllTimers();
2726
2727 expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2728 expect(onAfterBlur).toHaveBeenCalledTimes(1);
2729 expect(onAfterBlur).toHaveBeenCalledWith(
2730 expect.objectContaining({relatedTarget: inner}),
2731 );
2732 resolve();
2733 expect(log).toEqual(['beforeblur', 'afterblur']);
2734
2735 document.body.removeChild(container2);
2736 });
2737
2738 // @gate www
2739 it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', async () => {
2740 const onBeforeBlur = jest.fn();
2741 const innerRef = React.createRef();
2742 const innerRef2 = React.createRef();
2743 const setBeforeBlurHandle =
2744 ReactDOM.unstable_createEventHandle('beforeblur');
2745 const ref2 = React.createRef();
2746 const Suspense = React.Suspense;
2747 let suspend = false;
2748 let resolve;
2749 const promise = new Promise(
2750 resolvePromise => (resolve = resolvePromise),
2751 );
2752
2753 function Child() {
2754 if (suspend) {
2755 throw promise;
2756 } else {
2757 return <input ref={innerRef} />;
2758 }
2759 }
2760
2761 const Component = () => {
2762 const ref = React.useRef(null);
2763
2764 React.useEffect(() => {
2765 const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2766 let clear2;
2767 if (ref2.current) {
2768 clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur);
2769 }
2770
2771 return () => {
2772 clear1();
2773 if (clear2) {
2774 clear2();
2775 }
2776 };
2777 });
2778
2779 return (
2780 <div ref={ref}>
2781 <Suspense fallback="Loading...">
2782 <div ref={ref2}>
2783 <Child />
2784 </div>
2785 </Suspense>
2786 <div ref={innerRef2} />
2787 </div>
2788 );
2789 };
2790
2791 const container2 = document.createElement('div');
2792 document.body.appendChild(container2);
2793
2794 const root = ReactDOMClient.createRoot(container2);
2795
2796 await act(() => {
2797 root.render(<Component />);
2798 });
2799 jest.runAllTimers();
2800
2801 const inner = innerRef.current;
2802 const target = createEventTarget(inner);
2803 target.focus();
2804 expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2805
2806 suspend = true;
2807 await act(() => {
2808 root.render(<Component />);
2809 });
2810 jest.runAllTimers();
2811
2812 expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2813 resolve();
2814
2815 document.body.removeChild(container2);
2816 });
2817
2818 // @gate www
2819 it('regression: does not fire beforeblur/afterblur if target is already hidden', async () => {
2820 const Suspense = React.Suspense;
2821 let suspend = false;
2822 const fakePromise = {then() {}};
2823 const setBeforeBlurHandle =
2824 ReactDOM.unstable_createEventHandle('beforeblur');
2825 const innerRef = React.createRef();
2826
2827 function Child() {
2828 if (suspend) {
2829 throw fakePromise;
2830 }
2831 return <input ref={innerRef} />;
2832 }
2833
2834 const Component = () => {
2835 const ref = React.useRef(null);
2836 const [, setState] = React.useState(0);
2837
2838 React.useEffect(() => {
2839 return setBeforeBlurHandle(ref.current, () => {
2840 // In the regression case, this would trigger an update, then
2841 // the resulting render would trigger another blur event,
2842 // which would trigger an update again, and on and on in an
2843 // infinite loop.
2844 setState(n => n + 1);
2845 });
2846 }, []);
2847
2848 return (
2849 <div ref={ref}>
2850 <Suspense fallback="Loading...">
2851 <Child />
2852 </Suspense>
2853 </div>
2854 );
2855 };
2856
2857 const container2 = document.createElement('div');
2858 document.body.appendChild(container2);
2859
2860 const root = ReactDOMClient.createRoot(container2);
2861 await act(() => {
2862 root.render(<Component />);
2863 });
2864
2865 // Focus the input node
2866 const inner = innerRef.current;
2867 const target = createEventTarget(inner);
2868 target.focus();
2869
2870 // Suspend. This hides the input node, causing it to lose focus.
2871 suspend = true;
2872 await act(() => {
2873 root.render(<Component />);
2874 });
2875
2876 document.body.removeChild(container2);
2877 });
2878
2879 // @gate !disableCommentsAsDOMContainers
2880 it('handle propagation of click events between disjointed comment roots', async () => {
2881 const buttonRef = React.createRef();
2882 const divRef = React.createRef();
2883 const log = [];
2884 const setClick = ReactDOM.unstable_createEventHandle('click');
2885 const setClickCapture = ReactDOM.unstable_createEventHandle(
2886 'click',
2887 {capture: true},
2888 );
2889 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2890 const onClickCapture = jest.fn(e =>
2891 log.push(['capture', e.currentTarget]),
2892 );
2893
2894 function Child() {
2895 React.useEffect(() => {
2896 const click1 = setClick(divRef.current, onClick);
2897 const click2 = setClickCapture(divRef.current, onClickCapture);
2898 return () => {
2899 click1();
2900 click2();
2901 };
2902 });
2903
2904 return <div ref={divRef}>Click me!</div>;
2905 }
2906
2907 function Parent() {
2908 React.useEffect(() => {
2909 const click1 = setClick(buttonRef.current, onClick);
2910 const click2 = setClickCapture(
2911 buttonRef.current,
2912 onClickCapture,
2913 );
2914 return () => {
2915 click1();
2916 click2();
2917 };
2918 });
2919
2920 return <button ref={buttonRef} />;
2921 }
2922
2923 // We use a comment node here, then mount to it
2924 const disjointedNode = document.createComment(
2925 ' react-mount-point-unstable ',
2926 );
2927 const root = ReactDOMClient.createRoot(container);
2928 await act(() => {
2929 root.render(<Parent />);
2930 });
2931 buttonRef.current.appendChild(disjointedNode);
2932 const disjointedNodeRoot =
2933 ReactDOMClient.createRoot(disjointedNode);
2934 await act(() => {
2935 disjointedNodeRoot.render(<Child />);
2936 });
2937
2938 const buttonElement = buttonRef.current;
2939 dispatchClickEvent(buttonElement);
2940 expect(onClick).toHaveBeenCalledTimes(1);
2941 expect(onClickCapture).toHaveBeenCalledTimes(1);
2942 expect(log[0]).toEqual(['capture', buttonElement]);
2943 expect(log[1]).toEqual(['bubble', buttonElement]);
2944
2945 const divElement = divRef.current;
2946 dispatchClickEvent(divElement);
2947 expect(onClick).toHaveBeenCalledTimes(3);
2948 expect(onClickCapture).toHaveBeenCalledTimes(3);
2949 expect(log[2]).toEqual(['capture', buttonElement]);
2950 expect(log[3]).toEqual(['capture', divElement]);
2951 expect(log[4]).toEqual(['bubble', divElement]);
2952 expect(log[5]).toEqual(['bubble', buttonElement]);
2953 });
2954
2955 // @gate www
2956 it('propagates known createEventHandle events through portals without inner listeners', async () => {
2957 const buttonRef = React.createRef();
2958 const divRef = React.createRef();
2959 const log = [];
2960 const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2961 const onClickCapture = jest.fn(e =>
2962 log.push(['capture', e.currentTarget]),
2963 );
2964 const setClick = ReactDOM.unstable_createEventHandle('click');
2965 const setClickCapture = ReactDOM.unstable_createEventHandle(
2966 'click',
2967 {
2968 capture: true,
2969 },
2970 );
2971
2972 const portalElement = document.createElement('div');
2973 document.body.appendChild(portalElement);
2974
2975 function Child() {
2976 return <div ref={divRef}>Click me!</div>;
2977 }
2978
2979 function Parent() {
2980 React.useEffect(() => {
2981 const clear1 = setClick(buttonRef.current, onClick);
2982 const clear2 = setClickCapture(
2983 buttonRef.current,
2984 onClickCapture,
2985 );
2986 return () => {
2987 clear1();
2988 clear2();
2989 };
2990 });
2991
2992 return (
2993 <button ref={buttonRef}>
2994 {ReactDOM.createPortal(<Child />, portalElement)}
2995 </button>
2996 );
2997 }
2998
2999 const root = ReactDOMClient.createRoot(container);
3000 await act(() => {
3001 root.render(<Parent />);
3002 });
3003
3004 const divElement = divRef.current;
3005 const buttonElement = buttonRef.current;
3006 dispatchClickEvent(divElement);
3007 expect(onClick).toHaveBeenCalledTimes(1);
3008 expect(onClickCapture).toHaveBeenCalledTimes(1);
3009 expect(log[0]).toEqual(['capture', buttonElement]);
3010 expect(log[1]).toEqual(['bubble', buttonElement]);
3011
3012 document.body.removeChild(portalElement);
3013 });
3014
3015 describe('Compatibility with Scopes API', () => {
3016 beforeEach(() => {
3017 jest.resetModules();
3018 ReactFeatureFlags = require('shared/ReactFeatureFlags');
3019 ReactFeatureFlags.enableCreateEventHandleAPI = true;
3020 ReactFeatureFlags.enableScopeAPI = true;
3021
3022 React = require('react');
3023 ReactDOM = require('react-dom');
3024 ReactDOMClient = require('react-dom/client');
3025 Scheduler = require('scheduler');
3026 ReactDOMServer = require('react-dom/server');
3027 act = require('internal-test-utils').act;
3028 });
3029
3030 // @gate www
3031 it('handle propagation of click events on a scope', async () => {
3032 const buttonRef = React.createRef();
3033 const log = [];
3034 const onClick = jest.fn(e =>
3035 log.push(['bubble', e.currentTarget]),
3036 );
3037 const onClickCapture = jest.fn(e =>
3038 log.push(['capture', e.currentTarget]),
3039 );
3040 const TestScope = React.unstable_Scope;
3041 const setClick = ReactDOM.unstable_createEventHandle('click');
3042 const setClickCapture = ReactDOM.unstable_createEventHandle(
3043 'click',
3044 {
3045 capture: true,
3046 },
3047 );
3048
3049 function Test() {
3050 const scopeRef = React.useRef(null);
3051
3052 React.useEffect(() => {
3053 const clear1 = setClick(scopeRef.current, onClick);
3054 const clear2 = setClickCapture(
3055 scopeRef.current,
3056 onClickCapture,
3057 );
3058
3059 return () => {
3060 clear1();
3061 clear2();
3062 };
3063 });
3064
3065 return (
3066 <TestScope ref={scopeRef}>
3067 <button ref={buttonRef} />
3068 </TestScope>
3069 );
3070 }
3071
3072 const root = ReactDOMClient.createRoot(container);
3073 await act(() => {
3074 root.render(<Test />);
3075 });
3076
3077 const buttonElement = buttonRef.current;
3078 dispatchClickEvent(buttonElement);
3079
3080 expect(onClick).toHaveBeenCalledTimes(1);
3081 expect(onClickCapture).toHaveBeenCalledTimes(1);
3082 expect(log).toEqual([
3083 ['capture', buttonElement],
3084 ['bubble', buttonElement],
3085 ]);
3086 });
3087
3088 // @gate www
3089 it('handle mixed propagation of click events on a scope', async () => {
3090 const buttonRef = React.createRef();
3091 const divRef = React.createRef();
3092 const log = [];
3093 const onClick = jest.fn(e =>
3094 log.push(['bubble', e.currentTarget]),
3095 );
3096 const onClickCapture = jest.fn(e =>
3097 log.push(['capture', e.currentTarget]),
3098 );
3099 const TestScope = React.unstable_Scope;
3100 const setClick = ReactDOM.unstable_createEventHandle('click');
3101 const setClickCapture = ReactDOM.unstable_createEventHandle(
3102 'click',
3103 {
3104 capture: true,
3105 },
3106 );
3107
3108 function Test() {
3109 const scopeRef = React.useRef(null);
3110
3111 React.useEffect(() => {
3112 const clear1 = setClick(scopeRef.current, onClick);
3113 const clear2 = setClickCapture(
3114 scopeRef.current,
3115 onClickCapture,
3116 );
3117 const clear3 = setClick(buttonRef.current, onClick);
3118 const clear4 = setClickCapture(
3119 buttonRef.current,
3120 onClickCapture,
3121 );
3122
3123 return () => {
3124 clear1();
3125 clear2();
3126 clear3();
3127 clear4();
3128 };
3129 });
3130
3131 return (
3132 <TestScope ref={scopeRef}>
3133 <button ref={buttonRef}>
3134 <div
3135 ref={divRef}
3136 onClick={onClick}
3137 onClickCapture={onClickCapture}>
3138 Click me!
3139 </div>
3140 </button>
3141 </TestScope>
3142 );
3143 }
3144
3145 const root = ReactDOMClient.createRoot(container);
3146 await act(() => {
3147 root.render(<Test />);
3148 });
3149
3150 const buttonElement = buttonRef.current;
3151 dispatchClickEvent(buttonElement);
3152
3153 expect(onClick).toHaveBeenCalledTimes(2);
3154 expect(onClickCapture).toHaveBeenCalledTimes(2);
3155 expect(log).toEqual([
3156 ['capture', buttonElement],
3157 ['capture', buttonElement],
3158 ['bubble', buttonElement],
3159 ['bubble', buttonElement],
3160 ]);
3161
3162 log.length = 0;
3163 onClick.mockClear();
3164 onClickCapture.mockClear();
3165
3166 const divElement = divRef.current;
3167 dispatchClickEvent(divElement);
3168
3169 expect(onClick).toHaveBeenCalledTimes(3);
3170 expect(onClickCapture).toHaveBeenCalledTimes(3);
3171 expect(log).toEqual([
3172 ['capture', buttonElement],
3173 ['capture', buttonElement],
3174 ['capture', divElement],
3175 ['bubble', divElement],
3176 ['bubble', buttonElement],
3177 ['bubble', buttonElement],
3178 ]);
3179 });
3180
3181 // @gate www
3182 it('should not handle the target being a dangling text node within a scope', async () => {
3183 const clickEvent = jest.fn();
3184 const buttonRef = React.createRef();
3185 const TestScope = React.unstable_Scope;
3186 const setClick = ReactDOM.unstable_createEventHandle('click');
3187
3188 function Test() {
3189 const scopeRef = React.useRef(null);
3190
3191 React.useEffect(() => {
3192 return setClick(scopeRef.current, clickEvent);
3193 });
3194
3195 return (
3196 <button ref={buttonRef}>
3197 <TestScope ref={scopeRef}>Click me!</TestScope>
3198 </button>
3199 );
3200 }
3201
3202 const root = ReactDOMClient.createRoot(container);
3203 await act(() => {
3204 root.render(<Test />);
3205 });
3206
3207 const textNode = buttonRef.current.firstChild;
3208 dispatchClickEvent(textNode);
3209 // This should not work, as the target instance will be the
3210 // <button>, which is actually outside the scope.
3211 expect(clickEvent).toHaveBeenCalledTimes(0);
3212 });
3213
3214 // @gate www
3215 it('handle stopPropagation (inner) correctly between scopes', async () => {
3216 const buttonRef = React.createRef();
3217 const outerOnClick = jest.fn();
3218 const innerOnClick = jest.fn(e => e.stopPropagation());
3219 const TestScope = React.unstable_Scope;
3220 const TestScope2 = React.unstable_Scope;
3221 const setClick = ReactDOM.unstable_createEventHandle('click');
3222
3223 function Test() {
3224 const scopeRef = React.useRef(null);
3225 const scope2Ref = React.useRef(null);
3226
3227 React.useEffect(() => {
3228 const clear1 = setClick(scopeRef.current, outerOnClick);
3229 const clear2 = setClick(scope2Ref.current, innerOnClick);
3230
3231 return () => {
3232 clear1();
3233 clear2();
3234 };
3235 });
3236
3237 return (
3238 <TestScope ref={scopeRef}>
3239 <TestScope2 ref={scope2Ref}>
3240 <button ref={buttonRef} />
3241 </TestScope2>
3242 </TestScope>
3243 );
3244 }
3245
3246 const root = ReactDOMClient.createRoot(container);
3247 await act(() => {
3248 root.render(<Test />);
3249 });
3250
3251 const buttonElement = buttonRef.current;
3252 dispatchClickEvent(buttonElement);
3253
3254 expect(innerOnClick).toHaveBeenCalledTimes(1);
3255 expect(outerOnClick).toHaveBeenCalledTimes(0);
3256 });
3257
3258 // @gate www
3259 it('handle stopPropagation (outer) correctly between scopes', async () => {
3260 const buttonRef = React.createRef();
3261 const outerOnClick = jest.fn(e => e.stopPropagation());
3262 const innerOnClick = jest.fn();
3263 const TestScope = React.unstable_Scope;
3264 const TestScope2 = React.unstable_Scope;
3265 const setClick = ReactDOM.unstable_createEventHandle('click');
3266
3267 function Test() {
3268 const scopeRef = React.useRef(null);
3269 const scope2Ref = React.useRef(null);
3270
3271 React.useEffect(() => {
3272 const clear1 = setClick(scopeRef.current, outerOnClick);
3273 const clear2 = setClick(scope2Ref.current, innerOnClick);
3274
3275 return () => {
3276 clear1();
3277 clear2();
3278 };
3279 });
3280
3281 return (
3282 <TestScope ref={scopeRef}>
3283 <TestScope2 ref={scope2Ref}>
3284 <button ref={buttonRef} />
3285 </TestScope2>
3286 </TestScope>
3287 );
3288 }
3289
3290 const root = ReactDOMClient.createRoot(container);
3291 await act(() => {
3292 root.render(<Test />);
3293 });
3294
3295 const buttonElement = buttonRef.current;
3296 dispatchClickEvent(buttonElement);
3297
3298 expect(innerOnClick).toHaveBeenCalledTimes(1);
3299 expect(outerOnClick).toHaveBeenCalledTimes(1);
3300 });
3301
3302 // @gate www
3303 it('handle stopPropagation (inner and outer) correctly between scopes', async () => {
3304 const buttonRef = React.createRef();
3305 const onClick = jest.fn(e => e.stopPropagation());
3306 const TestScope = React.unstable_Scope;
3307 const TestScope2 = React.unstable_Scope;
3308 const setClick = ReactDOM.unstable_createEventHandle('click');
3309
3310 function Test() {
3311 const scopeRef = React.useRef(null);
3312 const scope2Ref = React.useRef(null);
3313
3314 React.useEffect(() => {
3315 const clear1 = setClick(scopeRef.current, onClick);
3316 const clear2 = setClick(scope2Ref.current, onClick);
3317
3318 return () => {
3319 clear1();
3320 clear2();
3321 };
3322 });
3323
3324 return (
3325 <TestScope ref={scopeRef}>
3326 <TestScope2 ref={scope2Ref}>
3327 <button ref={buttonRef} />
3328 </TestScope2>
3329 </TestScope>
3330 );
3331 }
3332
3333 const root = ReactDOMClient.createRoot(container);
3334 await act(() => {
3335 root.render(<Test />);
3336 });
3337
3338 const buttonElement = buttonRef.current;
3339 dispatchClickEvent(buttonElement);
3340
3341 expect(onClick).toHaveBeenCalledTimes(1);
3342 });
3343
3344 // @gate www
3345 it('should be able to register handlers for events affected by the intervention', async () => {
3346 const rootContainer = document.createElement('div');
3347 container.appendChild(rootContainer);
3348
3349 const allEvents = [];
3350 const defaultPreventedEvents = [];
3351 const handler = e => {
3352 allEvents.push(e.type);
3353 if (e.defaultPrevented) defaultPreventedEvents.push(e.type);
3354 };
3355
3356 container.addEventListener('touchstart', handler);
3357 container.addEventListener('touchmove', handler);
3358 container.addEventListener('wheel', handler);
3359
3360 const ref = React.createRef();
3361 const setTouchStart =
3362 ReactDOM.unstable_createEventHandle('touchstart');
3363 const setTouchMove =
3364 ReactDOM.unstable_createEventHandle('touchmove');
3365 const setWheel = ReactDOM.unstable_createEventHandle('wheel');
3366
3367 function Component() {
3368 React.useEffect(() => {
3369 const clearTouchStart = setTouchStart(ref.current, e =>
3370 e.preventDefault(),
3371 );
3372 const clearTouchMove = setTouchMove(ref.current, e =>
3373 e.preventDefault(),
3374 );
3375 const clearWheel = setWheel(ref.current, e =>
3376 e.preventDefault(),
3377 );
3378 return () => {
3379 clearTouchStart();
3380 clearTouchMove();
3381 clearWheel();
3382 };
3383 });
3384 return <div ref={ref}>test</div>;
3385 }
3386
3387 const root = ReactDOMClient.createRoot(rootContainer);
3388 await act(() => {
3389 root.render(<Component />);
3390 });
3391
3392 dispatchEvent(ref.current, 'touchstart');
3393 dispatchEvent(ref.current, 'touchmove');
3394 dispatchEvent(ref.current, 'wheel');
3395
3396 expect(allEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
3397 // These events are passive by default, so we can't preventDefault.
3398 expect(defaultPreventedEvents).toEqual([]);
3399 });
3400 });
3401 });
3402 },
3403 );
3404 }
3405
3406 withEnableLegacyFBSupport(false);
3407 withEnableLegacyFBSupport(true);
3408 });