diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 6efcf65091c7..b59bc00ddecb 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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'; @@ -2230,6 +2233,9 @@ export type FragmentInstanceType = { observeUsing(observer: IntersectionObserver | ResizeObserver): void, unobserveUsing(observer: IntersectionObserver | ResizeObserver): void, getClientRects(): Array, + getRootNode(getRootNodeOptions?: { + composed: boolean, + }): Document | ShadowRoot | FragmentInstanceType, }; function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { @@ -2329,7 +2335,7 @@ FragmentInstance.prototype.focus = function ( FragmentInstance.prototype.focusLast = function ( this: FragmentInstanceType, focusOptions?: FocusOptions, -) { +): void { const children: Array = []; traverseFragmentInstance(this._fragmentFiber, collectChildren, children); for (let i = children.length - 1; i >= 0; i--) { @@ -2420,6 +2426,20 @@ function collectClientRects(child: Instance, rects: Array): 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, diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js index c2cfc99ee87b..c3d3a9ca7e45 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -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 ( +
+ +
+ +
+ ); + } + + await act(() => root.render()); + 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 ( +
+ {mounted && ( +
+ +
+ +
+ )} +
+ ); + } + + await act(() => root.render()); + expect(fragmentRef.current.getRootNode()).toBe(document); + const fragmentHandle = fragmentRef.current; + await act(() => root.render()); + // 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 ( +
+ {mounted && ( + +
+ + )} +
+ ); + } + + await act(() => root.render()); + expect(fragmentRef.current.getRootNode()).toBe(document); + const fragmentHandle = fragmentRef.current; + await act(() => root.render()); + expect(fragmentHandle.getRootNode()).toBe(fragmentHandle); + }); + }); }); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 266f860fe6d3..efb5f955cae7 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -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 }, diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index 6d113377c35c..2e2466a4f5a8 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -352,3 +352,18 @@ function traverseFragmentInstanceChildren( 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; +}