@samitouri / QOS-React / commits / 45a6532a08

Add compareDocumentPosition to Fabric FragmentInstance (#34103)

Stacked on https://github.com/facebook/react/pull/34069 Same basic semantics as the react-dom for determining document position of a Fragment compared to a given node. It's simpler here because we don't have to deal with inserted nodes or portals. So we can skip a bunch of the validation logic. The logic for handling empty fragments is the same so I've split out `compareDocumentPositionForEmptyFragment` into a shared module. There doesn't seem to be a great place to put shared DOM logic between Fabric and DOM configs at the moment. There may be more of this coming as we add more and more DOM APIs to RN. For testing I've written Fantom tests internally which pass the basic cases on this build. The renderer we have configured for Fabric tests in the repo doesn't support the Element APIs we need like `compareDocumentPosition`.

Jack Pope committed Aug 15, 2025 at 15:07 UTC 45a6532a088432010c40cbcac1fb6b6f8d56dd69
3 files changed +145 -48
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+10 -35
@@ -44,7 +44,6 @@ import {
44 isFragmentContainedByFiber,
45 traverseFragmentInstance,
46 getFragmentParentHostFiber,
47 - getNextSiblingHostFiber,
47 getInstanceFromHostFiber,
48 traverseFragmentInstanceDeeply,
49 fiberIsPortaledIntoHost,
@@ -70,6 +69,7 @@ import {
69 markNodeAsHoistable,
70 isOwnedInstance,
71 } from './ReactDOMComponentTree';
72 +import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
73
74 export {detachDeletedInstance};
75 import {hasRole} from './DOMAccessibilityRoles';
@@ -3055,40 +3055,13 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3055 const parentHostInstance =
3056 getInstanceFromHostFiber<Instance>(parentHostFiber);
3057
3058 - let result = Node.DOCUMENT_POSITION_DISCONNECTED;
3058 if (children.length === 0) {
3060 - // If the fragment has no children, we can use the parent and
3061 - // siblings to determine a position.
3062 - const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
3063 - result = parentResult;
3064 - if (parentHostInstance === otherNode) {
3065 - result = Node.DOCUMENT_POSITION_CONTAINS;
3066 - } else {
3067 - if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
3068 - // otherNode is one of the fragment's siblings. Use the next
3069 - // sibling to determine if its preceding or following.
3070 - const nextSiblingFiber = getNextSiblingHostFiber(this._fragmentFiber);
3071 - if (nextSiblingFiber === null) {
3072 - result = Node.DOCUMENT_POSITION_PRECEDING;
3073 - } else {
3074 - const nextSiblingInstance =
3075 - getInstanceFromHostFiber<Instance>(nextSiblingFiber);
3076 - const nextSiblingResult =
3077 - nextSiblingInstance.compareDocumentPosition(otherNode);
3078 - if (
3079 - nextSiblingResult === 0 ||
3080 - nextSiblingResult & Node.DOCUMENT_POSITION_FOLLOWING
3081 - ) {
3082 - result = Node.DOCUMENT_POSITION_FOLLOWING;
3083 - } else {
3084 - result = Node.DOCUMENT_POSITION_PRECEDING;
3085 - }
3086 - }
3087 - }
3088 - }
3089 -
3090 - result |= Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
3091 - return result;
3059 + return compareDocumentPositionForEmptyFragment(
3060 + this._fragmentFiber,
3061 + parentHostInstance,
3062 + otherNode,
3063 + getInstanceFromHostFiber,
3064 + );
3065 }
3066
3067 const firstElement = getInstanceFromHostFiber<Instance>(children[0]);
@@ -3099,8 +3072,9 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3072 // If the fragment has been portaled into another host instance, we need to
3073 // our best guess is to use the parent of the child instance, rather than
3074 // the fiber tree host parent.
3075 + const firstInstance = getInstanceFromHostFiber<Instance>(children[0]);
3076 const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
3103 - ? (getInstanceFromHostFiber<Instance>(children[0]).parentElement: ?Instance)
3077 + ? (firstInstance.parentElement: ?Instance)
3078 : parentHostInstance;
3079
3080 if (parentHostInstanceFromDOM == null) {
@@ -3133,6 +3107,7 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3107 firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
3108 lastResult & Node.DOCUMENT_POSITION_PRECEDING;
3109
3110 + let result = Node.DOCUMENT_POSITION_DISCONNECTED;
3111 if (
3112 otherNodeIsFirstOrLastChild ||
3113 otherNodeIsWithinFirstOrLastChild ||
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+77 -13
@@ -24,6 +24,7 @@ import {
24 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
25 import {HostText} from 'react-reconciler/src/ReactWorkTags';
26 import {
27 + getFragmentParentHostFiber,
28 getInstanceFromHostFiber,
29 traverseFragmentInstance,
30 } from 'react-reconciler/src/ReactFiberTreeReflection';
@@ -59,6 +60,7 @@ const {
60 } = nativeFabricUIManager;
61
62 import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
63 +import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
64
65 import {
66 getInspectorDataForViewTag,
@@ -87,7 +89,7 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
89 let nextReactTag = 2;
90
91 type InternalInstanceHandle = Object;
90 -type Node = Object;
92 +
93 export type Type = string;
94 export type Props = Object;
95 export type Instance = {
@@ -344,6 +346,15 @@ export function getPublicInstanceFromInternalInstanceHandle(
346 return getPublicInstance(elementInstance);
347 }
348
349 +function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance {
350 + const instance = getInstanceFromHostFiber<Instance>(fiber);
351 + const publicInstance = getPublicInstance(instance);
352 + if (publicInstance == null) {
353 + throw new Error('Expected to find a host node. This is a bug in React.');
354 + }
355 + return publicInstance;
356 +}
357 +
358 export function prepareForCommit(containerInfo: Container): null | Object {
359 // Noop
360 return null;
@@ -610,6 +621,7 @@ export type FragmentInstanceType = {
621 _observers: null | Set<IntersectionObserver>,
622 observeUsing: (observer: IntersectionObserver) => void,
623 unobserveUsing: (observer: IntersectionObserver) => void,
624 + compareDocumentPosition: (otherNode: PublicInstance) => number,
625 };
626
627 function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -629,12 +641,8 @@ FragmentInstance.prototype.observeUsing = function (
641 traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
642 };
643 function observeChild(child: Fiber, observer: IntersectionObserver) {
632 - const instance = getInstanceFromHostFiber<Instance>(child);
633 - const publicInstance = getPublicInstance(instance);
634 - if (publicInstance == null) {
635 - throw new Error('Expected to find a host node. This is a bug in React.');
636 - }
637 - // $FlowFixMe[incompatible-call] Element types are behind a flag in RN
644 + const publicInstance = getPublicInstanceFromHostFiber(child);
645 + // $FlowFixMe[incompatible-call] DOM types expect Element
646 observer.observe(publicInstance);
647 return false;
648 }
@@ -656,16 +664,72 @@ FragmentInstance.prototype.unobserveUsing = function (
664 }
665 };
666 function unobserveChild(child: Fiber, observer: IntersectionObserver) {
659 - const instance = getInstanceFromHostFiber<Instance>(child);
660 - const publicInstance = getPublicInstance(instance);
661 - if (publicInstance == null) {
662 - throw new Error('Expected to find a host node. This is a bug in React.');
663 - }
664 - // $FlowFixMe[incompatible-call] Element types are behind a flag in RN
667 + const publicInstance = getPublicInstanceFromHostFiber(child);
668 + // $FlowFixMe[incompatible-call] DOM types expect Element
669 observer.unobserve(publicInstance);
670 return false;
671 }
672
673 +// $FlowFixMe[prop-missing]
674 +FragmentInstance.prototype.compareDocumentPosition = function (
675 + this: FragmentInstanceType,
676 + otherNode: PublicInstance,
677 +): number {
678 + const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
679 + if (parentHostFiber === null) {
680 + return Node.DOCUMENT_POSITION_DISCONNECTED;
681 + }
682 + const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
683 + const children: Array<Fiber> = [];
684 + traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
685 + if (children.length === 0) {
686 + return compareDocumentPositionForEmptyFragment(
687 + this._fragmentFiber,
688 + parentHostInstance,
689 + otherNode,
690 + getPublicInstanceFromHostFiber,
691 + );
692 + }
693 +
694 + const firstInstance = getPublicInstanceFromHostFiber(children[0]);
695 + const lastInstance = getPublicInstanceFromHostFiber(
696 + children[children.length - 1],
697 + );
698 +
699 + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
700 + // $FlowFixMe[prop-missing]
701 + const firstResult = firstInstance.compareDocumentPosition(otherNode);
702 + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
703 + // $FlowFixMe[prop-missing]
704 + const lastResult = lastInstance.compareDocumentPosition(otherNode);
705 +
706 + const otherNodeIsFirstOrLastChild =
707 + firstInstance === otherNode || lastInstance === otherNode;
708 + const otherNodeIsWithinFirstOrLastChild =
709 + firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
710 + lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
711 + const otherNodeIsBetweenFirstAndLastChildren =
712 + firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
713 + lastResult & Node.DOCUMENT_POSITION_PRECEDING;
714 + let result;
715 + if (
716 + otherNodeIsFirstOrLastChild ||
717 + otherNodeIsWithinFirstOrLastChild ||
718 + otherNodeIsBetweenFirstAndLastChildren
719 + ) {
720 + result = Node.DOCUMENT_POSITION_CONTAINED_BY;
721 + } else {
722 + result = firstResult;
723 + }
724 +
725 + return result;
726 +};
727 +
728 +function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
729 + collection.push(child);
730 + return false;
731 +}
732 +
733 export function createFragmentInstance(
734 fragmentFiber: Fiber,
735 ): FragmentInstanceType {
packages/shared/ReactDOMFragmentRefShared.js new
+58
@@ -0,0 +1,58 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * Shared logic for Fragment Ref operations for DOM and Fabric configs
8 + *
9 + * @flow
10 + */
11 +
12 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
13 +
14 +import {getNextSiblingHostFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
15 +
16 +export function compareDocumentPositionForEmptyFragment<TPublicInstance>(
17 + fragmentFiber: Fiber,
18 + parentHostInstance: TPublicInstance,
19 + otherNode: TPublicInstance,
20 + getPublicInstance: (fiber: Fiber) => TPublicInstance,
21 +): number {
22 + let result;
23 + // If the fragment has no children, we can use the parent and
24 + // siblings to determine a position.
25 + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
26 + // $FlowFixMe[prop-missing]
27 + const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
28 + result = parentResult;
29 + if (parentHostInstance === otherNode) {
30 + result = Node.DOCUMENT_POSITION_CONTAINS;
31 + } else {
32 + if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
33 + // otherNode is one of the fragment's siblings. Use the next
34 + // sibling to determine if its preceding or following.
35 + const nextSiblingFiber = getNextSiblingHostFiber(fragmentFiber);
36 + if (nextSiblingFiber === null) {
37 + result = Node.DOCUMENT_POSITION_PRECEDING;
38 + } else {
39 + const nextSiblingInstance = getPublicInstance(nextSiblingFiber);
40 + const nextSiblingResult =
41 + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
42 + // $FlowFixMe[prop-missing]
43 + nextSiblingInstance.compareDocumentPosition(otherNode);
44 + if (
45 + nextSiblingResult === 0 ||
46 + nextSiblingResult & Node.DOCUMENT_POSITION_FOLLOWING
47 + ) {
48 + result = Node.DOCUMENT_POSITION_FOLLOWING;
49 + } else {
50 + result = Node.DOCUMENT_POSITION_PRECEDING;
51 + }
52 + }
53 + }
54 + }
55 +
56 + result |= Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
57 + return result;
58 +}