Skip to content

Commit 04bf10e

Browse files
authored
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.
1 parent c61e75b commit 04bf10e

File tree

4 files changed

+125
-4
lines changed

4 files changed

+125
-4
lines changed

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ import {
5353
markNodeAsHoistable,
5454
isOwnedInstance,
5555
} from './ReactDOMComponentTree';
56-
import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection';
56+
import {
57+
traverseFragmentInstance,
58+
getFragmentParentHostInstance,
59+
} from 'react-reconciler/src/ReactFiberTreeReflection';
5760

5861
export {detachDeletedInstance};
5962
import {hasRole} from './DOMAccessibilityRoles';
@@ -2239,6 +2242,9 @@ export type FragmentInstanceType = {
22392242
observeUsing(observer: IntersectionObserver | ResizeObserver): void,
22402243
unobserveUsing(observer: IntersectionObserver | ResizeObserver): void,
22412244
getClientRects(): Array<DOMRect>,
2245+
getRootNode(getRootNodeOptions?: {
2246+
composed: boolean,
2247+
}): Document | ShadowRoot | FragmentInstanceType,
22422248
};
22432249

22442250
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -2338,7 +2344,7 @@ FragmentInstance.prototype.focus = function (
23382344
FragmentInstance.prototype.focusLast = function (
23392345
this: FragmentInstanceType,
23402346
focusOptions?: FocusOptions,
2341-
) {
2347+
): void {
23422348
const children: Array<Instance> = [];
23432349
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
23442350
for (let i = children.length - 1; i >= 0; i--) {
@@ -2429,6 +2435,20 @@ function collectClientRects(child: Instance, rects: Array<DOMRect>): boolean {
24292435
rects.push.apply(rects, child.getClientRects());
24302436
return false;
24312437
}
2438+
// $FlowFixMe[prop-missing]
2439+
FragmentInstance.prototype.getRootNode = function (
2440+
this: FragmentInstanceType,
2441+
getRootNodeOptions?: {composed: boolean},
2442+
): Document | ShadowRoot | FragmentInstanceType {
2443+
const parentHostInstance = getFragmentParentHostInstance(this._fragmentFiber);
2444+
if (parentHostInstance === null) {
2445+
return this;
2446+
}
2447+
const rootNode =
2448+
// $FlowFixMe[incompatible-cast] Flow expects Node
2449+
(parentHostInstance.getRootNode(getRootNodeOptions): Document | ShadowRoot);
2450+
return rootNode;
2451+
};
24322452

24332453
function normalizeListenerOptions(
24342454
opts: ?EventListenerOptionsOrUseCapture,

packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ describe('FragmentRefs', () => {
846846

847847
describe('getClientRects', () => {
848848
// @gate enableFragmentRefs
849-
it('returns the bounding client recs of all children', async () => {
849+
it('returns the bounding client rects of all children', async () => {
850850
const fragmentRef = React.createRef();
851851
const childARef = React.createRef();
852852
const childBRef = React.createRef();
@@ -884,4 +884,86 @@ describe('FragmentRefs', () => {
884884
expect(clientRects[2].left).toBe(9);
885885
});
886886
});
887+
888+
describe('getRootNode', () => {
889+
// @gate enableFragmentRefs
890+
it('returns the root node of the parent', async () => {
891+
const fragmentRef = React.createRef();
892+
const root = ReactDOMClient.createRoot(container);
893+
894+
function Test() {
895+
return (
896+
<div>
897+
<React.Fragment ref={fragmentRef}>
898+
<div />
899+
</React.Fragment>
900+
</div>
901+
);
902+
}
903+
904+
await act(() => root.render(<Test />));
905+
expect(fragmentRef.current.getRootNode()).toBe(document);
906+
});
907+
908+
// The desired behavior here is to return the topmost disconnected element when
909+
// fragment + parent are unmounted. Currently we have a pass during unmount that
910+
// recursively cleans up return pointers of the whole tree. We can change this
911+
// with a future refactor. See: https://github.com/facebook/react/pull/32682#discussion_r2008313082
912+
// @gate enableFragmentRefs
913+
it('returns the topmost disconnected element if the fragment and parent are unmounted', async () => {
914+
const containerRef = React.createRef();
915+
const parentRef = React.createRef();
916+
const fragmentRef = React.createRef();
917+
const root = ReactDOMClient.createRoot(container);
918+
919+
function Test({mounted}) {
920+
return (
921+
<div ref={containerRef} id="container">
922+
{mounted && (
923+
<div ref={parentRef} id="parent">
924+
<React.Fragment ref={fragmentRef}>
925+
<div />
926+
</React.Fragment>
927+
</div>
928+
)}
929+
</div>
930+
);
931+
}
932+
933+
await act(() => root.render(<Test mounted={true} />));
934+
expect(fragmentRef.current.getRootNode()).toBe(document);
935+
const fragmentHandle = fragmentRef.current;
936+
await act(() => root.render(<Test mounted={false} />));
937+
// TODO: The commented out assertion is the desired behavior. For now, we return
938+
// the fragment instance itself. This is currently the same behavior if you unmount
939+
// the fragment but not the parent. See context above.
940+
// expect(fragmentHandle.getRootNode().id).toBe(parentRefHandle.id);
941+
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
942+
});
943+
944+
// @gate enableFragmentRefs
945+
it('returns self when only the fragment was unmounted', async () => {
946+
const fragmentRef = React.createRef();
947+
const parentRef = React.createRef();
948+
const root = ReactDOMClient.createRoot(container);
949+
950+
function Test({mounted}) {
951+
return (
952+
<div ref={parentRef} id="parent">
953+
{mounted && (
954+
<React.Fragment ref={fragmentRef}>
955+
<div />
956+
</React.Fragment>
957+
)}
958+
</div>
959+
);
960+
}
961+
962+
await act(() => root.render(<Test mounted={true} />));
963+
expect(fragmentRef.current.getRootNode()).toBe(document);
964+
const fragmentHandle = fragmentRef.current;
965+
await act(() => root.render(<Test mounted={false} />));
966+
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
967+
});
968+
});
887969
});

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,10 +512,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
512512
throw new Error('Not yet implemented.');
513513
},
514514

515-
createFragmentInstance(parentInstance) {
515+
createFragmentInstance(fragmentFiber) {
516516
return null;
517517
},
518518

519+
updateFragmentInstanceFiber(fragmentFiber, fragmentInstance) {
520+
// Noop
521+
},
522+
519523
commitNewChildToFragmentInstance(child, fragmentInstance) {
520524
// Noop
521525
},

packages/react-reconciler/src/ReactFiberTreeReflection.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,18 @@ function traverseFragmentInstanceChildren<A, B, C>(
352352
child = child.sibling;
353353
}
354354
}
355+
356+
export function getFragmentParentHostInstance(fiber: Fiber): null | Instance {
357+
let parent = fiber.return;
358+
while (parent !== null) {
359+
if (parent.tag === HostRoot) {
360+
return parent.stateNode.containerInfo;
361+
}
362+
if (parent.tag === HostComponent) {
363+
return parent.stateNode;
364+
}
365+
parent = parent.return;
366+
}
367+
368+
return null;
369+
}

0 commit comments

Comments
 (0)