@samitouri / QOS-React-1 / commits / a96a0f3903

Fix fragmentInstance#compareDocumentPosition nesting and portal cases (#34069)

Found a couple of issues while integrating FragmentInstance#compareDocumentPosition into Fabric. 1. Basic checks of nested host instances were inaccurate. For example, checking the first child of the first child of the Fragment would not return CONTAINED_BY. 2. Then fixing that logic exposed issues with Portals. The DOM positioning relied on the assumption that the first and last top-level children were in the same order as the Fiber tree. I added additional checks against the parent's position in the DOM, and special cased a portaled Fragment by getting its DOM parent from the child instance, rather than taking the instance from the Fiber return. This should be accurate in more cases. Though its still a guess and I'm not sure yet I've covered every variation of this. Portals are hard to deal with and we may end up having to push more results towards IMPLEMENTATION_SPECIFIC if accuracy is an issue.

Jack Pope committed Aug 15, 2025 at 12:14 UTC a96a0f3903ea0a9d45ff7c30a3fd9efe830c4628
3 files changed +219 -44
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+56 -16
@@ -38,9 +38,16 @@ import hasOwnProperty from 'shared/hasOwnProperty';
38 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
39 import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
40 import {
41 - isFiberContainedBy,
41 + isFiberContainedByFragment,
42 isFiberFollowing,
43 isFiberPreceding,
44 + isFragmentContainedByFiber,
45 + traverseFragmentInstance,
46 + getFragmentParentHostFiber,
47 + getNextSiblingHostFiber,
48 + getInstanceFromHostFiber,
49 + traverseFragmentInstanceDeeply,
50 + fiberIsPortaledIntoHost,
51 } from 'react-reconciler/src/ReactFiberTreeReflection';
52
53 export {
@@ -63,13 +70,6 @@ import {
70 markNodeAsHoistable,
71 isOwnedInstance,
72 } from './ReactDOMComponentTree';
66 -import {
67 - traverseFragmentInstance,
68 - getFragmentParentHostFiber,
69 - getNextSiblingHostFiber,
70 - getInstanceFromHostFiber,
71 - traverseFragmentInstanceDeeply,
72 -} from 'react-reconciler/src/ReactFiberTreeReflection';
73
74 export {detachDeletedInstance};
75 import {hasRole} from './DOMAccessibilityRoles';
@@ -3052,13 +3052,13 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3052 }
3053 const children: Array<Fiber> = [];
3054 traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
3055 + const parentHostInstance =
3056 + getInstanceFromHostFiber<Instance>(parentHostFiber);
3057
3058 let result = Node.DOCUMENT_POSITION_DISCONNECTED;
3059 if (children.length === 0) {
3060 // If the fragment has no children, we can use the parent and
3061 // siblings to determine a position.
3060 - const parentHostInstance =
3061 - getInstanceFromHostFiber<Instance>(parentHostFiber);
3062 const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
3063 result = parentResult;
3064 if (parentHostInstance === otherNode) {
@@ -3095,15 +3095,53 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3095 const lastElement = getInstanceFromHostFiber<Instance>(
3096 children[children.length - 1],
3097 );
3098 +
3099 + // If the fragment has been portaled into another host instance, we need to
3100 + // our best guess is to use the parent of the child instance, rather than
3101 + // the fiber tree host parent.
3102 + const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
3103 + ? (getInstanceFromHostFiber<Instance>(children[0]).parentElement: ?Instance)
3104 + : parentHostInstance;
3105 +
3106 + if (parentHostInstanceFromDOM == null) {
3107 + return Node.DOCUMENT_POSITION_DISCONNECTED;
3108 + }
3109 +
3110 + // Check if first and last element are actually in the expected document position
3111 + // before relying on them as source of truth for other contained elements
3112 + const firstElementIsContained =
3113 + parentHostInstanceFromDOM.compareDocumentPosition(firstElement) &
3114 + Node.DOCUMENT_POSITION_CONTAINED_BY;
3115 + const lastElementIsContained =
3116 + parentHostInstanceFromDOM.compareDocumentPosition(lastElement) &
3117 + Node.DOCUMENT_POSITION_CONTAINED_BY;
3118 const firstResult = firstElement.compareDocumentPosition(otherNode);
3119 const lastResult = lastElement.compareDocumentPosition(otherNode);
3120 +
3121 + const otherNodeIsFirstOrLastChild =
3122 + (firstElementIsContained && firstElement === otherNode) ||
3123 + (lastElementIsContained && lastElement === otherNode);
3124 + const otherNodeIsFirstOrLastChildDisconnected =
3125 + (!firstElementIsContained && firstElement === otherNode) ||
3126 + (!lastElementIsContained && lastElement === otherNode);
3127 + const otherNodeIsWithinFirstOrLastChild =
3128 + firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
3129 + lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
3130 + const otherNodeIsBetweenFirstAndLastChildren =
3131 + firstElementIsContained &&
3132 + lastElementIsContained &&
3133 + firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
3134 + lastResult & Node.DOCUMENT_POSITION_PRECEDING;
3135 +
3136 if (
3101 - (firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
3102 - lastResult & Node.DOCUMENT_POSITION_PRECEDING) ||
3103 - otherNode === firstElement ||
3104 - otherNode === lastElement
3137 + otherNodeIsFirstOrLastChild ||
3138 + otherNodeIsWithinFirstOrLastChild ||
3139 + otherNodeIsBetweenFirstAndLastChildren
3140 ) {
3141 result = Node.DOCUMENT_POSITION_CONTAINED_BY;
3142 + } else if (otherNodeIsFirstOrLastChildDisconnected) {
3143 + // otherNode has been portaled into another container
3144 + result = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
3145 } else {
3146 result = firstResult;
3147 }
@@ -3141,7 +3179,9 @@ function validateDocumentPositionWithFiberTree(
3179 ): boolean {
3180 const otherFiber = getClosestInstanceFromNode(otherNode);
3181 if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
3144 - return !!otherFiber && isFiberContainedBy(fragmentFiber, otherFiber);
3182 + return (
3183 + !!otherFiber && isFiberContainedByFragment(otherFiber, fragmentFiber)
3184 + );
3185 }
3186 if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
3187 if (otherFiber === null) {
@@ -3149,7 +3189,7 @@ function validateDocumentPositionWithFiberTree(
3189 const ownerDocument = otherNode.ownerDocument;
3190 return otherNode === ownerDocument || otherNode === ownerDocument.body;
3191 }
3152 - return isFiberContainedBy(otherFiber, fragmentFiber);
3192 + return isFragmentContainedByFiber(fragmentFiber, otherFiber);
3193 }
3194 if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) {
3195 return (
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+121 -18
@@ -1197,14 +1197,14 @@ describe('FragmentRefs', () => {
1197
1198 function Test() {
1199 return (
1200 - <div ref={containerRef}>
1201 - <div ref={beforeRef} />
1200 + <div ref={containerRef} id="container">
1201 + <div ref={beforeRef} id="before" />
1202 <React.Fragment ref={fragmentRef}>
1203 - <div ref={firstChildRef} />
1204 - <div ref={middleChildRef} />
1205 - <div ref={lastChildRef} />
1203 + <div ref={firstChildRef} id="first" />
1204 + <div ref={middleChildRef} id="middle" />
1205 + <div ref={lastChildRef} id="last" />
1206 </React.Fragment>
1207 - <div ref={afterRef} />
1207 + <div ref={afterRef} id="after" />
1208 </div>
1209 );
1210 }
@@ -1289,7 +1289,7 @@ describe('FragmentRefs', () => {
1289 },
1290 );
1291
1292 - // containerRef preceds and contains the fragment
1292 + // containerRef precedes and contains the fragment
1293 expectPosition(
1294 fragmentRef.current.compareDocumentPosition(containerRef.current),
1295 {
@@ -1328,7 +1328,7 @@ describe('FragmentRefs', () => {
1328 function Test() {
1329 return (
1330 <div id="container" ref={containerRef}>
1331 - <div>
1331 + <div id="innercontainer">
1332 <div ref={beforeRef} id="before" />
1333 <React.Fragment ref={fragmentRef}>
1334 <div ref={onlyChildRef} id="within" />
@@ -1491,6 +1491,77 @@ describe('FragmentRefs', () => {
1491 );
1492 });
1493
1494 + // @gate enableFragmentRefs
1495 + it('handles nested children', async () => {
1496 + const fragmentRef = React.createRef();
1497 + const nestedFragmentRef = React.createRef();
1498 + const childARef = React.createRef();
1499 + const childBRef = React.createRef();
1500 + const childCRef = React.createRef();
1501 + document.body.appendChild(container);
1502 + const root = ReactDOMClient.createRoot(container);
1503 +
1504 + function Child() {
1505 + return (
1506 + <div ref={childCRef} id="C">
1507 + C
1508 + </div>
1509 + );
1510 + }
1511 +
1512 + function Test() {
1513 + return (
1514 + <React.Fragment ref={fragmentRef}>
1515 + <div ref={childARef} id="A">
1516 + A
1517 + </div>
1518 + <React.Fragment ref={nestedFragmentRef}>
1519 + <div ref={childBRef} id="B">
1520 + B
1521 + </div>
1522 + </React.Fragment>
1523 + <Child />
1524 + </React.Fragment>
1525 + );
1526 + }
1527 +
1528 + await act(() => root.render(<Test />));
1529 +
1530 + expectPosition(
1531 + fragmentRef.current.compareDocumentPosition(childARef.current),
1532 + {
1533 + preceding: false,
1534 + following: false,
1535 + contains: false,
1536 + containedBy: true,
1537 + disconnected: false,
1538 + implementationSpecific: false,
1539 + },
1540 + );
1541 + expectPosition(
1542 + fragmentRef.current.compareDocumentPosition(childBRef.current),
1543 + {
1544 + preceding: false,
1545 + following: false,
1546 + contains: false,
1547 + containedBy: true,
1548 + disconnected: false,
1549 + implementationSpecific: false,
1550 + },
1551 + );
1552 + expectPosition(
1553 + fragmentRef.current.compareDocumentPosition(childCRef.current),
1554 + {
1555 + preceding: false,
1556 + following: false,
1557 + contains: false,
1558 + containedBy: true,
1559 + disconnected: false,
1560 + implementationSpecific: false,
1561 + },
1562 + );
1563 + });
1564 +
1565 // @gate enableFragmentRefs
1566 it('returns disconnected for comparison with an unmounted fragment instance', async () => {
1567 const fragmentRef = React.createRef();
@@ -1551,11 +1622,11 @@ describe('FragmentRefs', () => {
1622
1623 function Test() {
1624 return (
1554 - <div>
1555 - {createPortal(<div ref={portaledSiblingRef} />, document.body)}
1625 + <div id="wrapper">
1626 + {createPortal(<div ref={portaledSiblingRef} id="A" />, container)}
1627 <Fragment ref={fragmentRef}>
1557 - {createPortal(<div ref={portaledChildRef} />, document.body)}
1558 - <div />
1628 + {createPortal(<div ref={portaledChildRef} id="B" />, container)}
1629 + <div id="C" />
1630 </Fragment>
1631 </div>
1632 );
@@ -1600,6 +1671,8 @@ describe('FragmentRefs', () => {
1671 const childARef = React.createRef();
1672 const childBRef = React.createRef();
1673 const childCRef = React.createRef();
1674 + const childDRef = React.createRef();
1675 + const childERef = React.createRef();
1676
1677 function Test() {
1678 const [c, setC] = React.useState(false);
@@ -1612,23 +1685,30 @@ describe('FragmentRefs', () => {
1685 {createPortal(
1686 <Fragment ref={fragmentRef}>
1687 <div id="A" ref={childARef} />
1615 - {c ? <div id="C" ref={childCRef} /> : null}
1688 + {c ? (
1689 + <div id="C" ref={childCRef}>
1690 + <div id="D" ref={childDRef} />
1691 + </div>
1692 + ) : null}
1693 </Fragment>,
1694 document.body,
1695 )}
1696 {createPortal(<p id="B" ref={childBRef} />, document.body)}
1697 + <div id="E" ref={childERef} />
1698 </>
1699 );
1700 }
1701
1702 await act(() => root.render(<Test />));
1703
1626 - // Due to effect, order is A->B->C
1627 - expect(document.body.innerHTML).toBe(
1628 - '<div></div>' +
1704 + // Due to effect, order is E / A->B->C->D
1705 + expect(document.body.outerHTML).toBe(
1706 + '<body>' +
1707 + '<div><div id="E"></div></div>' +
1708 '<div id="A"></div>' +
1709 '<p id="B"></p>' +
1631 - '<div id="C"></div>',
1710 + '<div id="C"><div id="D"></div></div>' +
1711 + '</body>',
1712 );
1713
1714 expectPosition(
@@ -1642,7 +1722,6 @@ describe('FragmentRefs', () => {
1722 implementationSpecific: false,
1723 },
1724 );
1645 -
1725 expectPosition(
1726 fragmentRef.current.compareDocumentPosition(childARef.current),
1727 {
@@ -1654,6 +1733,7 @@ describe('FragmentRefs', () => {
1733 implementationSpecific: false,
1734 },
1735 );
1736 + // Contained by in DOM, but following in React tree
1737 expectPosition(
1738 fragmentRef.current.compareDocumentPosition(childBRef.current),
1739 {
@@ -1676,6 +1756,29 @@ describe('FragmentRefs', () => {
1756 implementationSpecific: false,
1757 },
1758 );
1759 + expectPosition(
1760 + fragmentRef.current.compareDocumentPosition(childDRef.current),
1761 + {
1762 + preceding: false,
1763 + following: false,
1764 + contains: false,
1765 + containedBy: true,
1766 + disconnected: false,
1767 + implementationSpecific: false,
1768 + },
1769 + );
1770 + // Preceding DOM but following in React tree
1771 + expectPosition(
1772 + fragmentRef.current.compareDocumentPosition(childERef.current),
1773 + {
1774 + preceding: false,
1775 + following: false,
1776 + contains: false,
1777 + containedBy: false,
1778 + disconnected: false,
1779 + implementationSpecific: true,
1780 + },
1781 + );
1782 });
1783
1784 // @gate enableFragmentRefs
packages/react-reconciler/src/ReactFiberTreeReflection.js
+42 -10
@@ -26,6 +26,7 @@ import {
26 ActivityComponent,
27 SuspenseComponent,
28 OffscreenComponent,
29 + Fragment,
30 } from './ReactWorkTags';
31 import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
32
@@ -405,6 +406,21 @@ export function getFragmentParentHostFiber(fiber: Fiber): null | Fiber {
406 return null;
407 }
408
409 +export function fiberIsPortaledIntoHost(fiber: Fiber): boolean {
410 + let foundPortalParent = false;
411 + let parent = fiber.return;
412 + while (parent !== null) {
413 + if (parent.tag === HostPortal) {
414 + foundPortalParent = true;
415 + }
416 + if (parent.tag === HostRoot || parent.tag === HostComponent) {
417 + break;
418 + }
419 + parent = parent.return;
420 + }
421 + return foundPortalParent;
422 +}
423 +
424 export function getInstanceFromHostFiber<I>(fiber: Fiber): I {
425 switch (fiber.tag) {
426 case HostComponent:
@@ -443,22 +459,38 @@ function findNextSibling(child: Fiber): boolean {
459 return true;
460 }
461
446 -export function isFiberContainedBy(
447 - maybeChild: Fiber,
448 - maybeParent: Fiber,
462 +export function isFiberContainedByFragment(
463 + fiber: Fiber,
464 + fragmentFiber: Fiber,
465 ): boolean {
450 - let parent = maybeParent.return;
451 - if (parent === maybeChild || parent === maybeChild.alternate) {
452 - return true;
466 + let current: Fiber | null = fiber;
467 + while (current !== null) {
468 + if (
469 + current.tag === Fragment &&
470 + (current === fragmentFiber || current.alternate === fragmentFiber)
471 + ) {
472 + return true;
473 + }
474 + current = current.return;
475 }
454 - while (parent !== null && parent !== maybeChild) {
476 + return false;
477 +}
478 +
479 +export function isFragmentContainedByFiber(
480 + fragmentFiber: Fiber,
481 + otherFiber: Fiber,
482 +): boolean {
483 + let current: Fiber | null = fragmentFiber;
484 + const fiberHostParent: Fiber | null =
485 + getFragmentParentHostFiber(fragmentFiber);
486 + while (current !== null) {
487 if (
456 - (parent.tag === HostComponent || parent.tag === HostRoot) &&
457 - (parent.return === maybeChild || parent.return === maybeChild.alternate)
488 + (current.tag === HostComponent || current.tag === HostRoot) &&
489 + (current === fiberHostParent || current.alternate === fiberHostParent)
490 ) {
491 return true;
492 }
461 - parent = parent.return;
493 + current = current.return;
494 }
495 return false;
496 }