Skip to content

Commit 45a6532

Browse files
authored
Add compareDocumentPosition to Fabric FragmentInstance (#34103)
Stacked on #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`.
1 parent 8dba931 commit 45a6532

3 files changed

Lines changed: 145 additions & 48 deletions

File tree

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {
4444
isFragmentContainedByFiber,
4545
traverseFragmentInstance,
4646
getFragmentParentHostFiber,
47-
getNextSiblingHostFiber,
4847
getInstanceFromHostFiber,
4948
traverseFragmentInstanceDeeply,
5049
fiberIsPortaledIntoHost,
@@ -70,6 +69,7 @@ import {
7069
markNodeAsHoistable,
7170
isOwnedInstance,
7271
} from './ReactDOMComponentTree';
72+
import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
7373

7474
export {detachDeletedInstance};
7575
import {hasRole} from './DOMAccessibilityRoles';
@@ -3055,40 +3055,13 @@ FragmentInstance.prototype.compareDocumentPosition = function (
30553055
const parentHostInstance =
30563056
getInstanceFromHostFiber<Instance>(parentHostFiber);
30573057

3058-
let result = Node.DOCUMENT_POSITION_DISCONNECTED;
30593058
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+
);
30923065
}
30933066

30943067
const firstElement = getInstanceFromHostFiber<Instance>(children[0]);
@@ -3099,8 +3072,9 @@ FragmentInstance.prototype.compareDocumentPosition = function (
30993072
// If the fragment has been portaled into another host instance, we need to
31003073
// our best guess is to use the parent of the child instance, rather than
31013074
// the fiber tree host parent.
3075+
const firstInstance = getInstanceFromHostFiber<Instance>(children[0]);
31023076
const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
3103-
? (getInstanceFromHostFiber<Instance>(children[0]).parentElement: ?Instance)
3077+
? (firstInstance.parentElement: ?Instance)
31043078
: parentHostInstance;
31053079

31063080
if (parentHostInstanceFromDOM == null) {
@@ -3133,6 +3107,7 @@ FragmentInstance.prototype.compareDocumentPosition = function (
31333107
firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
31343108
lastResult & Node.DOCUMENT_POSITION_PRECEDING;
31353109

3110+
let result = Node.DOCUMENT_POSITION_DISCONNECTED;
31363111
if (
31373112
otherNodeIsFirstOrLastChild ||
31383113
otherNodeIsWithinFirstOrLastChild ||

packages/react-native-renderer/src/ReactFiberConfigFabric.js

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
2525
import {HostText} from 'react-reconciler/src/ReactWorkTags';
2626
import {
27+
getFragmentParentHostFiber,
2728
getInstanceFromHostFiber,
2829
traverseFragmentInstance,
2930
} from 'react-reconciler/src/ReactFiberTreeReflection';
@@ -59,6 +60,7 @@ const {
5960
} = nativeFabricUIManager;
6061

6162
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
63+
import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
6264

6365
import {
6466
getInspectorDataForViewTag,
@@ -87,7 +89,7 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
8789
let nextReactTag = 2;
8890

8991
type InternalInstanceHandle = Object;
90-
type Node = Object;
92+
9193
export type Type = string;
9294
export type Props = Object;
9395
export type Instance = {
@@ -344,6 +346,15 @@ export function getPublicInstanceFromInternalInstanceHandle(
344346
return getPublicInstance(elementInstance);
345347
}
346348

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+
347358
export function prepareForCommit(containerInfo: Container): null | Object {
348359
// Noop
349360
return null;
@@ -610,6 +621,7 @@ export type FragmentInstanceType = {
610621
_observers: null | Set<IntersectionObserver>,
611622
observeUsing: (observer: IntersectionObserver) => void,
612623
unobserveUsing: (observer: IntersectionObserver) => void,
624+
compareDocumentPosition: (otherNode: PublicInstance) => number,
613625
};
614626

615627
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -629,12 +641,8 @@ FragmentInstance.prototype.observeUsing = function (
629641
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
630642
};
631643
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
638646
observer.observe(publicInstance);
639647
return false;
640648
}
@@ -656,16 +664,72 @@ FragmentInstance.prototype.unobserveUsing = function (
656664
}
657665
};
658666
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
665669
observer.unobserve(publicInstance);
666670
return false;
667671
}
668672

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+
669733
export function createFragmentInstance(
670734
fragmentFiber: Fiber,
671735
): FragmentInstanceType {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)