/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; import {createEventTarget} from 'dom-event-testing-library'; let React; let ReactFeatureFlags; let ReactDOM; let ReactDOMClient; let ReactDOMServer; let Scheduler; let act; let waitForAll; let waitFor; function dispatchEvent(element, type) { const event = document.createEvent('Event'); event.initEvent(type, true, true); element.dispatchEvent(event); } function dispatchClickEvent(element) { dispatchEvent(element, 'click'); } const eventListenersToClear = []; function startNativeEventListenerClearDown() { const nativeWindowEventListener = window.addEventListener; window.addEventListener = function (...params) { eventListenersToClear.push({target: window, params}); return nativeWindowEventListener.apply(this, params); }; const nativeDocumentEventListener = document.addEventListener; document.addEventListener = function (...params) { eventListenersToClear.push({target: document, params}); return nativeDocumentEventListener.apply(this, params); }; } function endNativeEventListenerClearDown() { eventListenersToClear.forEach(({target, params}) => { target.removeEventListener(...params); }); } describe('DOMPluginEventSystem', () => { let container; function withEnableLegacyFBSupport(enableLegacyFBSupport) { describe( 'enableLegacyFBSupport ' + (enableLegacyFBSupport ? 'enabled' : 'disabled'), () => { beforeAll(() => { // These tests are run twice, once with legacyFBSupport enabled and once disabled. // The document needs to be cleaned up a bit before the second pass otherwise it is // operating in a non pristine environment document.removeChild(document.documentElement); document.appendChild(document.createElement('html')); document.documentElement.appendChild(document.createElement('head')); document.documentElement.appendChild(document.createElement('body')); }); beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); ReactDOMServer = require('react-dom/server'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; act = InternalTestUtils.act; container = document.createElement('div'); document.body.appendChild(container); startNativeEventListenerClearDown(); }); afterEach(() => { document.body.removeChild(container); container = null; endNativeEventListenerClearDown(); }); it('does not pool events', async () => { const buttonRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(e)); function Test() { return ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of click events combined with sync clicks', async () => { const buttonRef = React.createRef(); let clicks = 0; function Test() { const inputRef = React.useRef(null); return (
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(clicks).toBe(1); }); it('handle propagation of click events between roots', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const childRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Child() { return (
Click me!
); } function Parent() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const childRoot = ReactDOMClient.createRoot(childRef.current); await act(() => { childRoot.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of click events between disjointed roots', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Child() { return (
Click me!
); } function Parent() { return ( ); } // We make a wrapper with an inner container that we // render to. So it looks like
// We then render to all three: // - container // - parentContainer // - childContainer const parentContainer = document.createElement('div'); const childContainer = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const parentRoot = ReactDOMClient.createRoot(parentContainer); await act(() => { parentRoot.render(); }); const childRoot = ReactDOMClient.createRoot(childContainer); await act(() => { childRoot.render(); }); parentContainer.appendChild(childContainer); spanRef.current.appendChild(parentContainer); // Inside const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); // Inside const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); // Inside const buttonElement2 = button2Ref.current; dispatchClickEvent(buttonElement2); expect(onClick).toHaveBeenCalledTimes(5); expect(onClickCapture).toHaveBeenCalledTimes(5); expect(log[6]).toEqual(['capture', buttonElement]); expect(log[7]).toEqual(['capture', buttonElement2]); expect(log[8]).toEqual(['bubble', buttonElement2]); expect(log[9]).toEqual(['bubble', buttonElement]); }); // @gate !disableCommentsAsDOMContainers it('handle propagation of click events between disjointed comment roots', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Child() { return (
Click me!
); } function Parent() { return ( ); } // We use a comment node here, then mount to it const disjointedNode = document.createComment( ' react-mount-point-unstable ', ); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); spanRef.current.appendChild(disjointedNode); const disjointedNodeRoot = ReactDOMClient.createRoot(disjointedNode); await act(() => { disjointedNodeRoot.render(); }); const buttonElement = buttonRef.current; await act(() => { dispatchClickEvent(buttonElement); }); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; await act(() => { dispatchClickEvent(divElement); }); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of click events between portals', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const portalElement = document.createElement('div'); document.body.appendChild(portalElement); function Child() { return (
Click me!
); } function Parent() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); document.body.removeChild(portalElement); }); it('handle click events on document.body portals', async () => { const log = []; function Child({label}) { return
log.push(label)}>{label}
; } function Parent() { return ( <> {ReactDOM.createPortal( , document.body, )} {ReactDOM.createPortal( , document.body, )} ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const second = document.body.lastChild; expect(second.textContent).toEqual('second'); dispatchClickEvent(second); expect(log).toEqual(['second']); const first = second.previousSibling; expect(first.textContent).toEqual('first'); dispatchClickEvent(first); expect(log).toEqual(['second', 'first']); }); it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; const promise = new Promise( resolvePromise => (resolve = resolvePromise), ); let clicks = 0; const childSlotRef = React.createRef(); function Parent() { return
clicks++} ref={childSlotRef} />; } function Child({text}) { if (suspend) { throw promise; } else { return Click me; } } function App() { // The root is a Suspense boundary. return ( ); } suspend = false; const finalHTML = ReactDOMServer.renderToString(); const parentContainer = document.createElement('div'); const childContainer = document.createElement('div'); // We need this to be in the document since we'll dispatch events on it. document.body.appendChild(parentContainer); // We're going to use a different root as a parent. // This lets us detect whether an event goes through React's event system. const parentRoot = ReactDOMClient.createRoot(parentContainer); await act(() => { parentRoot.render(); }); childSlotRef.current.appendChild(childContainer); childContainer.innerHTML = finalHTML; const a = childContainer.getElementsByTagName('a')[0]; suspend = true; // Hydrate asynchronously. await act(() => { ReactDOMClient.hydrateRoot(childContainer, ); }); // The Suspense boundary is not yet hydrated. await act(() => { a.click(); }); expect(clicks).toBe(0); // Resolving the promise so that rendering can complete. await act(async () => { suspend = false; resolve(); await promise; }); // We're now full hydrated. expect(clicks).toBe(0); document.body.removeChild(parentContainer); }); it('handle click events on dynamic portals', async () => { const log = []; function Parent() { const ref = React.useRef(null); const [portal, setPortal] = React.useState(null); React.useEffect(() => { setPortal( ReactDOM.createPortal( log.push('child')} id="child" />, ref.current, ), ); }, []); return (
log.push('parent')} id="parent"> {portal}
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const parent = container.lastChild; expect(parent.id).toEqual('parent'); await act(() => { dispatchClickEvent(parent); }); expect(log).toEqual(['parent']); const child = parent.lastChild; expect(child.id).toEqual('child'); await act(() => { dispatchClickEvent(child); }); // we add both 'child' and 'parent' due to bubbling expect(log).toEqual(['parent', 'child', 'parent']); }); // Slight alteration to the last test, to catch // a subtle difference in traversal. it('handle click events on dynamic portals #2', async () => { const log = []; function Parent() { const ref = React.useRef(null); const [portal, setPortal] = React.useState(null); React.useEffect(() => { setPortal( ReactDOM.createPortal( log.push('child')} id="child" />, ref.current, ), ); }, []); return (
log.push('parent')} id="parent">
{portal}
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const parent = container.lastChild; expect(parent.id).toEqual('parent'); await act(() => { dispatchClickEvent(parent); }); expect(log).toEqual(['parent']); const child = parent.lastChild; expect(child.id).toEqual('child'); await act(() => { dispatchClickEvent(child); }); // we add both 'child' and 'parent' due to bubbling expect(log).toEqual(['parent', 'child', 'parent']); }); it('native stopPropagation on click events between portals', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const middleDivRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const portalElement = document.createElement('div'); document.body.appendChild(portalElement); function Child() { return (
Click me!
); } function Parent() { React.useLayoutEffect(() => { // This should prevent the portalElement listeners from // capturing the events in the bubble phase. middleDivRef.current.addEventListener('click', e => { e.stopPropagation(); }); }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(3); document.body.removeChild(portalElement); }); it('handle propagation of focus events', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Test() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; buttonElement.focus(); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocusCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; divElement.focus(); expect(onFocus).toHaveBeenCalledTimes(3); expect(onFocusCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of focus events between roots', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const childRef = React.createRef(); const log = []; const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Child() { return (
Click me!
); } function Parent() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const childRoot = ReactDOMClient.createRoot(childRef.current); await act(() => { childRoot.render(); }); const buttonElement = buttonRef.current; buttonElement.focus(); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocusCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; divElement.focus(); expect(onFocus).toHaveBeenCalledTimes(3); expect(onFocusCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); it('handle propagation of focus events between portals', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const portalElement = document.createElement('div'); document.body.appendChild(portalElement); function Child() { return (
Click me!
); } function Parent() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; buttonElement.focus(); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocusCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; divElement.focus(); expect(onFocus).toHaveBeenCalledTimes(3); expect(onFocusCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); document.body.removeChild(portalElement); }); it('native stopPropagation on focus events between portals', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const middleDivRef = React.createRef(); const log = []; const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const portalElement = document.createElement('div'); document.body.appendChild(portalElement); function Child() { return (
Click me!
); } function Parent() { React.useLayoutEffect(() => { // This should prevent the portalElement listeners from // capturing the events in the bubble phase. middleDivRef.current.addEventListener('focusin', e => { e.stopPropagation(); }); }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; buttonElement.focus(); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocusCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; divElement.focus(); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocusCapture).toHaveBeenCalledTimes(3); document.body.removeChild(portalElement); }); it('handle propagation of enter and leave events between portals', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onMouseEnter = jest.fn(e => log.push(e.currentTarget)); const onMouseLeave = jest.fn(e => log.push(e.currentTarget)); const portalElement = document.createElement('div'); document.body.appendChild(portalElement); function Child() { return (
); } function Parent() { return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; buttonElement.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true, relatedTarget: null, }), ); expect(onMouseEnter).toHaveBeenCalledTimes(1); expect(onMouseLeave).toHaveBeenCalledTimes(0); expect(log[0]).toEqual(buttonElement); const divElement = divRef.current; buttonElement.dispatchEvent( new MouseEvent('mouseout', { bubbles: true, cancelable: true, relatedTarget: divElement, }), ); divElement.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true, relatedTarget: buttonElement, }), ); expect(onMouseEnter).toHaveBeenCalledTimes(2); expect(onMouseLeave).toHaveBeenCalledTimes(0); expect(log[1]).toEqual(divElement); document.body.removeChild(portalElement); }); it('handle propagation of enter and leave events between portals #2', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const portalRef = React.createRef(); const log = []; const onMouseEnter = jest.fn(e => log.push(e.currentTarget)); const onMouseLeave = jest.fn(e => log.push(e.currentTarget)); function Child() { return (
); } function Parent() { const [portal, setPortal] = React.useState(null); React.useLayoutEffect(() => { setPortal(ReactDOM.createPortal(, portalRef.current)); }, []); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; buttonElement.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true, relatedTarget: null, }), ); expect(onMouseEnter).toHaveBeenCalledTimes(1); expect(onMouseLeave).toHaveBeenCalledTimes(0); expect(log[0]).toEqual(buttonElement); const divElement = divRef.current; buttonElement.dispatchEvent( new MouseEvent('mouseout', { bubbles: true, cancelable: true, relatedTarget: divElement, }), ); divElement.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true, relatedTarget: buttonElement, }), ); expect(onMouseEnter).toHaveBeenCalledTimes(2); expect(onMouseLeave).toHaveBeenCalledTimes(0); expect(log[1]).toEqual(divElement); }); it('should preserve bubble/capture order between roots and nested portals', async () => { const targetRef = React.createRef(); let log = []; const onClickRoot = jest.fn(e => log.push('bubble root')); const onClickCaptureRoot = jest.fn(e => log.push('capture root')); const onClickPortal = jest.fn(e => log.push('bubble portal')); const onClickCapturePortal = jest.fn(e => log.push('capture portal')); function Portal() { return (
Click me!
); } const portalContainer = document.createElement('div'); let shouldStopPropagation = false; portalContainer.addEventListener( 'click', e => { if (shouldStopPropagation) { e.stopPropagation(); } }, false, ); function Root() { const portalTargetRef = React.useRef(null); React.useLayoutEffect(() => { portalTargetRef.current.appendChild(portalContainer); }); return (
{ReactDOM.createPortal(, portalContainer)}
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const divElement = targetRef.current; dispatchClickEvent(divElement); expect(log).toEqual([ 'capture root', 'capture portal', 'bubble portal', 'bubble root', ]); log = []; shouldStopPropagation = true; dispatchClickEvent(divElement); if (enableLegacyFBSupport) { // We aren't using roots with legacyFBSupport, we put clicks on the document, so we exbit the previous // behavior. expect(log).toEqual(['capture root', 'capture portal']); } else { expect(log).toEqual([ // The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do. 'capture root', 'capture portal', 'bubble portal', 'bubble root', ]); } }); describe('ReactDOM.createEventHandle', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport; ReactFeatureFlags.enableCreateEventHandleAPI = true; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); ReactDOMServer = require('react-dom/server'); act = require('internal-test-utils').act; const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; }); // @gate www it('can render correctly with the ReactDOMServer', () => { const clickEvent = jest.fn(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { const divRef = React.useRef(null); React.useEffect(() => { return setClick(divRef.current, clickEvent); }); return
Hello world
; } const output = ReactDOMServer.renderToString(); expect(output).toBe(`
Hello world
`); }); // @gate www it('can render correctly with the ReactDOMServer hydration', async () => { const clickEvent = jest.fn(); const spanRef = React.createRef(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { return setClick(spanRef.current, clickEvent); }); return (
Hello world
); } const output = ReactDOMServer.renderToString(); expect(output).toBe(`
Hello world
`); container.innerHTML = output; await act(() => { ReactDOMClient.hydrateRoot(container, ); }); dispatchClickEvent(spanRef.current); expect(clickEvent).toHaveBeenCalledTimes(1); }); // @gate www it('should correctly work for a basic "click" listener', async () => { let log = []; const clickEvent = jest.fn(event => { log.push({ eventPhase: event.eventPhase, type: event.type, currentTarget: event.currentTarget, target: event.target, }); }); const divRef = React.createRef(); const buttonRef = React.createRef(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { return setClick(buttonRef.current, clickEvent); }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe( '', ); // Clicking the button should trigger the event callback let divElement = divRef.current; dispatchClickEvent(divElement); expect(log).toEqual([ { eventPhase: 3, type: 'click', currentTarget: buttonRef.current, target: divRef.current, }, ]); expect(clickEvent).toBeCalledTimes(1); // Unmounting the container and clicking should not work await act(() => { root.render(null); }); dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(1); // Re-rendering the container and clicking should work await act(() => { root.render(); }); divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(2); log = []; // Clicking the button should also work const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(log).toEqual([ { eventPhase: 3, type: 'click', currentTarget: buttonRef.current, target: buttonRef.current, }, ]); const setClick2 = ReactDOM.unstable_createEventHandle('click'); function Test2({clickEvent2}) { React.useEffect(() => { return setClick2(buttonRef.current, clickEvent2); }); return ( ); } let clickEvent2 = jest.fn(); await act(() => { root.render(); }); divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent2).toBeCalledTimes(1); // Reset the function we pass in, so it's different clickEvent2 = jest.fn(); await act(() => { root.render(); }); divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent2).toBeCalledTimes(1); }); // @gate www it('should correctly work for setting and clearing a basic "click" listener', async () => { const clickEvent = jest.fn(); const divRef = React.createRef(); const buttonRef = React.createRef(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test({off}) { React.useEffect(() => { const clear = setClick(buttonRef.current, clickEvent); if (off) { clear(); } return clear; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); let divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(1); // The listener should get unmounted await act(() => { root.render(); }); clickEvent.mockClear(); divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(0); }); // @gate www it('should handle the target being a text node', async () => { const clickEvent = jest.fn(); const buttonRef = React.createRef(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { return setClick(buttonRef.current, clickEvent); }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const textNode = buttonRef.current.firstChild; dispatchClickEvent(textNode); expect(clickEvent).toBeCalledTimes(1); }); // @gate www it('handle propagation of click events', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const setClick = ReactDOM.unstable_createEventHandle('click'); const setCaptureClick = ReactDOM.unstable_createEventHandle( 'click', { capture: true, }, ); function Test() { React.useEffect(() => { const clearClick1 = setClick(buttonRef.current, onClick); const clearCaptureClick1 = setCaptureClick( buttonRef.current, onClickCapture, ); const clearClick2 = setClick(divRef.current, onClick); const clearCaptureClick2 = setCaptureClick( divRef.current, onClickCapture, ); return () => { clearClick1(); clearCaptureClick1(); clearClick2(); clearCaptureClick2(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); log.length = 0; onClick.mockClear(); onClickCapture.mockClear(); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(2); expect(onClickCapture).toHaveBeenCalledTimes(2); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['capture', divElement]); expect(log[2]).toEqual(['bubble', divElement]); expect(log[3]).toEqual(['bubble', buttonElement]); }); // @gate www it('handle propagation of click events mixed with onClick events', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const setClick = ReactDOM.unstable_createEventHandle('click'); const setClickCapture = ReactDOM.unstable_createEventHandle( 'click', { capture: true, }, ); function Test() { React.useEffect(() => { setClick(buttonRef.current, onClick); setClickCapture(buttonRef.current, onClickCapture); return () => { setClick(); setClickCapture(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); // @gate www it('should correctly work for a basic "click" listener on the outer target', async () => { const log = []; const clickEvent = jest.fn(event => { log.push({ eventPhase: event.eventPhase, type: event.type, currentTarget: event.currentTarget, target: event.target, }); }); const divRef = React.createRef(); const buttonRef = React.createRef(); const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { return setClick(divRef.current, clickEvent); }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe( '', ); // Clicking the button should trigger the event callback let divElement = divRef.current; dispatchClickEvent(divElement); expect(log).toEqual([ { eventPhase: 3, type: 'click', currentTarget: divRef.current, target: divRef.current, }, ]); // Unmounting the container and clicking should not work await act(() => { root.render(null); }); dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(1); // Re-rendering the container and clicking should work await act(() => { root.render(); }); divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent).toBeCalledTimes(2); // Clicking the button should not work const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(clickEvent).toBeCalledTimes(2); }); // @gate www it('should correctly handle many nested target listeners', async () => { const buttonRef = React.createRef(); const targetListener1 = jest.fn(); const targetListener2 = jest.fn(); const targetListener3 = jest.fn(); const targetListener4 = jest.fn(); let setClick1 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); let setClick2 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); let setClick3 = ReactDOM.unstable_createEventHandle('click'); let setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1( buttonRef.current, targetListener1, ); const clearClick2 = setClick2( buttonRef.current, targetListener2, ); const clearClick3 = setClick3( buttonRef.current, targetListener3, ); const clearClick4 = setClick4( buttonRef.current, targetListener4, ); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(targetListener1).toHaveBeenCalledTimes(1); expect(targetListener2).toHaveBeenCalledTimes(1); expect(targetListener3).toHaveBeenCalledTimes(1); expect(targetListener4).toHaveBeenCalledTimes(1); setClick1 = ReactDOM.unstable_createEventHandle('click'); setClick2 = ReactDOM.unstable_createEventHandle('click'); setClick3 = ReactDOM.unstable_createEventHandle('click'); setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test2() { React.useEffect(() => { const clearClick1 = setClick1( buttonRef.current, targetListener1, ); const clearClick2 = setClick2( buttonRef.current, targetListener2, ); const clearClick3 = setClick3( buttonRef.current, targetListener3, ); const clearClick4 = setClick4( buttonRef.current, targetListener4, ); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } await act(() => { root.render(); }); buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(targetListener1).toHaveBeenCalledTimes(2); expect(targetListener2).toHaveBeenCalledTimes(2); expect(targetListener3).toHaveBeenCalledTimes(2); expect(targetListener4).toHaveBeenCalledTimes(2); }); // @gate www it('should correctly handle stopPropagation correctly for target events', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const clickEvent = jest.fn(); const setClick1 = ReactDOM.unstable_createEventHandle('click', { bind: buttonRef, }); const setClick2 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1(buttonRef.current, clickEvent); const clearClick2 = setClick2(divRef.current, e => { e.stopPropagation(); }); return () => { clearClick1(); clearClick2(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const divElement = divRef.current; dispatchClickEvent(divElement); expect(clickEvent).toHaveBeenCalledTimes(0); }); // @gate www it('should correctly handle stopPropagation correctly for many target events', async () => { const buttonRef = React.createRef(); const targetListener1 = jest.fn(e => e.stopPropagation()); const targetListener2 = jest.fn(e => e.stopPropagation()); const targetListener3 = jest.fn(e => e.stopPropagation()); const targetListener4 = jest.fn(e => e.stopPropagation()); const setClick1 = ReactDOM.unstable_createEventHandle('click'); const setClick2 = ReactDOM.unstable_createEventHandle('click'); const setClick3 = ReactDOM.unstable_createEventHandle('click'); const setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1( buttonRef.current, targetListener1, ); const clearClick2 = setClick2( buttonRef.current, targetListener2, ); const clearClick3 = setClick3( buttonRef.current, targetListener3, ); const clearClick4 = setClick4( buttonRef.current, targetListener4, ); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(targetListener1).toHaveBeenCalledTimes(1); expect(targetListener2).toHaveBeenCalledTimes(1); expect(targetListener3).toHaveBeenCalledTimes(1); expect(targetListener4).toHaveBeenCalledTimes(1); }); // @gate www it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', async () => { const buttonRef = React.createRef(); const targetListener1 = jest.fn(e => e.stopPropagation()); const targetListener2 = jest.fn(e => e.stopPropagation()); const targetListener3 = jest.fn(e => e.stopPropagation()); const targetListener4 = jest.fn(e => e.stopPropagation()); const setClick1 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick2 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick3 = ReactDOM.unstable_createEventHandle('click'); const setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1( buttonRef.current, targetListener1, ); const clearClick2 = setClick2( buttonRef.current, targetListener2, ); const clearClick3 = setClick3( buttonRef.current, targetListener3, ); const clearClick4 = setClick4( buttonRef.current, targetListener4, ); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(targetListener1).toHaveBeenCalledTimes(1); expect(targetListener2).toHaveBeenCalledTimes(1); expect(targetListener3).toHaveBeenCalledTimes(0); expect(targetListener4).toHaveBeenCalledTimes(0); }); // @gate www it('should work with concurrent mode updates', async () => { const log = []; const ref = React.createRef(); const setClick1 = ReactDOM.unstable_createEventHandle('click'); function Test({counter}) { React.useLayoutEffect(() => { return setClick1(ref.current, () => { log.push({counter}); }); }); Scheduler.log('Test'); return ; } const root = ReactDOMClient.createRoot(container); root.render(); await waitForAll(['Test']); // Click the button dispatchClickEvent(ref.current); expect(log).toEqual([{counter: 0}]); // Clear log log.length = 0; // Increase counter React.startTransition(() => { root.render(); }); // Yield before committing await waitFor(['Test']); // Click the button again dispatchClickEvent(ref.current); expect(log).toEqual([{counter: 0}]); // Clear log log.length = 0; // Commit await waitForAll([]); dispatchClickEvent(ref.current); expect(log).toEqual([{counter: 1}]); }); // @gate www it('should correctly work for a basic "click" window listener', async () => { const log = []; const clickEvent = jest.fn(event => { log.push({ eventPhase: event.eventPhase, type: event.type, currentTarget: event.currentTarget, target: event.target, }); }); const setClick1 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { return setClick1(window, clickEvent); }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe( '', ); // Clicking outside the button should trigger the event callback dispatchClickEvent(document.body); expect(log[0]).toEqual({ eventPhase: 3, type: 'click', currentTarget: window, target: document.body, }); // Unmounting the container and clicking should not work await act(() => { root.render(null); }); dispatchClickEvent(document.body); expect(clickEvent).toBeCalledTimes(1); // Re-rendering and clicking the body should work again await act(() => { root.render(); }); dispatchClickEvent(document.body); expect(clickEvent).toBeCalledTimes(2); }); // @gate www it('handle propagation of click events on the window', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const setClick = ReactDOM.unstable_createEventHandle('click'); const setClickCapture = ReactDOM.unstable_createEventHandle( 'click', { capture: true, }, ); function Test() { React.useEffect(() => { const clearClick1 = setClick(window, onClick); const clearClickCapture1 = setClickCapture( window, onClickCapture, ); const clearClick2 = setClick(buttonRef.current, onClick); const clearClickCapture2 = setClickCapture( buttonRef.current, onClickCapture, ); const clearClick3 = setClick(divRef.current, onClick); const clearClickCapture3 = setClickCapture( divRef.current, onClickCapture, ); return () => { clearClick1(); clearClickCapture1(); clearClick2(); clearClickCapture2(); clearClick3(); clearClickCapture3(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(2); expect(onClickCapture).toHaveBeenCalledTimes(2); expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', buttonElement]); expect(log[2]).toEqual(['bubble', buttonElement]); expect(log[3]).toEqual(['bubble', window]); log.length = 0; onClick.mockClear(); onClickCapture.mockClear(); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', buttonElement]); expect(log[2]).toEqual(['capture', divElement]); expect(log[3]).toEqual(['bubble', divElement]); expect(log[4]).toEqual(['bubble', buttonElement]); expect(log[5]).toEqual(['bubble', window]); }); // @gate www it('should correctly handle stopPropagation for mixed listeners', async () => { const buttonRef = React.createRef(); const rootListener1 = jest.fn(e => e.stopPropagation()); const rootListener2 = jest.fn(); const targetListener1 = jest.fn(); const targetListener2 = jest.fn(); const setClick1 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick2 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick3 = ReactDOM.unstable_createEventHandle('click'); const setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1(window, rootListener1); const clearClick2 = setClick2( buttonRef.current, targetListener1, ); const clearClick3 = setClick3(window, rootListener2); const clearClick4 = setClick4( buttonRef.current, targetListener2, ); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(rootListener1).toHaveBeenCalledTimes(1); expect(targetListener1).toHaveBeenCalledTimes(0); expect(targetListener2).toHaveBeenCalledTimes(0); expect(rootListener2).toHaveBeenCalledTimes(0); }); // @gate www it('should correctly handle stopPropagation for delegated listeners', async () => { const buttonRef = React.createRef(); const rootListener1 = jest.fn(e => e.stopPropagation()); const rootListener2 = jest.fn(); const rootListener3 = jest.fn(e => e.stopPropagation()); const rootListener4 = jest.fn(); const setClick1 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick2 = ReactDOM.unstable_createEventHandle('click', { capture: true, }); const setClick3 = ReactDOM.unstable_createEventHandle('click'); const setClick4 = ReactDOM.unstable_createEventHandle('click'); function Test() { React.useEffect(() => { const clearClick1 = setClick1(window, rootListener1); const clearClick2 = setClick2(window, rootListener2); const clearClick3 = setClick3(window, rootListener3); const clearClick4 = setClick4(window, rootListener4); return () => { clearClick1(); clearClick2(); clearClick3(); clearClick4(); }; }); return ; } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(rootListener1).toHaveBeenCalledTimes(1); expect(rootListener2).toHaveBeenCalledTimes(1); expect(rootListener3).toHaveBeenCalledTimes(0); expect(rootListener4).toHaveBeenCalledTimes(0); }); // @gate www it('handle propagation of click events on the window and document', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const setClick = ReactDOM.unstable_createEventHandle('click'); const setClickCapture = ReactDOM.unstable_createEventHandle( 'click', { capture: true, }, ); function Test() { React.useEffect(() => { const clearClick1 = setClick(window, onClick); const clearClickCapture1 = setClickCapture( window, onClickCapture, ); const clearClick2 = setClick(document, onClick); const clearClickCapture2 = setClickCapture( document, onClickCapture, ); const clearClick3 = setClick(buttonRef.current, onClick); const clearClickCapture3 = setClickCapture( buttonRef.current, onClickCapture, ); const clearClick4 = setClick(divRef.current, onClick); const clearClickCapture4 = setClickCapture( divRef.current, onClickCapture, ); return () => { clearClick1(); clearClickCapture1(); clearClick2(); clearClickCapture2(); clearClick3(); clearClickCapture3(); clearClick4(); clearClickCapture4(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); if (enableLegacyFBSupport) { expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', document]); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['bubble', document]); expect(log[4]).toEqual(['bubble', buttonElement]); expect(log[5]).toEqual(['bubble', window]); } else { expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', document]); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['bubble', buttonElement]); expect(log[4]).toEqual(['bubble', document]); expect(log[5]).toEqual(['bubble', window]); } log.length = 0; onClick.mockClear(); onClickCapture.mockClear(); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(4); expect(onClickCapture).toHaveBeenCalledTimes(4); if (enableLegacyFBSupport) { expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', document]); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', document]); expect(log[5]).toEqual(['bubble', divElement]); expect(log[6]).toEqual(['bubble', buttonElement]); expect(log[7]).toEqual(['bubble', window]); } else { expect(log[0]).toEqual(['capture', window]); expect(log[1]).toEqual(['capture', document]); expect(log[2]).toEqual(['capture', buttonElement]); expect(log[3]).toEqual(['capture', divElement]); expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); expect(log[6]).toEqual(['bubble', document]); expect(log[7]).toEqual(['bubble', window]); } }); // @gate www it('does not support custom user events', () => { // With eager listeners, supporting custom events via this API doesn't make sense // because we can't know a full list of them ahead of time. Let's check we throw // since otherwise we'd end up with inconsistent behavior, like no portal bubbling. expect(() => { ReactDOM.unstable_createEventHandle('custom-event'); }).toThrow( 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.', ); }); // @gate www it('beforeblur and afterblur are called after a focused element is unmounted', async () => { const log = []; // We have to persist here because we want to read relatedTarget later. const onAfterBlur = jest.fn(e => { e.persist(); log.push(e.type); }); const onBeforeBlur = jest.fn(e => log.push(e.type)); const innerRef = React.createRef(); const innerRef2 = React.createRef(); const setAfterBlurHandle = ReactDOM.unstable_createEventHandle('afterblur'); const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const Component = ({show}) => { const ref = React.useRef(null); React.useEffect(() => { const clear1 = setAfterBlurHandle(document, onAfterBlur); const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); return () => { clear1(); clear2(); }; }); return (
{show && }
); }; const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); expect(onBeforeBlur).toHaveBeenCalledTimes(0); expect(onAfterBlur).toHaveBeenCalledTimes(0); await act(() => { root.render(); }); expect(onBeforeBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledWith( expect.objectContaining({relatedTarget: inner}), ); expect(log).toEqual(['beforeblur', 'afterblur']); }); // @gate www it('beforeblur and afterblur are called after a nested focused element is unmounted', async () => { const log = []; // We have to persist here because we want to read relatedTarget later. const onAfterBlur = jest.fn(e => { e.persist(); log.push(e.type); }); const onBeforeBlur = jest.fn(e => log.push(e.type)); const innerRef = React.createRef(); const innerRef2 = React.createRef(); const setAfterBlurHandle = ReactDOM.unstable_createEventHandle('afterblur'); const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const Component = ({show}) => { const ref = React.useRef(null); React.useEffect(() => { const clear1 = setAfterBlurHandle(document, onAfterBlur); const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); return () => { clear1(); clear2(); }; }); return (
{show && (
)}
); }; const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); expect(onBeforeBlur).toHaveBeenCalledTimes(0); expect(onAfterBlur).toHaveBeenCalledTimes(0); await act(() => { root.render(); }); expect(onBeforeBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledWith( expect.objectContaining({relatedTarget: inner}), ); expect(log).toEqual(['beforeblur', 'afterblur']); }); // @gate www it('beforeblur should skip handlers from a deleted subtree after the focused element is unmounted', async () => { const onBeforeBlur = jest.fn(); const innerRef = React.createRef(); const innerRef2 = React.createRef(); const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const ref2 = React.createRef(); const Component = ({show}) => { const ref = React.useRef(null); React.useEffect(() => { const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur); let clear2; if (ref2.current) { clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur); } return () => { clear1(); if (clear2) { clear2(); } }; }); return (
{show && (
)}
); }; const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); expect(onBeforeBlur).toHaveBeenCalledTimes(0); await act(() => { root.render(); }); expect(onBeforeBlur).toHaveBeenCalledTimes(1); }); // @gate www it('beforeblur and afterblur are called after a focused element is suspended', async () => { const log = []; // We have to persist here because we want to read relatedTarget later. const onAfterBlur = jest.fn(e => { e.persist(); log.push(e.type); }); const onBeforeBlur = jest.fn(e => log.push(e.type)); const innerRef = React.createRef(); const Suspense = React.Suspense; let suspend = false; let resolve; const promise = new Promise( resolvePromise => (resolve = resolvePromise), ); const setAfterBlurHandle = ReactDOM.unstable_createEventHandle('afterblur'); const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); function Child() { if (suspend) { throw promise; } else { return ; } } const Component = () => { const ref = React.useRef(null); React.useEffect(() => { const clear1 = setAfterBlurHandle(document, onAfterBlur); const clear2 = setBeforeBlurHandle(ref.current, onBeforeBlur); return () => { clear1(); clear2(); }; }); return (
); }; const container2 = document.createElement('div'); document.body.appendChild(container2); const root = ReactDOMClient.createRoot(container2); await act(() => { root.render(); }); jest.runAllTimers(); const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); expect(onBeforeBlur).toHaveBeenCalledTimes(0); expect(onAfterBlur).toHaveBeenCalledTimes(0); suspend = true; await act(() => { root.render(); }); jest.runAllTimers(); expect(onBeforeBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledTimes(1); expect(onAfterBlur).toHaveBeenCalledWith( expect.objectContaining({relatedTarget: inner}), ); resolve(); expect(log).toEqual(['beforeblur', 'afterblur']); document.body.removeChild(container2); }); // @gate www it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', async () => { const onBeforeBlur = jest.fn(); const innerRef = React.createRef(); const innerRef2 = React.createRef(); const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const ref2 = React.createRef(); const Suspense = React.Suspense; let suspend = false; let resolve; const promise = new Promise( resolvePromise => (resolve = resolvePromise), ); function Child() { if (suspend) { throw promise; } else { return ; } } const Component = () => { const ref = React.useRef(null); React.useEffect(() => { const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur); let clear2; if (ref2.current) { clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur); } return () => { clear1(); if (clear2) { clear2(); } }; }); return (
); }; const container2 = document.createElement('div'); document.body.appendChild(container2); const root = ReactDOMClient.createRoot(container2); await act(() => { root.render(); }); jest.runAllTimers(); const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); expect(onBeforeBlur).toHaveBeenCalledTimes(0); suspend = true; await act(() => { root.render(); }); jest.runAllTimers(); expect(onBeforeBlur).toHaveBeenCalledTimes(1); resolve(); document.body.removeChild(container2); }); // @gate www it('regression: does not fire beforeblur/afterblur if target is already hidden', async () => { const Suspense = React.Suspense; let suspend = false; const fakePromise = {then() {}}; const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle('beforeblur'); const innerRef = React.createRef(); function Child() { if (suspend) { throw fakePromise; } return ; } const Component = () => { const ref = React.useRef(null); const [, setState] = React.useState(0); React.useEffect(() => { return setBeforeBlurHandle(ref.current, () => { // In the regression case, this would trigger an update, then // the resulting render would trigger another blur event, // which would trigger an update again, and on and on in an // infinite loop. setState(n => n + 1); }); }, []); return (
); }; const container2 = document.createElement('div'); document.body.appendChild(container2); const root = ReactDOMClient.createRoot(container2); await act(() => { root.render(); }); // Focus the input node const inner = innerRef.current; const target = createEventTarget(inner); target.focus(); // Suspend. This hides the input node, causing it to lose focus. suspend = true; await act(() => { root.render(); }); document.body.removeChild(container2); }); // @gate !disableCommentsAsDOMContainers it('handle propagation of click events between disjointed comment roots', async () => { const buttonRef = React.createRef(); const divRef = React.createRef(); const log = []; const setClick = ReactDOM.unstable_createEventHandle('click'); const setClickCapture = ReactDOM.unstable_createEventHandle( 'click', {capture: true}, ); const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); function Child() { React.useEffect(() => { const click1 = setClick(divRef.current, onClick); const click2 = setClickCapture(divRef.current, onClickCapture); return () => { click1(); click2(); }; }); return
Click me!
; } function Parent() { React.useEffect(() => { const click1 = setClick(buttonRef.current, onClick); const click2 = setClickCapture( buttonRef.current, onClickCapture, ); return () => { click1(); click2(); }; }); return ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const divElement = divRef.current; const buttonElement = buttonRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(1); expect(onClickCapture).toHaveBeenCalledTimes(1); expect(log[0]).toEqual(['capture', buttonElement]); expect(log[1]).toEqual(['bubble', buttonElement]); document.body.removeChild(portalElement); }); describe('Compatibility with Scopes API', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableCreateEventHandleAPI = true; ReactFeatureFlags.enableScopeAPI = true; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); ReactDOMServer = require('react-dom/server'); act = require('internal-test-utils').act; }); // @gate www it('handle propagation of click events on a scope', async () => { const buttonRef = React.createRef(); const log = []; const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]), ); const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]), ); const TestScope = React.unstable_Scope; const setClick = ReactDOM.unstable_createEventHandle('click'); const setClickCapture = ReactDOM.unstable_createEventHandle( 'click', { capture: true, }, ); function Test() { const scopeRef = React.useRef(null); React.useEffect(() => { const clear1 = setClick(scopeRef.current, onClick); const clear2 = setClickCapture( scopeRef.current, onClickCapture, ); return () => { clear1(); clear2(); }; }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(onClick).toHaveBeenCalledTimes(2); expect(onClickCapture).toHaveBeenCalledTimes(2); expect(log).toEqual([ ['capture', buttonElement], ['capture', buttonElement], ['bubble', buttonElement], ['bubble', buttonElement], ]); log.length = 0; onClick.mockClear(); onClickCapture.mockClear(); const divElement = divRef.current; dispatchClickEvent(divElement); expect(onClick).toHaveBeenCalledTimes(3); expect(onClickCapture).toHaveBeenCalledTimes(3); expect(log).toEqual([ ['capture', buttonElement], ['capture', buttonElement], ['capture', divElement], ['bubble', divElement], ['bubble', buttonElement], ['bubble', buttonElement], ]); }); // @gate www it('should not handle the target being a dangling text node within a scope', async () => { const clickEvent = jest.fn(); const buttonRef = React.createRef(); const TestScope = React.unstable_Scope; const setClick = ReactDOM.unstable_createEventHandle('click'); function Test() { const scopeRef = React.useRef(null); React.useEffect(() => { return setClick(scopeRef.current, clickEvent); }); return ( ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const textNode = buttonRef.current.firstChild; dispatchClickEvent(textNode); // This should not work, as the target instance will be the //