mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user