From 500e17271095656813a61116c5a6bf82d4dc6050 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 1 Aug 2025 16:44:54 -0400 Subject: [PATCH] Add compareDocumentPosition to FragmentInstance on Fabric --- .../src/client/ReactFiberConfigDOM.js | 45 +++------- .../src/ReactFiberConfigFabric.js | 90 ++++++++++++++++--- packages/shared/ReactDOMFragmentRefShared.js | 58 ++++++++++++ 3 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 packages/shared/ReactDOMFragmentRefShared.js diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 03a6104e68d..568e892ad81 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -44,7 +44,6 @@ import { isFragmentContainedByFiber, traverseFragmentInstance, getFragmentParentHostFiber, - getNextSiblingHostFiber, getInstanceFromHostFiber, traverseFragmentInstanceDeeply, fiberIsPortaledIntoHost, @@ -70,6 +69,7 @@ import { markNodeAsHoistable, isOwnedInstance, } from './ReactDOMComponentTree'; +import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared'; export {detachDeletedInstance}; import {hasRole} from './DOMAccessibilityRoles'; @@ -3055,40 +3055,13 @@ FragmentInstance.prototype.compareDocumentPosition = function ( const parentHostInstance = getInstanceFromHostFiber(parentHostFiber); - 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 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(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; + return compareDocumentPositionForEmptyFragment( + this._fragmentFiber, + parentHostInstance, + otherNode, + getInstanceFromHostFiber, + ); } const firstElement = getInstanceFromHostFiber(children[0]); @@ -3099,8 +3072,9 @@ FragmentInstance.prototype.compareDocumentPosition = function ( // If the fragment has been portaled into another host instance, we need to // our best guess is to use the parent of the child instance, rather than // the fiber tree host parent. + const firstInstance = getInstanceFromHostFiber(children[0]); const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber) - ? (getInstanceFromHostFiber(children[0]).parentElement: ?Instance) + ? (firstInstance.parentElement: ?Instance) : parentHostInstance; if (parentHostInstanceFromDOM == null) { @@ -3133,6 +3107,7 @@ FragmentInstance.prototype.compareDocumentPosition = function ( firstResult & Node.DOCUMENT_POSITION_FOLLOWING && lastResult & Node.DOCUMENT_POSITION_PRECEDING; + let result = Node.DOCUMENT_POSITION_DISCONNECTED; if ( otherNodeIsFirstOrLastChild || otherNodeIsWithinFirstOrLastChild || diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 82745e0a997..3f2f0d192a8 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -24,6 +24,7 @@ import { import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import {HostText} from 'react-reconciler/src/ReactWorkTags'; import { + getFragmentParentHostFiber, getInstanceFromHostFiber, traverseFragmentInstance, } from 'react-reconciler/src/ReactFiberTreeReflection'; @@ -59,6 +60,7 @@ const { } = nativeFabricUIManager; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; +import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared'; import { getInspectorDataForViewTag, @@ -87,7 +89,7 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry; let nextReactTag = 2; type InternalInstanceHandle = Object; -type Node = Object; + export type Type = string; export type Props = Object; export type Instance = { @@ -344,6 +346,15 @@ export function getPublicInstanceFromInternalInstanceHandle( return getPublicInstance(elementInstance); } +function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance { + const instance = getInstanceFromHostFiber(fiber); + const publicInstance = getPublicInstance(instance); + if (publicInstance == null) { + throw new Error('Expected to find a host node. This is a bug in React.'); + } + return publicInstance; +} + export function prepareForCommit(containerInfo: Container): null | Object { // Noop return null; @@ -610,6 +621,7 @@ export type FragmentInstanceType = { _observers: null | Set, observeUsing: (observer: IntersectionObserver) => void, unobserveUsing: (observer: IntersectionObserver) => void, + compareDocumentPosition: (otherNode: PublicInstance) => number, }; function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { @@ -629,12 +641,8 @@ FragmentInstance.prototype.observeUsing = function ( traverseFragmentInstance(this._fragmentFiber, observeChild, observer); }; function observeChild(child: Fiber, observer: IntersectionObserver) { - const instance = getInstanceFromHostFiber(child); - const publicInstance = getPublicInstance(instance); - if (publicInstance == null) { - throw new Error('Expected to find a host node. This is a bug in React.'); - } - // $FlowFixMe[incompatible-call] Element types are behind a flag in RN + const publicInstance = getPublicInstanceFromHostFiber(child); + // $FlowFixMe[incompatible-call] DOM types expect Element observer.observe(publicInstance); return false; } @@ -656,16 +664,72 @@ FragmentInstance.prototype.unobserveUsing = function ( } }; function unobserveChild(child: Fiber, observer: IntersectionObserver) { - const instance = getInstanceFromHostFiber(child); - const publicInstance = getPublicInstance(instance); - if (publicInstance == null) { - throw new Error('Expected to find a host node. This is a bug in React.'); - } - // $FlowFixMe[incompatible-call] Element types are behind a flag in RN + const publicInstance = getPublicInstanceFromHostFiber(child); + // $FlowFixMe[incompatible-call] DOM types expect Element observer.unobserve(publicInstance); return false; } +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.compareDocumentPosition = function ( + this: FragmentInstanceType, + otherNode: PublicInstance, +): number { + const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); + if (parentHostFiber === null) { + return Node.DOCUMENT_POSITION_DISCONNECTED; + } + const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber); + const children: Array = []; + traverseFragmentInstance(this._fragmentFiber, collectChildren, children); + if (children.length === 0) { + return compareDocumentPositionForEmptyFragment( + this._fragmentFiber, + parentHostInstance, + otherNode, + getPublicInstanceFromHostFiber, + ); + } + + const firstInstance = getPublicInstanceFromHostFiber(children[0]); + const lastInstance = getPublicInstanceFromHostFiber( + children[children.length - 1], + ); + + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque + // $FlowFixMe[prop-missing] + const firstResult = firstInstance.compareDocumentPosition(otherNode); + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque + // $FlowFixMe[prop-missing] + const lastResult = lastInstance.compareDocumentPosition(otherNode); + + const otherNodeIsFirstOrLastChild = + firstInstance === otherNode || lastInstance === otherNode; + const otherNodeIsWithinFirstOrLastChild = + firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY || + lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY; + const otherNodeIsBetweenFirstAndLastChildren = + firstResult & Node.DOCUMENT_POSITION_FOLLOWING && + lastResult & Node.DOCUMENT_POSITION_PRECEDING; + let result; + if ( + otherNodeIsFirstOrLastChild || + otherNodeIsWithinFirstOrLastChild || + otherNodeIsBetweenFirstAndLastChildren + ) { + result = Node.DOCUMENT_POSITION_CONTAINED_BY; + } else { + result = firstResult; + } + + return result; +}; + +function collectChildren(child: Fiber, collection: Array): boolean { + collection.push(child); + return false; +} + export function createFragmentInstance( fragmentFiber: Fiber, ): FragmentInstanceType { diff --git a/packages/shared/ReactDOMFragmentRefShared.js b/packages/shared/ReactDOMFragmentRefShared.js new file mode 100644 index 00000000000..c46d67d4114 --- /dev/null +++ b/packages/shared/ReactDOMFragmentRefShared.js @@ -0,0 +1,58 @@ +/** + * 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. + * + * Shared logic for Fragment Ref operations for DOM and Fabric configs + * + * @flow + */ + +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; + +import {getNextSiblingHostFiber} from 'react-reconciler/src/ReactFiberTreeReflection'; + +export function compareDocumentPositionForEmptyFragment( + fragmentFiber: Fiber, + parentHostInstance: TPublicInstance, + otherNode: TPublicInstance, + getPublicInstance: (fiber: Fiber) => TPublicInstance, +): number { + let result; + // If the fragment has no children, we can use the parent and + // siblings to determine a position. + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque + // $FlowFixMe[prop-missing] + 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(fragmentFiber); + if (nextSiblingFiber === null) { + result = Node.DOCUMENT_POSITION_PRECEDING; + } else { + const nextSiblingInstance = getPublicInstance(nextSiblingFiber); + const nextSiblingResult = + // $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque + // $FlowFixMe[prop-missing] + 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; +}