/** * 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 reactcore */ 'use strict'; let React; let ReactDOMClient; let ReactDOM; let createPortal; let act; let container; let Fragment; let Activity; let mockIntersectionObserver; let simulateIntersection; let setClientRects; let mockRangeClientRects; let assertConsoleErrorDev; let assertConsoleWarnDev; function Wrapper({children}) { return children; } describe('FragmentRefs', () => { beforeEach(() => { jest.resetModules(); React = require('react'); Fragment = React.Fragment; Activity = React.Activity; ReactDOMClient = require('react-dom/client'); ReactDOM = require('react-dom'); createPortal = ReactDOM.createPortal; act = require('internal-test-utils').act; const IntersectionMocks = require('./utils/IntersectionMocks'); mockIntersectionObserver = IntersectionMocks.mockIntersectionObserver; simulateIntersection = IntersectionMocks.simulateIntersection; setClientRects = IntersectionMocks.setClientRects; mockRangeClientRects = IntersectionMocks.mockRangeClientRects; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; assertConsoleWarnDev = require('internal-test-utils').assertConsoleWarnDev; container = document.createElement('div'); document.body.innerHTML = ''; document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); // @gate enableFragmentRefs it('attaches a ref to Fragment', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); await act(() => root.render(
Hi
, ), ); expect(container.innerHTML).toEqual( '
Hi
', ); expect(fragmentRef.current).not.toBe(null); }); // @gate enableFragmentRefs it('accepts a ref callback', async () => { let fragmentRef; const root = ReactDOMClient.createRoot(container); await act(() => { root.render( (fragmentRef = ref)}>
Hi
, ); }); expect(fragmentRef._fragmentFiber).toBeTruthy(); }); // @gate enableFragmentRefs it('is available in effects', async () => { function Test() { const fragmentRef = React.useRef(null); React.useLayoutEffect(() => { expect(fragmentRef.current).not.toBe(null); }); React.useEffect(() => { expect(fragmentRef.current).not.toBe(null); }); return (
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); }); // @gate enableFragmentRefs && enableFragmentRefsInstanceHandles it('attaches fragment handles to nodes', async () => { const fragmentParentRef = React.createRef(); const fragmentRef = React.createRef(); function Test({show}) { return (
A
B
C
{show &&
D
}
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); const childA = document.querySelector('#childA'); const childB = document.querySelector('#childB'); const childC = document.querySelector('#childC'); expect(childA.reactFragments.has(fragmentRef.current)).toBe(true); expect(childB.reactFragments.has(fragmentRef.current)).toBe(true); expect(childC.reactFragments.has(fragmentRef.current)).toBe(false); expect(childA.reactFragments.has(fragmentParentRef.current)).toBe(true); expect(childB.reactFragments.has(fragmentParentRef.current)).toBe(true); expect(childC.reactFragments.has(fragmentParentRef.current)).toBe(true); await act(() => root.render()); const childD = document.querySelector('#childD'); expect(childD.reactFragments.has(fragmentRef.current)).toBe(false); expect(childD.reactFragments.has(fragmentParentRef.current)).toBe(true); }); describe('focus methods', () => { describe('focus()', () => { // @gate enableFragmentRefs it('focuses the first focusable child', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
B C
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-b'); document.activeElement.blur(); }); // @gate enableFragmentRefs it('focuses deeply nested focusable children, depth first', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( ); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('grandchild-a'); }); // @gate enableFragmentRefs it('preserves document order when adding and removing children', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({showA, showB}) { return ( {showA && } {showB && }
); } // Render with A as the first focusable child await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-a'); document.activeElement.blur(); // A is still the first focusable child, but B is also tracked await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-a'); document.activeElement.blur(); // B is now the first focusable child await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-b'); document.activeElement.blur(); }); // @gate enableFragmentRefs it('keeps focus on the first focusable child if already focused', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( A B ); } await act(() => { root.render(); }); // Focus the first child manually document.getElementById('child-a').focus(); expect(document.activeElement.id).toEqual('child-a'); // Calling fragment.focus() should keep focus on child-a, // not skip to child-b await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-a'); document.activeElement.blur(); }); // @gate enableFragmentRefs it('keeps focus on a nested child if already focused', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
Link
); } await act(() => { root.render(); }); // Focus the nested input manually document.getElementById('nested-input').focus(); expect(document.activeElement.id).toEqual('nested-input'); // Calling fragment.focus() should keep focus on nested-input await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('nested-input'); document.activeElement.blur(); }); // @gate enableFragmentRefs it('focuses the first focusable child in a fieldset', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
Shipping
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('street'); document.activeElement.blur(); }); }); describe('focusLast()', () => { // @gate enableFragmentRefs it('focuses the last focusable child', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
A B C
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focusLast(); }); expect(document.activeElement.id).toEqual('child-c'); document.activeElement.blur(); }); // @gate enableFragmentRefs it('focuses deeply nested focusable children, depth first', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( ); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focusLast(); }); expect(document.activeElement.id).toEqual('grandchild-b'); }); }); describe('blur()', () => { // @gate enableFragmentRefs it('removes focus from an element inside of the Fragment', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( A
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('child-a'); await act(() => { fragmentRef.current.blur(); }); expect(document.activeElement).toEqual(document.body); }); // @gate enableFragmentRefs it('removes focus from a nested element inside of the Fragment', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('nested-input'); await act(() => { fragmentRef.current.blur(); }); expect(document.activeElement).toEqual(document.body); }); // @gate enableFragmentRefs it('removes focus from a portaled element inside of the Fragment', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
{createPortal(
, document.body, )}
); } await act(() => { root.render(); }); await act(() => { fragmentRef.current.focus(); }); expect(document.activeElement.id).toEqual('portaled-input'); await act(() => { fragmentRef.current.blur(); }); expect(document.activeElement).toEqual(document.body); }); // @gate enableFragmentRefs it('does not remove focus from elements outside of the Fragment', async () => { const fragmentRefA = React.createRef(); const fragmentRefB = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( A B ); } await act(() => { root.render(); }); await act(() => { fragmentRefA.current.focus(); }); expect(document.activeElement.id).toEqual('child-a'); await act(() => { fragmentRefB.current.blur(); }); expect(document.activeElement.id).toEqual('child-a'); }); }); }); 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); let logs = []; function handleFragmentRefClicks() { logs.push('fragmentRef'); } function Test() { React.useEffect(() => { fragmentRef.current.addEventListener( 'click', handleFragmentRefClicks, ); return () => { fragmentRef.current.removeEventListener( 'click', handleFragmentRefClicks, ); }; }, []); return (
<>Text
A
<>
B
); } await act(() => { root.render(); }); childARef.current.addEventListener('click', () => { logs.push('A'); }); childBRef.current.addEventListener('click', () => { logs.push('B'); }); // Clicking on the parent should not trigger any listeners parentRef.current.click(); expect(logs).toEqual([]); // Clicking a child triggers its own listeners and the Fragment's childARef.current.click(); expect(logs).toEqual(['fragmentRef', 'A']); logs = []; childBRef.current.click(); expect(logs).toEqual(['fragmentRef', 'B']); logs = []; fragmentRef.current.removeEventListener( 'click', handleFragmentRefClicks, ); childARef.current.click(); expect(logs).toEqual(['A']); logs = []; childBRef.current.click(); expect(logs).toEqual(['B']); }); // @gate enableFragmentRefs it('regression: does not detach a registered listener when removing an unregistered one', async () => { const fragmentRef = React.createRef(); const childRef = React.createRef(); const root = ReactDOMClient.createRoot(container); let logs = []; function registeredListener() { logs.push('registered'); } function unregisteredListener() { logs.push('unregistered'); } await act(() => { root.render(
child
, ); }); fragmentRef.current.addEventListener('click', registeredListener); childRef.current.click(); expect(logs).toEqual(['registered']); // Regression: removing a listener that was never added must be a no-op. // It must not detach registered listeners from fragmentInstance, // causing them to stay attached to DOM even after removeEventListener. fragmentRef.current.removeEventListener('click', unregisteredListener); logs = []; childRef.current.click(); expect(logs).toEqual(['registered']); fragmentRef.current.removeEventListener('click', registeredListener); logs = []; childRef.current.click(); expect(logs).toEqual([]); }); // @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
, ); }); let logs = []; function handleFragmentRefClicks() { logs.push('fragmentRef'); } function handleNestedFragmentRefClicks() { logs.push('nestedFragmentRef'); } function handleNestedFragmentRef2Clicks() { logs.push('nestedFragmentRef2'); } 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']); logs = []; childARef.current.click(); expect(logs).toEqual(['fragmentRef']); logs = []; childCRef.current.click(); expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']); logs = []; 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; function Component() { const [shouldShowChild, setShouldShowChild] = React.useState(false); showChild = () => { setShouldShowChild(true); }; return (
A
{shouldShowChild && (
B
)}
); } await act(() => { root.render(); }); expect(fragmentRef.current).not.toBe(null); expect(childRef.current).toBe(null); let hasClicked = false; fragmentRef.current.addEventListener('click', () => { hasClicked = true; }); await act(() => { showChild(); }); expect(childRef.current).not.toBe(null); childRef.current.click(); expect(hasClicked).toBe(true); }); // @gate enableFragmentRefs it('fires a once listener only once across existing children', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
A
B
, ); }); const logs = []; fragmentRef.current.addEventListener( 'click', () => { logs.push('once'); }, {once: true}, ); childARef.current.click(); expect(logs).toEqual(['once']); logs.length = 0; childBRef.current.click(); expect(logs).toEqual([]); }); // @gate enableFragmentRefs it('does not re-arm a once listener when a new child is inserted', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); let showChildB; function Component() { const [shouldShowChildB, setShouldShowChildB] = React.useState(false); showChildB = () => { setShouldShowChildB(true); }; return (
A
{shouldShowChildB && (
B
)}
); } await act(() => { root.render(); }); const logs = []; fragmentRef.current.addEventListener( 'click', () => { logs.push('once'); }, {once: true}, ); childARef.current.click(); expect(logs).toEqual(['once']); await act(() => { showChildB(); }); logs.length = 0; childBRef.current.click(); expect(logs).toEqual([]); }); // @gate enableFragmentRefs && enableFragmentRefsTextNodes it('adds an event listener to a newly added text child', async () => { const fragmentRef = React.createRef(); const parentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); let showText; function Component() { const [shouldShowText, setShouldShowText] = React.useState(false); showText = () => { setShouldShowText(true); }; return (
{shouldShowText ? 'Hello' : null}
); } await act(() => { root.render(); }); const logs = []; fragmentRef.current.addEventListener('click', () => { logs.push('fragment'); }); await act(() => { showText(); }); const textNode = Array.from(parentRef.current.childNodes).find( node => node.nodeType === 3, ); expect(textNode).not.toBe(undefined); textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual(['fragment']); }); // @gate enableFragmentRefs && enableFragmentRefsTextNodes it('removes event listeners from a deleted text child', async () => { const fragmentRef = React.createRef(); const parentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); let hideText; function Component() { const [shouldShowText, setShouldShowText] = React.useState(true); hideText = () => { setShouldShowText(false); }; return (
{shouldShowText ? 'Hello' : null}
); } await act(() => { root.render(); }); const textNode = Array.from(parentRef.current.childNodes).find( node => node.nodeType === 3, ); expect(textNode).not.toBe(undefined); const logs = []; fragmentRef.current.addEventListener('click', () => { logs.push('fragment'); }); textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual(['fragment']); await act(() => { hideText(); }); const detachedHost = document.createElement('div'); document.body.appendChild(detachedHost); detachedHost.appendChild(textNode); logs.length = 0; textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual([]); document.body.removeChild(detachedHost); }); // @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
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; }); return (
); } // The event listener was applied await act(() => root.render()); expect(logs).toEqual([]); document.querySelector('#child-a').click(); expect(logs).toEqual(['child-a']); // 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('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); } await act(() => { root.render(); }); fragmentRef.current.addEventListener('click', logClick); const childA = document.querySelector('#child-a'); childA.click(); expect(logs).toEqual(['child-a']); logs = []; fragmentRef.current.removeEventListener('click', logClick); childA.click(); expect(logs).toEqual([]); }); // @gate enableFragmentRefs it('removes a capture listener registered with boolean when removed with options object', async () => { const fragmentRef = React.createRef(null); function Test() { return (
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const logs = []; function logCapture() { logs.push('capture'); } // Register with boolean `true` (capture phase) fragmentRef.current.addEventListener('click', logCapture, true); document.querySelector('#child-a').click(); expect(logs).toEqual(['capture']); logs.length = 0; // Remove with equivalent options object {capture: true} // Per DOM spec, these are identical - the listener MUST be removed fragmentRef.current.removeEventListener('click', logCapture, { capture: true, }); document.querySelector('#child-a').click(); // Listener should have been removed - logs must remain empty expect(logs).toEqual([]); }); // @gate enableFragmentRefs it('removes a capture listener registered with options object when removed with boolean', async () => { const fragmentRef = React.createRef(null); function Test() { return (
); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const logs = []; function logCapture() { logs.push('capture'); } // Register with options object {capture: true} fragmentRef.current.addEventListener('click', logCapture, { capture: true, }); document.querySelector('#child-b').click(); expect(logs).toEqual(['capture']); logs.length = 0; // Remove with boolean `true` // Per DOM spec, these are identical - the listener MUST be removed fragmentRef.current.removeEventListener('click', logCapture, true); document.querySelector('#child-b').click(); // Listener should have been removed - logs must remain empty expect(logs).toEqual([]); }); // @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 (
{createPortal(
, document.body, )} ); } await act(() => { root.render(); }); const logs = []; fragmentRef.current.addEventListener('click', e => { logs.push(e.target.id); }); childARef.current.click(); expect(logs).toEqual(['child-a']); logs.length = 0; childBRef.current.click(); expect(logs).toEqual(['child-b']); }); // @gate enableFragmentRefs it('applies event listeners to children portaled in after registration', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); let showChildB; function Test() { const [shouldShowChildB, setShouldShowChildB] = React.useState(false); showChildB = () => { setShouldShowChildB(true); }; return ( {createPortal( <>
{shouldShowChildB &&
} , document.body, )} ); } await act(() => { root.render(); }); const logs = []; fragmentRef.current.addEventListener('click', e => { logs.push(e.target.id); }); childARef.current.click(); expect(logs).toEqual(['child-a']); // child-b is inserted into the same portal after the listener was // registered, so it should be treated like its sibling child-a. await act(() => { showChildB(); }); logs.length = 0; childBRef.current.click(); expect(logs).toEqual(['child-b']); }); describe('with activity', () => { // @gate enableFragmentRefs 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 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 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 && enableFragmentRefsTextNodes it('does not dispatch fragment events from text children while hidden', async () => { const parentRef = React.createRef(); const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({mode}) { return (
Element
Text
); } await act(() => { root.render(); }); const logs = []; fragmentRef.current.addEventListener('click', e => { logs.push( e.target.nodeType === 3 ? 'text' : e.target.id || e.target.tagName, ); }); const textNode = Array.from(parentRef.current.childNodes).find( node => node.nodeType === 3, ); expect(textNode).not.toBe(undefined); document.getElementById('child').click(); textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual(['child', 'text']); logs.length = 0; await act(() => { root.render(); }); document.getElementById('child').click(); textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual([]); logs.length = 0; await act(() => { root.render(); }); document.getElementById('child').click(); textNode.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(logs).toEqual(['child', 'text']); }); }); }); describe('dispatchEvent()', () => { // @gate enableFragmentRefs it('fires events on the host parent if bubbles=true', 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({isMounted}) { return (
{isMounted && (
Hi
)}
); } await act(() => { root.render(); }); let isCancelable = !fragmentRef.current.dispatchEvent( new MouseEvent('click', {bubbles: true}), ); expect(logs).toEqual([ ['click', 'parent', 'parent'], ['click', 'parent', 'grandparent'], ]); expect(isCancelable).toBe(false); const fragmentInstanceHandle = fragmentRef.current; await act(() => { root.render(); }); 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(); }); 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]]); }); }); }); describe('observers', () => { beforeEach(() => { mockIntersectionObserver(); }); // @gate enableFragmentRefs it('attaches intersection observers to children', async () => { let logs = []; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { logs.push(entry.target.id); }); }); function Test({showB}) { const fragmentRef = React.useRef(null); React.useEffect(() => { fragmentRef.current.observeUsing(observer); const lastRefValue = fragmentRef.current; return () => { lastRefValue.unobserveUsing(observer); }; }, []); return (
A
{showB &&
B
}
); } function simulateAllChildrenIntersecting() { const parent = container.firstChild; if (parent) { const children = Array.from(parent.children).map(child => { return [child, {y: 0, x: 0, width: 1, height: 1}, 1]; }); simulateIntersection(...children); } } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); simulateAllChildrenIntersecting(); expect(logs).toEqual(['childA']); // Reveal child and expect it to be observed logs = []; await act(() => root.render()); simulateAllChildrenIntersecting(); expect(logs).toEqual(['childA', 'childB']); // Hide child and expect it to be unobserved logs = []; await act(() => root.render()); simulateAllChildrenIntersecting(); expect(logs).toEqual(['childA']); // Unmount component and expect all children to be unobserved logs = []; await act(() => root.render(null)); simulateAllChildrenIntersecting(); expect(logs).toEqual([]); }); // @gate enableFragmentRefs it('warns when unobserveUsing() is called with an observer that was not observed', async () => { const fragmentRef = React.createRef(); const observer = new IntersectionObserver(() => {}); const observer2 = new IntersectionObserver(() => {}); function Test() { return (
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); // Warning when there is no attached observer fragmentRef.current.unobserveUsing(observer); assertConsoleErrorDev([ 'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' + 'instance. First attach the observer with observeUsing()', ]); // Warning when the attached observer does not match fragmentRef.current.observeUsing(observer); fragmentRef.current.unobserveUsing(observer2); assertConsoleErrorDev([ 'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' + 'instance. First attach the observer with observeUsing()', ]); }); // @gate enableFragmentRefs && enableFragmentRefsInstanceHandles it('attaches handles to observed elements to allow caching of observers', async () => { const targetToCallbackMap = new WeakMap(); let cachedObserver = null; function createObserverIfNeeded(fragmentInstance, onIntersection) { const callbacks = targetToCallbackMap.get(fragmentInstance); targetToCallbackMap.set( fragmentInstance, callbacks ? [...callbacks, onIntersection] : [onIntersection], ); if (cachedObserver !== null) { return cachedObserver; } const observer = new IntersectionObserver(entries => { entries.forEach(entry => { const fragmentInstances = entry.target.reactFragments; if (fragmentInstances) { Array.from(fragmentInstances).forEach(fInstance => { const cbs = targetToCallbackMap.get(fInstance) || []; cbs.forEach(callback => { callback(entry); }); }); } targetToCallbackMap.get(entry.target)?.forEach(callback => { callback(entry); }); }); }); cachedObserver = observer; return observer; } function IntersectionObserverFragment({onIntersection, children}) { const fragmentRef = React.useRef(null); React.useLayoutEffect(() => { const observer = createObserverIfNeeded( fragmentRef.current, onIntersection, ); fragmentRef.current.observeUsing(observer); const lastRefValue = fragmentRef.current; return () => { lastRefValue.unobserveUsing(observer); }; }, []); return {children}; } let logs = []; function logIntersection(id) { logs.push(`observe: ${id}`); } function ChildWithManualIO({id}) { const divRef = React.useRef(null); React.useLayoutEffect(() => { const observer = createObserverIfNeeded(divRef.current, entry => { logIntersection(id); }); observer.observe(divRef.current); return () => { observer.unobserve(divRef.current); }; }, []); return (
{id}
); } function Test() { return ( <> logIntersection('grandparent')}> logIntersection('parentA')}>
A
logIntersection('parentB')}>
B
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); simulateIntersection([ container.querySelector('#childA'), {y: 0, x: 0, width: 1, height: 1}, 1, ]); expect(logs).toEqual(['observe: grandparent', 'observe: parentA']); logs = []; simulateIntersection([ container.querySelector('#childB'), {y: 0, x: 0, width: 1, height: 1}, 1, ]); expect(logs).toEqual(['observe: parentB']); logs = []; simulateIntersection([ container.querySelector('#childC'), {y: 0, x: 0, width: 1, height: 1}, 1, ]); expect(logs).toEqual(['observe: parentB', 'observe: childC']); }); }); describe('getClientRects', () => { // @gate enableFragmentRefs it('returns the bounding client rects of all children', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); setClientRects(childARef.current, [ { x: 1, y: 2, width: 3, height: 4, }, { x: 5, y: 6, width: 7, height: 8, }, ]); setClientRects(childBRef.current, [{x: 9, y: 10, width: 11, height: 12}]); const clientRects = fragmentRef.current.getClientRects(); expect(clientRects.length).toBe(3); expect(clientRects[0].left).toBe(1); expect(clientRects[1].left).toBe(5); expect(clientRects[2].left).toBe(9); }); }); describe('getRootNode', () => { // @gate enableFragmentRefs it('returns the root node of the parent', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); expect(fragmentRef.current.getRootNode()).toBe(document); }); // The desired behavior here is to return the topmost disconnected element when // fragment + parent are unmounted. Currently we have a pass during unmount that // recursively cleans up return pointers of the whole tree. We can change this // with a future refactor. See: https://github.com/facebook/react/pull/32682#discussion_r2008313082 // @gate enableFragmentRefs it('returns the topmost disconnected element if the fragment and parent are unmounted', async () => { const containerRef = React.createRef(); const parentRef = React.createRef(); const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({mounted}) { return (
{mounted && (
)}
); } await act(() => root.render()); expect(fragmentRef.current.getRootNode()).toBe(document); const fragmentHandle = fragmentRef.current; await act(() => root.render()); // TODO: The commented out assertion is the desired behavior. For now, we return // the fragment instance itself. This is currently the same behavior if you unmount // the fragment but not the parent. See context above. // expect(fragmentHandle.getRootNode().id).toBe(parentRefHandle.id); expect(fragmentHandle.getRootNode()).toBe(fragmentHandle); }); // @gate enableFragmentRefs it('returns self when only the fragment was unmounted', async () => { const fragmentRef = React.createRef(); const parentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({mounted}) { return (
{mounted && (
)}
); } await act(() => root.render()); expect(fragmentRef.current.getRootNode()).toBe(document); const fragmentHandle = fragmentRef.current; await act(() => root.render()); expect(fragmentHandle.getRootNode()).toBe(fragmentHandle); }); }); describe('compareDocumentPosition', () => { function expectPosition(position, spec) { const positionResult = { following: (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0, preceding: (position & Node.DOCUMENT_POSITION_PRECEDING) !== 0, contains: (position & Node.DOCUMENT_POSITION_CONTAINS) !== 0, containedBy: (position & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0, disconnected: (position & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0, implementationSpecific: (position & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0, }; expect(positionResult).toEqual(spec); } // @gate enableFragmentRefs it('returns the relationship between the fragment instance and a given node', async () => { const fragmentRef = React.createRef(); const beforeRef = React.createRef(); const afterRef = React.createRef(); const middleChildRef = React.createRef(); const firstChildRef = React.createRef(); const lastChildRef = React.createRef(); const containerRef = React.createRef(); const disconnectedElement = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); // document.body is preceding and contains the fragment expectPosition( fragmentRef.current.compareDocumentPosition(document.body), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); // beforeRef is preceding the fragment expectPosition( fragmentRef.current.compareDocumentPosition(beforeRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); // afterRef is following the fragment expectPosition( fragmentRef.current.compareDocumentPosition(afterRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); // firstChildRef is contained by the fragment expectPosition( fragmentRef.current.compareDocumentPosition(firstChildRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); // middleChildRef is contained by the fragment expectPosition( fragmentRef.current.compareDocumentPosition(middleChildRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); // lastChildRef is contained by the fragment expectPosition( fragmentRef.current.compareDocumentPosition(lastChildRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); // containerRef precedes and contains the fragment expectPosition( fragmentRef.current.compareDocumentPosition(containerRef.current), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(disconnectedElement), { preceding: false, following: true, contains: false, containedBy: false, disconnected: true, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles fragment instances with one child', async () => { const fragmentRef = React.createRef(); const beforeRef = React.createRef(); const afterRef = React.createRef(); const containerRef = React.createRef(); const onlyChildRef = React.createRef(); const disconnectedElement = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); expectPosition( fragmentRef.current.compareDocumentPosition(beforeRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(afterRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(onlyChildRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(containerRef.current), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(disconnectedElement), { preceding: false, following: true, contains: false, containedBy: false, disconnected: true, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles empty fragment instances', async () => { const fragmentRef = React.createRef(); const beforeParentRef = React.createRef(); const beforeRef = React.createRef(); const afterRef = React.createRef(); const afterParentRef = React.createRef(); const containerRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( <>
); } await act(() => root.render()); expectPosition( fragmentRef.current.compareDocumentPosition(document.body), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(beforeRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(beforeParentRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(afterRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(afterParentRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(containerRef.current), { preceding: false, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles empty fragments nested inside non-host wrappers', async () => { const fragmentRef = React.createRef(); const beforeRef = React.createRef(); const afterRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); expectPosition( fragmentRef.current.compareDocumentPosition(beforeRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(afterRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles nested children', async () => { const fragmentRef = React.createRef(); const nestedFragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const childCRef = React.createRef(); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); function Child() { return (
C
); } function Test() { return (
A
B
); } await act(() => root.render()); expectPosition( fragmentRef.current.compareDocumentPosition(childARef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childBRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childCRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); }); // @gate enableFragmentRefs it('returns disconnected for comparison with an unmounted fragment instance', async () => { const fragmentRef = React.createRef(); const containerRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({mount}) { return (
{mount && (
)}
); } await act(() => root.render()); const fragmentHandle = fragmentRef.current; expectPosition( fragmentHandle.compareDocumentPosition(containerRef.current), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); await act(() => { root.render(); }); expectPosition( fragmentHandle.compareDocumentPosition(containerRef.current), { preceding: false, following: false, contains: false, containedBy: false, disconnected: true, implementationSpecific: false, }, ); }); // @gate enableFragmentRefs it('compares a root-level Fragment', async () => { const fragmentRef = React.createRef(); const emptyFragmentRef = React.createRef(); const childRef = React.createRef(); const siblingPrecedingRef = React.createRef(); const siblingFollowingRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return (
); } await act(() => root.render()); const fragmentInstance = fragmentRef.current; if (fragmentInstance == null) { throw new Error('Expected fragment instance to be non-null'); } const emptyFragmentInstance = emptyFragmentRef.current; if (emptyFragmentInstance == null) { throw new Error('Expected empty fragment instance to be non-null'); } expectPosition( fragmentInstance.compareDocumentPosition(childRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentInstance.compareDocumentPosition(siblingPrecedingRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentInstance.compareDocumentPosition(siblingFollowingRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( emptyFragmentInstance.compareDocumentPosition(childRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( emptyFragmentInstance.compareDocumentPosition( siblingPrecedingRef.current, ), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( emptyFragmentInstance.compareDocumentPosition( siblingFollowingRef.current, ), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); describe('with portals', () => { // @gate enableFragmentRefs it('handles portaled elements', async () => { const fragmentRef = React.createRef(); const portaledSiblingRef = React.createRef(); const portaledChildRef = React.createRef(); function Test() { return (
{createPortal(
, container)} {createPortal(
, container)}
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); // The sibling is preceding in both the DOM and the React tree expectPosition( fragmentRef.current.compareDocumentPosition( portaledSiblingRef.current, ), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: false, }, ); // The child is contained by in the React tree but not in the DOM expectPosition( fragmentRef.current.compareDocumentPosition(portaledChildRef.current), { preceding: false, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles multiple portals to the same element', async () => { const root = ReactDOMClient.createRoot(container); const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const childCRef = React.createRef(); const childDRef = React.createRef(); const childERef = React.createRef(); function Test() { const [c, setC] = React.useState(false); React.useEffect(() => { setC(true); }); return ( <> {createPortal(
{c ? (
) : null} , document.body, )} {createPortal(

, document.body)}

); } await act(() => root.render()); // Due to effect, order is E / A->B->C->D expect(document.body.outerHTML).toBe( '' + '
' + '
' + '

' + '
' + '', ); expectPosition( fragmentRef.current.compareDocumentPosition(document.body), { preceding: true, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childARef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); // Contained by in DOM, but following in React tree expectPosition( fragmentRef.current.compareDocumentPosition(childBRef.current), { preceding: false, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childCRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childDRef.current), { preceding: false, following: false, contains: false, containedBy: true, disconnected: false, implementationSpecific: false, }, ); // Preceding DOM but following in React tree expectPosition( fragmentRef.current.compareDocumentPosition(childERef.current), { preceding: false, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('handles empty fragments', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); function Test() { return ( <>
{createPortal(, document.body)}
); } const root = ReactDOMClient.createRoot(container); await act(() => root.render()); expectPosition( fragmentRef.current.compareDocumentPosition(document.body), { preceding: false, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childARef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(childBRef.current), { preceding: false, following: true, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); // @gate enableFragmentRefs it('positions empty portaled fragments against the portal container', async () => { const fragmentRef = React.createRef(); const reactParentRef = React.createRef(); const portalTarget = document.createElement('div'); portalTarget.id = 'portal-target'; document.body.appendChild(portalTarget); const root = ReactDOMClient.createRoot(container); function Test() { return (
{createPortal(, portalTarget)}
); } await act(() => root.render()); // Empty CDP must use the portal container as parent expectPosition( fragmentRef.current.compareDocumentPosition(portalTarget), { preceding: false, following: false, contains: true, containedBy: false, disconnected: false, implementationSpecific: true, }, ); expectPosition( fragmentRef.current.compareDocumentPosition(reactParentRef.current), { preceding: true, following: false, contains: false, containedBy: false, disconnected: false, implementationSpecific: true, }, ); }); }); }); describe('scrollIntoView', () => { function expectLast(arr, test) { expect(arr[arr.length - 1]).toBe(test); } // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView it('does not yet support options', async () => { const fragmentRef = React.createRef(); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(() => { fragmentRef.current.scrollIntoView({block: 'start'}); }).toThrowError( 'FragmentInstance.scrollIntoView() does not support ' + 'scrollIntoViewOptions. Use the alignToTop boolean instead.', ); }); describe('with children', () => { // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView it('settles scroll on the first child by default, or if alignToTop=true', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
A
B
, ); }); let logs = []; childARef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childA'); }); childBRef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childB'); }); // Default call fragmentRef.current.scrollIntoView(); expectLast(logs, 'childA'); logs = []; // alignToTop=true fragmentRef.current.scrollIntoView(true); expectLast(logs, 'childA'); }); // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView it('calls scrollIntoView on the last child if alignToTop is false', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
A
B
, ); }); const logs = []; childARef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childA'); }); childBRef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childB'); }); fragmentRef.current.scrollIntoView(false); expectLast(logs, 'childB'); }); // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView it('handles portaled elements -- same scroll container', async () => { const fragmentRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test() { return ( {createPortal(
A
, document.body, )}
B
); } await act(() => { root.render(); }); const logs = []; childARef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childA'); }); childBRef.current.scrollIntoView = jest.fn().mockImplementation(() => { logs.push('childB'); }); // Default call fragmentRef.current.scrollIntoView(); expectLast(logs, 'childA'); }); // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView it('handles portaled elements -- different scroll container', async () => { const fragmentRef = React.createRef(); const headerChildRef = React.createRef(); const childARef = React.createRef(); const childBRef = React.createRef(); const childCRef = React.createRef(); const scrollContainerRef = React.createRef(); const scrollContainerNestedRef = React.createRef(); const root = ReactDOMClient.createRoot(container); function Test({mountFragment}) { return ( <>