Skip to content

Add compareDocumentPosition to fragment instances #32722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 171 additions & 24 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -60,7 +65,9 @@ import {
} from './ReactDOMComponentTree';
import {
traverseFragmentInstance,
getFragmentParentHostInstance,
getFragmentParentHostFiber,
getNextSiblingHostFiber,
getInstanceFromHostFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';

export {detachDeletedInstance};
Expand Down Expand Up @@ -2599,6 +2606,7 @@ export type FragmentInstanceType = {
getRootNode(getRootNodeOptions?: {
composed: boolean,
}): Document | ShadowRoot | FragmentInstanceType,
compareDocumentPosition(otherNode: Instance): number,
};

function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -2784,25 +2801,155 @@ 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]
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated support for empty fragments to compare against parent/siblings

// 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,
Expand Down
44 changes: 3 additions & 41 deletions packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
HostText,
ScopeComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection';

import getEventTarget from './getEventTarget';
import {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading