Add getRootNode to fragment instances (#32682)

This implements `getRootNode(options)` on fragment instances as the
equivalent of calling `getRootNode` on the fragment's parent host node.

The parent host instance will also be used to proxy dispatchEvent in an
upcoming PR.
This commit is contained in:
Jack Pope
2025-03-24 10:19:55 -04:00
committed by GitHub
parent 0f66ac6811
commit da1aabeb42
4 changed files with 125 additions and 4 deletions
@@ -53,7 +53,10 @@ import {
markNodeAsHoistable,
isOwnedInstance,
} from './ReactDOMComponentTree';
import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection';
import {
traverseFragmentInstance,
getFragmentParentHostInstance,
} from 'react-reconciler/src/ReactFiberTreeReflection';
export {detachDeletedInstance};
import {hasRole} from './DOMAccessibilityRoles';
@@ -2239,6 +2242,9 @@ export type FragmentInstanceType = {
observeUsing(observer: IntersectionObserver | ResizeObserver): void,
unobserveUsing(observer: IntersectionObserver | ResizeObserver): void,
getClientRects(): Array<DOMRect>,
getRootNode(getRootNodeOptions?: {
composed: boolean,
}): Document | ShadowRoot | FragmentInstanceType,
};
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -2338,7 +2344,7 @@ FragmentInstance.prototype.focus = function (
FragmentInstance.prototype.focusLast = function (
this: FragmentInstanceType,
focusOptions?: FocusOptions,
) {
): void {
const children: Array<Instance> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
for (let i = children.length - 1; i >= 0; i--) {
@@ -2429,6 +2435,20 @@ function collectClientRects(child: Instance, rects: Array<DOMRect>): boolean {
rects.push.apply(rects, child.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) {
return this;
}
const rootNode =
// $FlowFixMe[incompatible-cast] Flow expects Node
(parentHostInstance.getRootNode(getRootNodeOptions): Document | ShadowRoot);
return rootNode;
};
function normalizeListenerOptions(
opts: ?EventListenerOptionsOrUseCapture,
@@ -846,7 +846,7 @@ describe('FragmentRefs', () => {
describe('getClientRects', () => {
// @gate enableFragmentRefs
it('returns the bounding client recs of all children', async () => {
it('returns the bounding client rects of all children', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
const childBRef = React.createRef();
@@ -884,4 +884,86 @@ describe('FragmentRefs', () => {
expect(clientRects[2].left).toBe(9);
});
});
describe('getRootNode', () => {
// @gate enableFragmentRefs
it('returns the root node of the parent', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<div>
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
</div>
);
}
await act(() => root.render(<Test />));
expect(fragmentRef.current.getRootNode()).toBe(document);
});
// The desired behavior here is to return the topmost disconnected element when
// fragment + parent are unmounted. Currently we have a pass during unmount that
// recursively cleans up return pointers of the whole tree. We can change this
// with a future refactor. See: https://github.com/facebook/react/pull/32682#discussion_r2008313082
// @gate enableFragmentRefs
it('returns the topmost disconnected element if the fragment and parent are unmounted', async () => {
const containerRef = React.createRef();
const parentRef = React.createRef();
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mounted}) {
return (
<div ref={containerRef} id="container">
{mounted && (
<div ref={parentRef} id="parent">
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
</div>
)}
</div>
);
}
await act(() => root.render(<Test mounted={true} />));
expect(fragmentRef.current.getRootNode()).toBe(document);
const fragmentHandle = fragmentRef.current;
await act(() => root.render(<Test mounted={false} />));
// TODO: The commented out assertion is the desired behavior. For now, we return
// the fragment instance itself. This is currently the same behavior if you unmount
// the fragment but not the parent. See context above.
// expect(fragmentHandle.getRootNode().id).toBe(parentRefHandle.id);
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
});
// @gate enableFragmentRefs
it('returns self when only the fragment was unmounted', async () => {
const fragmentRef = React.createRef();
const parentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test({mounted}) {
return (
<div ref={parentRef} id="parent">
{mounted && (
<React.Fragment ref={fragmentRef}>
<div />
</React.Fragment>
)}
</div>
);
}
await act(() => root.render(<Test mounted={true} />));
expect(fragmentRef.current.getRootNode()).toBe(document);
const fragmentHandle = fragmentRef.current;
await act(() => root.render(<Test mounted={false} />));
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
});
});
});
+5 -1
View File
@@ -512,10 +512,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
throw new Error('Not yet implemented.');
},
createFragmentInstance(parentInstance) {
createFragmentInstance(fragmentFiber) {
return null;
},
updateFragmentInstanceFiber(fragmentFiber, fragmentInstance) {
// Noop
},
commitNewChildToFragmentInstance(child, fragmentInstance) {
// Noop
},
@@ -352,3 +352,18 @@ function traverseFragmentInstanceChildren<A, B, C>(
child = child.sibling;
}
}
export function getFragmentParentHostInstance(fiber: Fiber): null | Instance {
let parent = fiber.return;
while (parent !== null) {
if (parent.tag === HostRoot) {
return parent.stateNode.containerInfo;
}
if (parent.tag === HostComponent) {
return parent.stateNode;
}
parent = parent.return;
}
return null;
}