mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
+171
-24
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user