mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
8a8df5dbdd
`fragmentInstance.dispatchEvent(evt)` calls `element.dispatchEvent(evt)` on the fragment's host parent. This mimics bubbling if the `fragmentInstance` could receive an event itself. If the parent is disconnected, there is a dev warning and no event is dispatched.
1737 lines
51 KiB
JavaScript
1737 lines
51 KiB
JavaScript
/**
|
|
* 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 assertConsoleErrorDev;
|
|
|
|
function Wrapper({children}) {
|
|
return children;
|
|
}
|
|
|
|
describe('FragmentRefs', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
Fragment = React.Fragment;
|
|
Activity = React.unstable_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;
|
|
assertConsoleErrorDev =
|
|
require('internal-test-utils').assertConsoleErrorDev;
|
|
|
|
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(
|
|
<div id="parent">
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child">Hi</div>
|
|
</Fragment>
|
|
</div>,
|
|
),
|
|
);
|
|
expect(container.innerHTML).toEqual(
|
|
'<div id="parent"><div id="child">Hi</div></div>',
|
|
);
|
|
|
|
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(
|
|
<Fragment ref={ref => (fragmentRef = ref)}>
|
|
<div id="child">Hi</div>
|
|
</Fragment>,
|
|
);
|
|
});
|
|
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div />
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => root.render(<Test />));
|
|
});
|
|
|
|
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 (
|
|
<div>
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a" />
|
|
<style>{`#child-c {}`}</style>
|
|
<a id="child-b" href="/">
|
|
B
|
|
</a>
|
|
<a id="child-c" href="/">
|
|
C
|
|
</a>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a">
|
|
<div tabIndex={0} id="grandchild-a">
|
|
<a id="greatgrandchild-a" href="/" />
|
|
</div>
|
|
</div>
|
|
<a id="child-b" href="/" />
|
|
</Fragment>
|
|
);
|
|
}
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
{showA && <a href="/" id="child-a" />}
|
|
{showB && <a href="/" id="child-b" />}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
// Render with A as the first focusable child
|
|
await act(() => {
|
|
root.render(<Test showA={true} showB={false} />);
|
|
});
|
|
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(<Test showA={true} showB={true} />);
|
|
});
|
|
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(<Test showA={false} showB={true} />);
|
|
});
|
|
await act(() => {
|
|
fragmentRef.current.focus();
|
|
});
|
|
expect(document.activeElement.id).toEqual('child-b');
|
|
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 (
|
|
<div>
|
|
<Fragment ref={fragmentRef}>
|
|
<a id="child-a" href="/">
|
|
A
|
|
</a>
|
|
<a id="child-b" href="/">
|
|
B
|
|
</a>
|
|
<Wrapper>
|
|
<a id="child-c" href="/">
|
|
C
|
|
</a>
|
|
</Wrapper>
|
|
<div id="child-d" />
|
|
<style id="child-e">{`#child-d {}`}</style>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a" href="/">
|
|
<a id="grandchild-a" href="/" />
|
|
<a id="grandchild-b" href="/" />
|
|
</div>
|
|
<div tabIndex={0} id="child-b">
|
|
<a id="grandchild-a" href="/" />
|
|
<a id="grandchild-b" href="/" />
|
|
</div>
|
|
</Fragment>
|
|
);
|
|
}
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<a id="child-a" href="/">
|
|
A
|
|
</a>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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('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 (
|
|
<Fragment ref={fragmentRefA}>
|
|
<a id="child-a" href="/">
|
|
A
|
|
</a>
|
|
<Fragment ref={fragmentRefB}>
|
|
<a id="child-b" href="/">
|
|
B
|
|
</a>
|
|
</Fragment>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<div ref={parentRef}>
|
|
<Fragment ref={fragmentRef}>
|
|
<>Text</>
|
|
<div ref={childARef}>A</div>
|
|
<>
|
|
<div ref={childBRef}>B</div>
|
|
</>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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('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(
|
|
<div>
|
|
<Fragment ref={fragmentRef}>
|
|
<div ref={childARef}>A</div>
|
|
<div>
|
|
<Fragment ref={nestedFragmentRef}>
|
|
<div ref={childBRef}>B</div>
|
|
</Fragment>
|
|
</div>
|
|
<Fragment ref={nestedFragmentRef2}>
|
|
<div ref={childCRef}>C</div>
|
|
</Fragment>
|
|
</Fragment>
|
|
</div>,
|
|
);
|
|
});
|
|
|
|
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 (
|
|
<div>
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="a">A</div>
|
|
{shouldShowChild && (
|
|
<div ref={childRef} id="b">
|
|
B
|
|
</div>
|
|
)}
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Component />);
|
|
});
|
|
|
|
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('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(
|
|
<div>
|
|
<Fragment ref={fragmentRef}>
|
|
<div ref={childRef}>Host A</div>
|
|
<Wrapper>
|
|
<Wrapper>
|
|
<Wrapper>
|
|
<div ref={nestedChildRef}>Host B</div>
|
|
</Wrapper>
|
|
</Wrapper>
|
|
</Wrapper>
|
|
</Fragment>
|
|
</div>,
|
|
);
|
|
});
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a" />
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
// The event listener was applied
|
|
await act(() => root.render(<Test />));
|
|
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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a" />
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
let logs = [];
|
|
function logClick(e) {
|
|
logs.push(e.currentTarget.id);
|
|
}
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
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('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 (
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="child-a" ref={childARef} />
|
|
{createPortal(
|
|
<div id="child-b" ref={childBRef} />,
|
|
document.body,
|
|
)}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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']);
|
|
});
|
|
|
|
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 (
|
|
<div ref={parentRef}>
|
|
<Fragment ref={fragmentRef}>
|
|
<div>Child 1</div>
|
|
<Activity mode="hidden">
|
|
<div>Child 2</div>
|
|
</Activity>
|
|
<div>Child 3</div>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<div ref={parentRef}>
|
|
<Fragment ref={fragmentRef}>
|
|
<div>Child 1</div>
|
|
<Activity mode="visible">
|
|
<div>Child 2</div>
|
|
</Activity>
|
|
<div>Child 3</div>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<div id="parent" ref={parentRef}>
|
|
<Fragment ref={fragmentRef}>
|
|
<Activity mode={mode}>
|
|
<div id="child1">Child</div>
|
|
<Fragment ref={fragmentRef2}>
|
|
<div id="child2">Child 2</div>
|
|
</Fragment>
|
|
</Activity>
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test mode="visible" />);
|
|
});
|
|
|
|
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(<Test mode="hidden" />);
|
|
});
|
|
parentRef.current.firstChild.click();
|
|
parentRef.current.lastChild.click();
|
|
expect(logs).toEqual([]);
|
|
|
|
logs = [];
|
|
await act(() => {
|
|
root.render(<Test mode="visible" />);
|
|
});
|
|
parentRef.current.lastChild.click();
|
|
// Event order is flipped here because the nested child re-registers first
|
|
expect(logs).toEqual(['clicked 2', 'clicked 1']);
|
|
});
|
|
});
|
|
});
|
|
|
|
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 (
|
|
<div onClick={handleClick} id="grandparent">
|
|
<div onClick={handleClick} id="parent">
|
|
{isMounted && (
|
|
<Fragment ref={fragmentRef}>
|
|
<div onClick={handleClick} id="child">
|
|
Hi
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test isMounted={true} />);
|
|
});
|
|
|
|
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(<Test isMounted={false} />);
|
|
});
|
|
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 (
|
|
<div id="parent" onClick={handleClick}>
|
|
<Fragment ref={fragmentRef} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => {
|
|
root.render(<Test />);
|
|
});
|
|
|
|
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 (
|
|
<div id="parent">
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div id="childA">A</div>
|
|
{showB && <div id="childB">B</div>}
|
|
</React.Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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(<Test showB={false} />));
|
|
simulateAllChildrenIntersecting();
|
|
expect(logs).toEqual(['childA']);
|
|
|
|
// Reveal child and expect it to be observed
|
|
logs = [];
|
|
await act(() => root.render(<Test showB={true} />));
|
|
simulateAllChildrenIntersecting();
|
|
expect(logs).toEqual(['childA', 'childB']);
|
|
|
|
// Hide child and expect it to be unobserved
|
|
logs = [];
|
|
await act(() => root.render(<Test showB={false} />));
|
|
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 (
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div />
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => root.render(<Test />));
|
|
|
|
// 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()',
|
|
],
|
|
{withoutStack: true},
|
|
);
|
|
|
|
// 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()',
|
|
],
|
|
{withoutStack: true},
|
|
);
|
|
});
|
|
});
|
|
|
|
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 (
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div ref={childARef} />
|
|
<div ref={childBRef} />
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
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 (
|
|
<div>
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div />
|
|
</React.Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
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 (
|
|
<div ref={containerRef} id="container">
|
|
{mounted && (
|
|
<div ref={parentRef} id="parent">
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div />
|
|
</React.Fragment>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test mounted={true} />));
|
|
expect(fragmentRef.current.getRootNode()).toBe(document);
|
|
const fragmentHandle = fragmentRef.current;
|
|
await act(() => root.render(<Test mounted={false} />));
|
|
// 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 (
|
|
<div ref={parentRef} id="parent">
|
|
{mounted && (
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div />
|
|
</React.Fragment>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test mounted={true} />));
|
|
expect(fragmentRef.current.getRootNode()).toBe(document);
|
|
const fragmentHandle = fragmentRef.current;
|
|
await act(() => root.render(<Test mounted={false} />));
|
|
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 (
|
|
<div ref={containerRef}>
|
|
<div ref={beforeRef} />
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div ref={firstChildRef} />
|
|
<div ref={middleChildRef} />
|
|
<div ref={lastChildRef} />
|
|
</React.Fragment>
|
|
<div ref={afterRef} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
|
|
// 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 preceds 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 (
|
|
<div id="container" ref={containerRef}>
|
|
<div>
|
|
<div ref={beforeRef} id="before" />
|
|
<React.Fragment ref={fragmentRef}>
|
|
<div ref={onlyChildRef} id="within" />
|
|
</React.Fragment>
|
|
<div id="after" ref={afterRef} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
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 (
|
|
<>
|
|
<div id="before-container" ref={beforeParentRef} />
|
|
<div id="container" ref={containerRef}>
|
|
<div id="before" ref={beforeRef} />
|
|
<React.Fragment ref={fragmentRef} />
|
|
<div id="after" ref={afterRef} />
|
|
</div>
|
|
<div id="after-container" ref={afterParentRef} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
|
|
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('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 (
|
|
<div ref={containerRef}>
|
|
{mount && (
|
|
<Fragment ref={fragmentRef}>
|
|
<div />
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test mount={true} />));
|
|
|
|
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(<Test mount={false} />);
|
|
});
|
|
|
|
expectPosition(
|
|
fragmentHandle.compareDocumentPosition(containerRef.current),
|
|
{
|
|
preceding: false,
|
|
following: false,
|
|
contains: false,
|
|
containedBy: false,
|
|
disconnected: true,
|
|
implementationSpecific: false,
|
|
},
|
|
);
|
|
});
|
|
|
|
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 (
|
|
<div>
|
|
{createPortal(<div ref={portaledSiblingRef} />, document.body)}
|
|
<Fragment ref={fragmentRef}>
|
|
{createPortal(<div ref={portaledChildRef} />, document.body)}
|
|
<div />
|
|
</Fragment>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => root.render(<Test />));
|
|
|
|
// 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();
|
|
|
|
function Test() {
|
|
const [c, setC] = React.useState(false);
|
|
React.useEffect(() => {
|
|
setC(true);
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{createPortal(
|
|
<Fragment ref={fragmentRef}>
|
|
<div id="A" ref={childARef} />
|
|
{c ? <div id="C" ref={childCRef} /> : null}
|
|
</Fragment>,
|
|
document.body,
|
|
)}
|
|
{createPortal(<p id="B" ref={childBRef} />, document.body)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(() => root.render(<Test />));
|
|
|
|
// Due to effect, order is A->B->C
|
|
expect(document.body.innerHTML).toBe(
|
|
'<div></div>' +
|
|
'<div id="A"></div>' +
|
|
'<p id="B"></p>' +
|
|
'<div id="C"></div>',
|
|
);
|
|
|
|
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,
|
|
},
|
|
);
|
|
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,
|
|
},
|
|
);
|
|
});
|
|
|
|
// @gate enableFragmentRefs
|
|
it('handles empty fragments', async () => {
|
|
const fragmentRef = React.createRef();
|
|
const childARef = React.createRef();
|
|
const childBRef = React.createRef();
|
|
|
|
function Test() {
|
|
return (
|
|
<>
|
|
<div id="A" ref={childARef} />
|
|
{createPortal(<Fragment ref={fragmentRef} />, document.body)}
|
|
<div id="B" ref={childBRef} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => root.render(<Test />));
|
|
|
|
expectPosition(
|
|
fragmentRef.current.compareDocumentPosition(document.body),
|
|
{
|
|
preceding: true,
|
|
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: true,
|
|
following: false,
|
|
contains: false,
|
|
containedBy: false,
|
|
disconnected: false,
|
|
implementationSpecific: true,
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|