Add compareDocumentPosition to fragment instances (#32722)

This adds `compareDocumentPosition(otherNode)` to fragment instances.

The semantics implemented are meant to match typical element
positioning, with some fragment specifics. See the unit tests for all
expectations.

- An element preceding a fragment is `Node.DOCUMENT_POSITION_PRECEDING`
- An element after a fragment is `Node.DOCUMENT_POSITION_FOLLOWING`
- An element containing the fragment is
`Node.DOCUMENT_POSITION_PRECEDING` and
`Node.DOCUMENT_POSITION_CONTAINING`
- An element within the fragment is
`Node.DOCUMENT_POSITION_CONTAINED_BY`
- An element compared against an empty fragment will result in
`Node.DOCUMENT_POSITION_DISCONNECTED` and
`Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`

Since we assume a fragment instances target children are DOM siblings
and we want to compare the full fragment as a pseudo container, we can
compare against the first target child outside of handling the special
cases (empty fragments and contained elements).
This commit is contained in:
Jack Pope
2025-05-06 13:01:40 -04:00
committed by GitHub
parent 7a2c7045ae
commit e5a8de81e5
5 changed files with 1014 additions and 85 deletions
+171 -24
View File
@@ -37,6 +37,11 @@ import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
import hasOwnProperty from 'shared/hasOwnProperty';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {
isFiberContainedBy,
isFiberFollowing,
isFiberPreceding,
} from 'react-reconciler/src/ReactFiberTreeReflection';
export {
setCurrentUpdatePriority,
@@ -60,7 +65,9 @@ import {
} from './ReactDOMComponentTree';
import {
traverseFragmentInstance,
getFragmentParentHostInstance,
getFragmentParentHostFiber,
getNextSiblingHostFiber,
getInstanceFromHostFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';
export {detachDeletedInstance};
@@ -2599,6 +2606,7 @@ export type FragmentInstanceType = {
getRootNode(getRootNodeOptions?: {
composed: boolean,
}): Document | ShadowRoot | FragmentInstanceType,
compareDocumentPosition(otherNode: Instance): number,
};
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -2636,12 +2644,13 @@ FragmentInstance.prototype.addEventListener = function (
this._eventListeners = listeners;
};
function addEventListenerToChild(
child: Instance,
child: Fiber,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): boolean {
child.addEventListener(type, listener, optionsOrUseCapture);
const instance = getInstanceFromHostFiber<Instance>(child);
instance.addEventListener(type, listener, optionsOrUseCapture);
return false;
}
// $FlowFixMe[prop-missing]
@@ -2675,12 +2684,13 @@ FragmentInstance.prototype.removeEventListener = function (
}
};
function removeEventListenerFromChild(
child: Instance,
child: Fiber,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): boolean {
child.removeEventListener(type, listener, optionsOrUseCapture);
const instance = getInstanceFromHostFiber<Instance>(child);
instance.removeEventListener(type, listener, optionsOrUseCapture);
return false;
}
// $FlowFixMe[prop-missing]
@@ -2690,28 +2700,32 @@ FragmentInstance.prototype.focus = function (
): void {
traverseFragmentInstance(
this._fragmentFiber,
setFocusIfFocusable,
setFocusOnFiberIfFocusable,
focusOptions,
);
};
function setFocusOnFiberIfFocusable(
fiber: Fiber,
focusOptions?: FocusOptions,
): boolean {
const instance = getInstanceFromHostFiber<Instance>(fiber);
return setFocusIfFocusable(instance, focusOptions);
}
// $FlowFixMe[prop-missing]
FragmentInstance.prototype.focusLast = function (
this: FragmentInstanceType,
focusOptions?: FocusOptions,
): void {
const children: Array<Instance> = [];
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (setFocusIfFocusable(child, focusOptions)) {
if (setFocusOnFiberIfFocusable(child, focusOptions)) {
break;
}
}
};
function collectChildren(
child: Instance,
collection: Array<Instance>,
): boolean {
function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
collection.push(child);
return false;
}
@@ -2724,12 +2738,13 @@ FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void {
blurActiveElementWithinFragment,
);
};
function blurActiveElementWithinFragment(child: Instance): boolean {
function blurActiveElementWithinFragment(child: Fiber): boolean {
// TODO: We can get the activeElement from the parent outside of the loop when we have a reference.
const ownerDocument = child.ownerDocument;
if (child === ownerDocument.activeElement) {
const instance = getInstanceFromHostFiber<Instance>(child);
const ownerDocument = instance.ownerDocument;
if (instance === ownerDocument.activeElement) {
// $FlowFixMe[prop-missing]
child.blur();
instance.blur();
return true;
}
return false;
@@ -2746,10 +2761,11 @@ FragmentInstance.prototype.observeUsing = function (
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
};
function observeChild(
child: Instance,
child: Fiber,
observer: IntersectionObserver | ResizeObserver,
) {
observer.observe(child);
const instance = getInstanceFromHostFiber<Instance>(child);
observer.observe(instance);
return false;
}
// $FlowFixMe[prop-missing]
@@ -2770,10 +2786,11 @@ FragmentInstance.prototype.unobserveUsing = function (
}
};
function unobserveChild(
child: Instance,
child: Fiber,
observer: IntersectionObserver | ResizeObserver,
) {
observer.unobserve(child);
const instance = getInstanceFromHostFiber<Instance>(child);
observer.unobserve(instance);
return false;
}
// $FlowFixMe[prop-missing]
@@ -2784,9 +2801,10 @@ FragmentInstance.prototype.getClientRects = function (
traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects);
return rects;
};
function collectClientRects(child: Instance, rects: Array<DOMRect>): boolean {
function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
const instance = getInstanceFromHostFiber<Instance>(child);
// $FlowFixMe[method-unbinding]
rects.push.apply(rects, child.getClientRects());
rects.push.apply(rects, instance.getClientRects());
return false;
}
// $FlowFixMe[prop-missing]
@@ -2794,15 +2812,144 @@ FragmentInstance.prototype.getRootNode = function (
this: FragmentInstanceType,
getRootNodeOptions?: {composed: boolean},
): Document | ShadowRoot | FragmentInstanceType {
const parentHostInstance = getFragmentParentHostInstance(this._fragmentFiber);
if (parentHostInstance === null) {
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return this;
}
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);
const rootNode =
// $FlowFixMe[incompatible-cast] Flow expects Node
(parentHostInstance.getRootNode(getRootNodeOptions): Document | ShadowRoot);
return rootNode;
};
// $FlowFixMe[prop-missing]
FragmentInstance.prototype.compareDocumentPosition = function (
this: FragmentInstanceType,
otherNode: Instance,
): number {
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
let result = Node.DOCUMENT_POSITION_DISCONNECTED;
if (children.length === 0) {
// If the fragment has no children, we can use the parent and
// siblings to determine a position.
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);
const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
result = parentResult;
if (parentHostInstance === otherNode) {
result = Node.DOCUMENT_POSITION_CONTAINS;
} else {
if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
// otherNode is one of the fragment's siblings. Use the next
// sibling to determine if its preceding or following.
const nextSiblingFiber = getNextSiblingHostFiber(this._fragmentFiber);
if (nextSiblingFiber === null) {
result = Node.DOCUMENT_POSITION_PRECEDING;
} else {
const nextSiblingInstance =
getInstanceFromHostFiber<Instance>(nextSiblingFiber);
const nextSiblingResult =
nextSiblingInstance.compareDocumentPosition(otherNode);
if (
nextSiblingResult === 0 ||
nextSiblingResult & Node.DOCUMENT_POSITION_FOLLOWING
) {
result = Node.DOCUMENT_POSITION_FOLLOWING;
} else {
result = Node.DOCUMENT_POSITION_PRECEDING;
}
}
}
}
result |= Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
return result;
}
const firstElement = getInstanceFromHostFiber<Instance>(children[0]);
const lastElement = getInstanceFromHostFiber<Instance>(
children[children.length - 1],
);
const firstResult = firstElement.compareDocumentPosition(otherNode);
const lastResult = lastElement.compareDocumentPosition(otherNode);
if (
(firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
lastResult & Node.DOCUMENT_POSITION_PRECEDING) ||
otherNode === firstElement ||
otherNode === lastElement
) {
result = Node.DOCUMENT_POSITION_CONTAINED_BY;
} else {
result = firstResult;
}
if (
result & Node.DOCUMENT_POSITION_DISCONNECTED ||
result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
) {
return result;
}
// Now that we have the result from the DOM API, we double check it matches
// the state of the React tree. If it doesn't, we have a case of portaled or
// otherwise injected elements and we return DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC.
const documentPositionMatchesFiberPosition =
validateDocumentPositionWithFiberTree(
result,
this._fragmentFiber,
children[0],
children[children.length - 1],
otherNode,
);
if (documentPositionMatchesFiberPosition) {
return result;
}
return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
};
function validateDocumentPositionWithFiberTree(
documentPosition: number,
fragmentFiber: Fiber,
precedingBoundaryFiber: Fiber,
followingBoundaryFiber: Fiber,
otherNode: Instance,
): boolean {
const otherFiber = getClosestInstanceFromNode(otherNode);
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
return !!otherFiber && isFiberContainedBy(fragmentFiber, otherFiber);
}
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
if (otherFiber === null) {
// otherFiber could be null if its the document or body element
const ownerDocument = otherNode.ownerDocument;
return otherNode === ownerDocument || otherNode === ownerDocument.body;
}
return isFiberContainedBy(otherFiber, fragmentFiber);
}
if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) {
return (
!!otherFiber &&
(otherFiber === precedingBoundaryFiber ||
isFiberPreceding(precedingBoundaryFiber, otherFiber))
);
}
if (documentPosition & Node.DOCUMENT_POSITION_FOLLOWING) {
return (
!!otherFiber &&
(otherFiber === followingBoundaryFiber ||
isFiberFollowing(followingBoundaryFiber, otherFiber))
);
}
return false;
}
function normalizeListenerOptions(
opts: ?EventListenerOptionsOrUseCapture,
@@ -36,6 +36,7 @@ import {
HostText,
ScopeComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection';
import getEventTarget from './getEventTarget';
import {
@@ -891,46 +892,6 @@ function getParent(inst: Fiber | null): Fiber | null {
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
function getLowestCommonAncestor(instA: Fiber, instB: Fiber): Fiber | null {
let nodeA: null | Fiber = instA;
let nodeB: null | Fiber = instB;
let depthA = 0;
for (let tempA: null | Fiber = nodeA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB: null | Fiber = nodeB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
nodeA = getParent(nodeA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
nodeB = getParent(nodeB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (nodeA === nodeB || (nodeB !== null && nodeA === nodeB.alternate)) {
return nodeA;
}
nodeA = getParent(nodeA);
nodeB = getParent(nodeB);
}
return null;
}
function accumulateEnterLeaveListenersForEvent(
dispatchQueue: DispatchQueue,
event: KnownReactSyntheticEvent,
@@ -992,7 +953,8 @@ export function accumulateEnterLeaveTwoPhaseListeners(
from: Fiber | null,
to: Fiber | null,
): void {
const common = from && to ? getLowestCommonAncestor(from, to) : null;
const common =
from && to ? getLowestCommonAncestor(from, to, getParent) : null;
if (from !== null) {
accumulateEnterLeaveListenersForEvent(
@@ -11,6 +11,8 @@
let React;
let ReactDOMClient;
let ReactDOM;
let createPortal;
let act;
let container;
let Fragment;
@@ -31,6 +33,8 @@ describe('FragmentRefs', () => {
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;
@@ -40,6 +44,7 @@ describe('FragmentRefs', () => {
require('internal-test-utils').assertConsoleErrorDev;
container = document.createElement('div');
document.body.innerHTML = '';
document.body.appendChild(container);
});
@@ -611,6 +616,39 @@ describe('FragmentRefs', () => {
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 () => {
@@ -966,4 +1004,568 @@ describe('FragmentRefs', () => {
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,
},
);
});
});
});
});
@@ -24,7 +24,10 @@ import {
} from 'react-reconciler/src/ReactEventPriorities';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {HostText} from 'react-reconciler/src/ReactWorkTags';
import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection';
import {
getInstanceFromHostFiber,
traverseFragmentInstance,
} from 'react-reconciler/src/ReactFiberTreeReflection';
// Modules provided by RN:
import {
@@ -640,7 +643,8 @@ FragmentInstance.prototype.observeUsing = function (
this._observers.add(observer);
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
};
function observeChild(instance: Instance, observer: IntersectionObserver) {
function observeChild(child: Fiber, observer: IntersectionObserver) {
const instance = getInstanceFromHostFiber<Instance>(child);
const publicInstance = getPublicInstance(instance);
if (publicInstance == null) {
throw new Error('Expected to find a host node. This is a bug in React.');
@@ -666,7 +670,8 @@ FragmentInstance.prototype.unobserveUsing = function (
traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer);
}
};
function unobserveChild(instance: Instance, observer: IntersectionObserver) {
function unobserveChild(child: Fiber, observer: IntersectionObserver) {
const instance = getInstanceFromHostFiber<Instance>(child);
const publicInstance = getPublicInstance(instance);
if (publicInstance == null) {
throw new Error('Expected to find a host node. This is a bug in React.');
@@ -690,7 +695,7 @@ export function updateFragmentInstanceFiber(
}
export function commitNewChildToFragmentInstance(
child: Instance,
child: Fiber,
fragmentInstance: FragmentInstanceType,
): void {
if (fragmentInstance._observers !== null) {
+229 -16
View File
@@ -12,7 +12,6 @@ import type {
Container,
ActivityInstance,
SuspenseInstance,
Instance,
} from './ReactFiberConfig';
import type {ActivityState} from './ReactFiberActivityComponent';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
@@ -345,27 +344,42 @@ export function doesFiberContain(
return false;
}
export function traverseFragmentInstance<I, A, B, C>(
export function traverseFragmentInstance<A, B, C>(
fragmentFiber: Fiber,
fn: (I, A, B, C) => boolean,
fn: (Fiber, A, B, C) => boolean,
a: A,
b: B,
c: C,
): void {
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c);
}
function traverseFragmentInstanceChildren<I, A, B, C>(
function traverseVisibleHostChildren<A, B, C>(
child: Fiber | null,
fn: (I, A, B, C) => boolean,
searchWithinHosts: boolean,
fn: (Fiber, A, B, C) => boolean,
a: A,
b: B,
c: C,
): void {
): boolean {
while (child !== null) {
if (child.tag === HostComponent) {
if (fn(child.stateNode, a, b, c)) {
return;
if (fn(child, a, b, c)) {
return true;
}
if (searchWithinHosts) {
if (
traverseVisibleHostChildren(
child.child,
searchWithinHosts,
fn,
a,
b,
c,
)
) {
return true;
}
}
} else if (
child.tag === OffscreenComponent &&
@@ -373,23 +387,222 @@ function traverseFragmentInstanceChildren<I, A, B, C>(
) {
// Skip hidden subtrees
} else {
traverseFragmentInstanceChildren(child.child, fn, a, b, c);
if (
traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c)
) {
return true;
}
}
child = child.sibling;
}
return false;
}
export function getFragmentParentHostInstance(fiber: Fiber): null | Instance {
export function getFragmentParentHostFiber(fiber: Fiber): null | Fiber {
let parent = fiber.return;
while (parent !== null) {
if (parent.tag === HostRoot) {
return parent.stateNode.containerInfo;
}
if (parent.tag === HostComponent) {
return parent.stateNode;
if (parent.tag === HostRoot || parent.tag === HostComponent) {
return parent;
}
parent = parent.return;
}
return null;
}
export function getInstanceFromHostFiber<I>(fiber: Fiber): I {
switch (fiber.tag) {
case HostComponent:
return fiber.stateNode;
case HostRoot:
return fiber.stateNode.containerInfo;
default:
throw new Error('Expected to find a host node. This is a bug in React.');
}
}
let searchTarget = null;
let searchBoundary = null;
function pushSearchTarget(target: null | Fiber): void {
searchTarget = target;
}
function popSearchTarget(): null | Fiber {
return searchTarget;
}
function pushSearchBoundary(value: null | Fiber): void {
searchBoundary = value;
}
function popSearchBoundary(): null | Fiber {
return searchBoundary;
}
export function getNextSiblingHostFiber(fiber: Fiber): null | Fiber {
traverseVisibleHostChildren(fiber.sibling, false, findNextSibling);
const sibling = popSearchTarget();
pushSearchTarget(null);
return sibling;
}
function findNextSibling(child: Fiber): boolean {
pushSearchTarget(child);
return true;
}
export function isFiberContainedBy(
maybeChild: Fiber,
maybeParent: Fiber,
): boolean {
let parent = maybeParent.return;
if (parent === maybeChild || parent === maybeChild.alternate) {
return true;
}
while (parent !== null && parent !== maybeChild) {
if (
(parent.tag === HostComponent || parent.tag === HostRoot) &&
(parent.return === maybeChild || parent.return === maybeChild.alternate)
) {
return true;
}
parent = parent.return;
}
return false;
}
export function isFiberPreceding(fiber: Fiber, otherFiber: Fiber): boolean {
const commonAncestor = getLowestCommonAncestor(
fiber,
otherFiber,
getParentForFragmentAncestors,
);
if (commonAncestor === null) {
return false;
}
traverseVisibleHostChildren(
commonAncestor,
true,
isFiberPrecedingCheck,
otherFiber,
fiber,
);
const target = popSearchTarget();
pushSearchTarget(null);
return target !== null;
}
function isFiberPrecedingCheck(
child: Fiber,
target: Fiber,
boundary: Fiber,
): boolean {
if (child === boundary) {
return true;
}
if (child === target) {
pushSearchTarget(child);
return true;
}
return false;
}
export function isFiberFollowing(fiber: Fiber, otherFiber: Fiber): boolean {
const commonAncestor = getLowestCommonAncestor(
fiber,
otherFiber,
getParentForFragmentAncestors,
);
if (commonAncestor === null) {
return false;
}
traverseVisibleHostChildren(
commonAncestor,
true,
isFiberFollowingCheck,
otherFiber,
fiber,
);
const target = popSearchTarget();
pushSearchTarget(null);
pushSearchBoundary(null);
return target !== null;
}
function isFiberFollowingCheck(
child: Fiber,
target: Fiber,
boundary: Fiber,
): boolean {
if (child === boundary) {
pushSearchBoundary(child);
return false;
}
if (child === target) {
// The target is only following if we already found the boundary.
if (popSearchBoundary() !== null) {
pushSearchTarget(child);
}
return true;
}
return false;
}
function getParentForFragmentAncestors(inst: Fiber | null): Fiber | null {
if (inst === null) {
return null;
}
do {
inst = inst === null ? null : inst.return;
} while (
inst &&
inst.tag !== HostComponent &&
inst.tag !== HostSingleton &&
inst.tag !== HostRoot
);
if (inst) {
return inst;
}
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
export function getLowestCommonAncestor(
instA: Fiber,
instB: Fiber,
getParent: (inst: Fiber | null) => Fiber | null,
): Fiber | null {
let nodeA: null | Fiber = instA;
let nodeB: null | Fiber = instB;
let depthA = 0;
for (let tempA: null | Fiber = nodeA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB: null | Fiber = nodeB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
nodeA = getParent(nodeA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
nodeB = getParent(nodeB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (nodeA === nodeB || (nodeB !== null && nodeA === nodeB.alternate)) {
return nodeA;
}
nodeA = getParent(nodeA);
nodeB = getParent(nodeB);
}
return null;
}