diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/EventDispatchCase.js b/fixtures/dom/src/components/fixtures/fragment-refs/EventDispatchCase.js new file mode 100644 index 0000000000000..ef5a959021332 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/fragment-refs/EventDispatchCase.js @@ -0,0 +1,157 @@ +import TestCase from '../../TestCase'; +import Fixture from '../../Fixture'; + +const React = window.React; +const {Fragment, useRef, useState} = React; + +function WrapperComponent(props) { + return props.children; +} + +const initialState = { + child: false, + parent: false, + grandparent: false, +}; + +export default function EventListenerCase() { + const fragmentRef = useRef(null); + const [clickedState, setClickedState] = useState({...initialState}); + const [fragmentEventFired, setFragmentEventFired] = useState(false); + const [bubblesState, setBubblesState] = useState(true); + + function setClick(id) { + setClickedState(prev => ({...prev, [id]: true})); + } + + function fragmentClickHandler(e) { + setFragmentEventFired(true); + } + + return ( + + +
  • + Each box has regular click handlers, you can click each one to observe + the status changing through standard bubbling. +
  • +
  • Clear the clicked state
  • +
  • + Click the "Dispatch click event" button to dispatch a click event on + the Fragment. The event will be dispatched on the Fragment's parent, + so the child will not change state. +
  • +
  • + Click the "Add event listener" button to add a click event listener on + the Fragment. This registers a handler that will turn the child blue + on click. +
  • +
  • + Now click the "Dispatch click event" button again. You can see that it + will fire the Fragment's event handler in addition to bubbling the + click from the parent. +
  • +
  • + If you turn off bubbling, only the Fragment's event handler will be + called. +
  • +
    + + +

    + Dispatching an event on a Fragment will forward the dispatch to its + parent for the standard case. You can observe when dispatching that + the parent handler is called in additional to bubbling from there. A + delay is added to make the bubbling more clear.{' '} +

    +

    + When there have been event handlers added to the Fragment, the + Fragment's event handler will be called in addition to bubbling from + the parent. Without bubbling, only the Fragment's event handler will + be called. +

    +
    + + + + + + + + + +
    { + setTimeout(() => { + setClick('grandparent'); + }, 200); + }} + className="card"> + Fragment grandparent - clicked:{' '} + {clickedState.grandparent ? 'true' : 'false'} +
    { + setTimeout(() => { + setClick('parent'); + }, 100); + }} + className="card"> + Fragment parent - clicked: {clickedState.parent ? 'true' : 'false'} + +
    { + setClick('child'); + }}> + Fragment child - clicked:{' '} + {clickedState.child ? 'true' : 'false'} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/index.js b/fixtures/dom/src/components/fixtures/fragment-refs/index.js index b84f273177d3a..23b440938cf7a 100644 --- a/fixtures/dom/src/components/fixtures/fragment-refs/index.js +++ b/fixtures/dom/src/components/fixtures/fragment-refs/index.js @@ -1,5 +1,6 @@ import FixtureSet from '../../FixtureSet'; import EventListenerCase from './EventListenerCase'; +import EventDispatchCase from './EventDispatchCase'; import IntersectionObserverCase from './IntersectionObserverCase'; import ResizeObserverCase from './ResizeObserverCase'; import FocusCase from './FocusCase'; @@ -11,6 +12,7 @@ export default function FragmentRefsPage() { return ( + diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 8584b644eff9d..84eeacc9cc7ef 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -2597,6 +2597,7 @@ export type FragmentInstanceType = { listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): void, + dispatchEvent(event: Event): boolean, focus(focusOptions?: FocusOptions): void, focusLast(focusOptions?: FocusOptions): void, blur(): void, @@ -2694,6 +2695,43 @@ function removeEventListenerFromChild( return false; } // $FlowFixMe[prop-missing] +FragmentInstance.prototype.dispatchEvent = function ( + this: FragmentInstanceType, + event: Event, +): boolean { + const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); + if (parentHostFiber === null) { + return true; + } + const parentHostInstance = + getInstanceFromHostFiber(parentHostFiber); + const eventListeners = this._eventListeners; + if ( + (eventListeners !== null && eventListeners.length > 0) || + !event.bubbles + ) { + const temp = document.createTextNode(''); + if (eventListeners) { + for (let i = 0; i < eventListeners.length; i++) { + const {type, listener, optionsOrUseCapture} = eventListeners[i]; + temp.addEventListener(type, listener, optionsOrUseCapture); + } + } + parentHostInstance.appendChild(temp); + const cancelable = temp.dispatchEvent(event); + if (eventListeners) { + for (let i = 0; i < eventListeners.length; i++) { + const {type, listener, optionsOrUseCapture} = eventListeners[i]; + temp.removeEventListener(type, listener, optionsOrUseCapture); + } + } + parentHostInstance.removeChild(temp); + return cancelable; + } else { + return parentHostInstance.dispatchEvent(event); + } +}; +// $FlowFixMe[prop-missing] FragmentInstance.prototype.focus = function ( this: FragmentInstanceType, focusOptions?: FocusOptions, diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js index 50447e1eac677..fb802ed2d4132 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -299,410 +299,346 @@ describe('FragmentRefs', () => { }); }); - describe('event listeners', () => { - // @gate enableFragmentRefs - it('adds and removes event listeners from children', async () => { - const parentRef = React.createRef(); - const fragmentRef = React.createRef(); - const childARef = React.createRef(); - const childBRef = React.createRef(); - const root = ReactDOMClient.createRoot(container); - - let logs = []; + describe('events', () => { + describe('add/remove event listeners', () => { + // @gate enableFragmentRefs + it('adds and removes event listeners from children', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); - function handleFragmentRefClicks() { - logs.push('fragmentRef'); - } + let logs = []; - function Test() { - React.useEffect(() => { - fragmentRef.current.addEventListener( - 'click', - handleFragmentRefClicks, - ); + function handleFragmentRefClicks() { + logs.push('fragmentRef'); + } - return () => { - fragmentRef.current.removeEventListener( + function Test() { + React.useEffect(() => { + fragmentRef.current.addEventListener( 'click', handleFragmentRefClicks, ); - }; - }, []); - return ( -
    - - <>Text -
    A
    - <> -
    B
    - -
    -
    - ); - } - await act(() => { - root.render(); - }); + return () => { + fragmentRef.current.removeEventListener( + 'click', + handleFragmentRefClicks, + ); + }; + }, []); + return ( +
    + + <>Text +
    A
    + <> +
    B
    + +
    +
    + ); + } - childARef.current.addEventListener('click', () => { - logs.push('A'); - }); + await act(() => { + root.render(); + }); - childBRef.current.addEventListener('click', () => { - logs.push('B'); - }); + childARef.current.addEventListener('click', () => { + logs.push('A'); + }); - // Clicking on the parent should not trigger any listeners - parentRef.current.click(); - expect(logs).toEqual([]); + childBRef.current.addEventListener('click', () => { + logs.push('B'); + }); - // Clicking a child triggers its own listeners and the Fragment's - childARef.current.click(); - expect(logs).toEqual(['fragmentRef', 'A']); + // Clicking on the parent should not trigger any listeners + parentRef.current.click(); + expect(logs).toEqual([]); - logs = []; + // Clicking a child triggers its own listeners and the Fragment's + childARef.current.click(); + expect(logs).toEqual(['fragmentRef', 'A']); - childBRef.current.click(); - expect(logs).toEqual(['fragmentRef', 'B']); + logs = []; - logs = []; + childBRef.current.click(); + expect(logs).toEqual(['fragmentRef', 'B']); - fragmentRef.current.removeEventListener('click', handleFragmentRefClicks); + logs = []; - childARef.current.click(); - expect(logs).toEqual(['A']); + fragmentRef.current.removeEventListener( + 'click', + handleFragmentRefClicks, + ); - logs = []; + childARef.current.click(); + expect(logs).toEqual(['A']); - childBRef.current.click(); - expect(logs).toEqual(['B']); - }); + logs = []; - // @gate enableFragmentRefs - it('adds and removes event listeners from children with multiple fragments', async () => { - const fragmentRef = React.createRef(); - const nestedFragmentRef = React.createRef(); - const nestedFragmentRef2 = React.createRef(); - const childARef = React.createRef(); - const childBRef = React.createRef(); - const childCRef = React.createRef(); - const root = ReactDOMClient.createRoot(container); + childBRef.current.click(); + expect(logs).toEqual(['B']); + }); - await act(() => { - root.render( -
    - -
    A
    -
    - -
    B
    + // @gate enableFragmentRefs + it('adds and removes event listeners from children with multiple fragments', async () => { + const fragmentRef = React.createRef(); + const nestedFragmentRef = React.createRef(); + const nestedFragmentRef2 = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const childCRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( +
    + +
    A
    +
    + +
    B
    +
    +
    + +
    C
    -
    - -
    C
    -
    -
    , - ); - }); +
    , + ); + }); - let logs = []; + let logs = []; - function handleFragmentRefClicks() { - logs.push('fragmentRef'); - } + function handleFragmentRefClicks() { + logs.push('fragmentRef'); + } - function handleNestedFragmentRefClicks() { - logs.push('nestedFragmentRef'); - } + function handleNestedFragmentRefClicks() { + logs.push('nestedFragmentRef'); + } - function handleNestedFragmentRef2Clicks() { - logs.push('nestedFragmentRef2'); - } + function handleNestedFragmentRef2Clicks() { + logs.push('nestedFragmentRef2'); + } - fragmentRef.current.addEventListener('click', handleFragmentRefClicks); - nestedFragmentRef.current.addEventListener( - 'click', - handleNestedFragmentRefClicks, - ); - nestedFragmentRef2.current.addEventListener( - 'click', - handleNestedFragmentRef2Clicks, - ); + fragmentRef.current.addEventListener('click', handleFragmentRefClicks); + nestedFragmentRef.current.addEventListener( + 'click', + handleNestedFragmentRefClicks, + ); + nestedFragmentRef2.current.addEventListener( + 'click', + handleNestedFragmentRef2Clicks, + ); - childBRef.current.click(); - // Event bubbles to the parent fragment - expect(logs).toEqual(['nestedFragmentRef', 'fragmentRef']); + childBRef.current.click(); + // Event bubbles to the parent fragment + expect(logs).toEqual(['nestedFragmentRef', 'fragmentRef']); - logs = []; + logs = []; - childARef.current.click(); - expect(logs).toEqual(['fragmentRef']); + childARef.current.click(); + expect(logs).toEqual(['fragmentRef']); - logs = []; - childCRef.current.click(); - expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']); + logs = []; + childCRef.current.click(); + expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']); - logs = []; + logs = []; - fragmentRef.current.removeEventListener('click', handleFragmentRefClicks); - nestedFragmentRef.current.removeEventListener( - 'click', - handleNestedFragmentRefClicks, - ); - childCRef.current.click(); - expect(logs).toEqual(['nestedFragmentRef2']); - }); + fragmentRef.current.removeEventListener( + 'click', + handleFragmentRefClicks, + ); + nestedFragmentRef.current.removeEventListener( + 'click', + handleNestedFragmentRefClicks, + ); + childCRef.current.click(); + expect(logs).toEqual(['nestedFragmentRef2']); + }); - // @gate enableFragmentRefs - it('adds an event listener to a newly added child', async () => { - const fragmentRef = React.createRef(); - const childRef = React.createRef(); - const root = ReactDOMClient.createRoot(container); - let showChild; + // @gate enableFragmentRefs + it('adds an event listener to a newly added child', async () => { + const fragmentRef = React.createRef(); + const childRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + let showChild; - function Component() { - const [shouldShowChild, setShouldShowChild] = React.useState(false); - showChild = () => { - setShouldShowChild(true); - }; + function Component() { + const [shouldShowChild, setShouldShowChild] = React.useState(false); + showChild = () => { + setShouldShowChild(true); + }; - return ( -
    - -
    A
    - {shouldShowChild && ( -
    - B -
    - )} -
    -
    - ); - } + return ( +
    + +
    A
    + {shouldShowChild && ( +
    + B +
    + )} +
    +
    + ); + } - await act(() => { - root.render(); - }); + await act(() => { + root.render(); + }); - expect(fragmentRef.current).not.toBe(null); - expect(childRef.current).toBe(null); + expect(fragmentRef.current).not.toBe(null); + expect(childRef.current).toBe(null); - let hasClicked = false; - fragmentRef.current.addEventListener('click', () => { - hasClicked = true; - }); + let hasClicked = false; + fragmentRef.current.addEventListener('click', () => { + hasClicked = true; + }); - await act(() => { - showChild(); - }); - expect(childRef.current).not.toBe(null); + await act(() => { + showChild(); + }); + expect(childRef.current).not.toBe(null); - childRef.current.click(); - expect(hasClicked).toBe(true); - }); + childRef.current.click(); + expect(hasClicked).toBe(true); + }); - // @gate enableFragmentRefs - it('applies event listeners to host children nested within non-host children', async () => { - const fragmentRef = React.createRef(); - const childRef = React.createRef(); - const nestedChildRef = React.createRef(); - const root = ReactDOMClient.createRoot(container); + // @gate enableFragmentRefs + it('applies event listeners to host children nested within non-host children', async () => { + const fragmentRef = React.createRef(); + const childRef = React.createRef(); + const nestedChildRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( -
    - -
    Host A
    - + await act(() => { + root.render( +
    + +
    Host A
    -
    Host B
    + +
    Host B
    +
    - -
    -
    , - ); - }); - const logs = []; - fragmentRef.current.addEventListener('click', e => { - logs.push(e.target.textContent); - }); - - expect(logs).toEqual([]); - childRef.current.click(); - expect(logs).toEqual(['Host A']); - nestedChildRef.current.click(); - expect(logs).toEqual(['Host A', 'Host B']); - }); - - // @gate enableFragmentRefs - it('allows adding and cleaning up listeners in effects', async () => { - const root = ReactDOMClient.createRoot(container); - - let logs = []; - function logClick(e) { - logs.push(e.currentTarget.id); - } - - let rerender; - let removeEventListeners; - - function Test() { - const fragmentRef = React.useRef(null); - // eslint-disable-next-line no-unused-vars - const [_, setState] = React.useState(0); - rerender = () => { - setState(p => p + 1); - }; - removeEventListeners = () => { - fragmentRef.current.removeEventListener('click', logClick); - }; - React.useEffect(() => { - fragmentRef.current.addEventListener('click', logClick); - - return removeEventListeners; +
    +
    , + ); + }); + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); }); - return ( - -
    - - ); - } - - // The event listener was applied - await act(() => root.render()); - expect(logs).toEqual([]); - document.querySelector('#child-a').click(); - expect(logs).toEqual(['child-a']); + expect(logs).toEqual([]); + childRef.current.click(); + expect(logs).toEqual(['Host A']); + nestedChildRef.current.click(); + expect(logs).toEqual(['Host A', 'Host B']); + }); - // The event listener can be removed and re-added - logs = []; - await act(rerender); - document.querySelector('#child-a').click(); - expect(logs).toEqual(['child-a']); - }); + // @gate enableFragmentRefs + it('allows adding and cleaning up listeners in effects', async () => { + const root = ReactDOMClient.createRoot(container); - // @gate enableFragmentRefs - it('does not apply removed event listeners to new children', async () => { - const root = ReactDOMClient.createRoot(container); - const fragmentRef = React.createRef(null); - function Test() { - return ( - -
    - - ); - } + let logs = []; + function logClick(e) { + logs.push(e.currentTarget.id); + } - let logs = []; - function logClick(e) { - logs.push(e.currentTarget.id); - } - await act(() => { - root.render(); - }); - fragmentRef.current.addEventListener('click', logClick); - const childA = document.querySelector('#child-a'); - childA.click(); - expect(logs).toEqual(['child-a']); + let rerender; + let removeEventListeners; - logs = []; - fragmentRef.current.removeEventListener('click', logClick); - childA.click(); - expect(logs).toEqual([]); - }); + function Test() { + const fragmentRef = React.useRef(null); + // eslint-disable-next-line no-unused-vars + const [_, setState] = React.useState(0); + rerender = () => { + setState(p => p + 1); + }; + removeEventListeners = () => { + fragmentRef.current.removeEventListener('click', logClick); + }; + React.useEffect(() => { + fragmentRef.current.addEventListener('click', logClick); - // @gate enableFragmentRefs - it('applies event listeners to portaled children', async () => { - const fragmentRef = React.createRef(); - const childARef = React.createRef(); - const childBRef = React.createRef(); - const root = ReactDOMClient.createRoot(container); + return removeEventListeners; + }); - function Test() { - return ( - -
    - {createPortal(
    , document.body)} - - ); - } + return ( + +
    + + ); + } - await act(() => { - root.render(); - }); + // The event listener was applied + await act(() => root.render()); + expect(logs).toEqual([]); + document.querySelector('#child-a').click(); + expect(logs).toEqual(['child-a']); - const logs = []; - fragmentRef.current.addEventListener('click', e => { - logs.push(e.target.id); + // The event listener can be removed and re-added + logs = []; + await act(rerender); + document.querySelector('#child-a').click(); + expect(logs).toEqual(['child-a']); }); - childARef.current.click(); - expect(logs).toEqual(['child-a']); - - logs.length = 0; - childBRef.current.click(); - expect(logs).toEqual(['child-b']); - }); - - describe('with activity', () => { - // @gate enableFragmentRefs && enableActivity - it('does not apply event listeners to hidden trees', async () => { - const parentRef = React.createRef(); - const fragmentRef = React.createRef(); + // @gate enableFragmentRefs + it('does not apply removed event listeners to new children', async () => { const root = ReactDOMClient.createRoot(container); - + const fragmentRef = React.createRef(null); function Test() { return ( -
    - -
    Child 1
    - -
    Child 2
    -
    -
    Child 3
    -
    -
    + +
    + ); } + let logs = []; + function logClick(e) { + logs.push(e.currentTarget.id); + } await act(() => { root.render(); }); + fragmentRef.current.addEventListener('click', logClick); + const childA = document.querySelector('#child-a'); + childA.click(); + expect(logs).toEqual(['child-a']); - const logs = []; - fragmentRef.current.addEventListener('click', e => { - logs.push(e.target.textContent); - }); - - const [child1, child2, child3] = parentRef.current.children; - child1.click(); - child2.click(); - child3.click(); - expect(logs).toEqual(['Child 1', 'Child 3']); + logs = []; + fragmentRef.current.removeEventListener('click', logClick); + childA.click(); + expect(logs).toEqual([]); }); - // @gate enableFragmentRefs && enableActivity - it('applies event listeners to visible trees', async () => { - const parentRef = React.createRef(); + // @gate enableFragmentRefs + it('applies event listeners to portaled children', async () => { const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( -
    - -
    Child 1
    - -
    Child 2
    -
    -
    Child 3
    -
    -
    + +
    + {createPortal( +
    , + document.body, + )} + ); } @@ -712,67 +648,242 @@ describe('FragmentRefs', () => { const logs = []; fragmentRef.current.addEventListener('click', e => { - logs.push(e.target.textContent); + logs.push(e.target.id); }); - const [child1, child2, child3] = parentRef.current.children; - child1.click(); - child2.click(); - child3.click(); - expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']); + childARef.current.click(); + expect(logs).toEqual(['child-a']); + + logs.length = 0; + childBRef.current.click(); + expect(logs).toEqual(['child-b']); + }); + + describe('with activity', () => { + // @gate enableFragmentRefs && enableActivity + it('does not apply event listeners to hidden trees', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
    + +
    Child 1
    + +
    Child 2
    +
    +
    Child 3
    +
    +
    + ); + } + + await act(() => { + root.render(); + }); + + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); + }); + + const [child1, child2, child3] = parentRef.current.children; + child1.click(); + child2.click(); + child3.click(); + expect(logs).toEqual(['Child 1', 'Child 3']); + }); + + // @gate enableFragmentRefs && enableActivity + it('applies event listeners to visible trees', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
    + +
    Child 1
    + +
    Child 2
    +
    +
    Child 3
    +
    +
    + ); + } + + await act(() => { + root.render(); + }); + + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); + }); + + const [child1, child2, child3] = parentRef.current.children; + child1.click(); + child2.click(); + child3.click(); + expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']); + }); + + // @gate enableFragmentRefs && enableActivity + it('handles Activity modes switching', async () => { + const fragmentRef = React.createRef(); + const fragmentRef2 = React.createRef(); + const parentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test({mode}) { + return ( +
    + + +
    Child
    + +
    Child 2
    +
    +
    +
    +
    + ); + } + + await act(() => { + root.render(); + }); + + let logs = []; + fragmentRef.current.addEventListener('click', () => { + logs.push('clicked 1'); + }); + fragmentRef2.current.addEventListener('click', () => { + logs.push('clicked 2'); + }); + parentRef.current.lastChild.click(); + expect(logs).toEqual(['clicked 1', 'clicked 2']); + + logs = []; + await act(() => { + root.render(); + }); + parentRef.current.firstChild.click(); + parentRef.current.lastChild.click(); + expect(logs).toEqual([]); + + logs = []; + await act(() => { + root.render(); + }); + parentRef.current.lastChild.click(); + // Event order is flipped here because the nested child re-registers first + expect(logs).toEqual(['clicked 2', 'clicked 1']); + }); }); + }); - // @gate enableFragmentRefs && enableActivity - it('handles Activity modes switching', async () => { + describe('dispatchEvent()', () => { + // @gate enableFragmentRefs + it('fires events on the host parent if bubbles=true', async () => { const fragmentRef = React.createRef(); - const fragmentRef2 = React.createRef(); - const parentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); + let logs = []; + + function handleClick(e) { + logs.push([e.type, e.target.id, e.currentTarget.id]); + } - function Test({mode}) { + function Test({isMounted}) { return ( -
    - - -
    Child
    - -
    Child 2
    +
    +
    + {isMounted && ( + +
    + Hi +
    - - + )} +
    ); } await act(() => { - root.render(); + root.render(); }); - let logs = []; - fragmentRef.current.addEventListener('click', () => { - logs.push('clicked 1'); - }); - fragmentRef2.current.addEventListener('click', () => { - logs.push('clicked 2'); - }); - parentRef.current.lastChild.click(); - expect(logs).toEqual(['clicked 1', 'clicked 2']); + let isCancelable = !fragmentRef.current.dispatchEvent( + new MouseEvent('click', {bubbles: true}), + ); + expect(logs).toEqual([ + ['click', 'parent', 'parent'], + ['click', 'parent', 'grandparent'], + ]); + expect(isCancelable).toBe(false); - logs = []; + const fragmentInstanceHandle = fragmentRef.current; await act(() => { - root.render(); + root.render(); }); - parentRef.current.firstChild.click(); - parentRef.current.lastChild.click(); + logs = []; + isCancelable = !fragmentInstanceHandle.dispatchEvent( + new MouseEvent('click', {bubbles: true}), + ); expect(logs).toEqual([]); + expect(isCancelable).toBe(false); logs = []; + isCancelable = !fragmentInstanceHandle.dispatchEvent( + new MouseEvent('click', {bubbles: false}), + ); + expect(logs).toEqual([]); + expect(isCancelable).toBe(false); + }); + + // @gate enableFragmentRefs + it('fires events on self, and only self if bubbles=false', async () => { + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + let logs = []; + + function handleClick(e) { + logs.push([e.type, e.target.id, e.currentTarget.id]); + } + + function Test() { + return ( +
    + +
    + ); + } + await act(() => { - root.render(); + root.render(); }); - parentRef.current.lastChild.click(); - // Event order is flipped here because the nested child re-registers first - expect(logs).toEqual(['clicked 2', 'clicked 1']); + + fragmentRef.current.addEventListener('click', handleClick); + + fragmentRef.current.dispatchEvent( + new MouseEvent('click', {bubbles: true}), + ); + expect(logs).toEqual([ + ['click', undefined, undefined], + ['click', 'parent', 'parent'], + ]); + + logs = []; + + fragmentRef.current.dispatchEvent( + new MouseEvent('click', {bubbles: false}), + ); + expect(logs).toEqual([['click', undefined, undefined]]); }); }); });