diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 7d705e059bdf8..8584b644eff9d 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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, @@ -60,7 +65,9 @@ import { } from './ReactDOMComponentTree'; import { traverseFragmentInstance, - getFragmentParentHostInstance, + getFragmentParentHostFiber, + getNextSiblingHostFiber, + getInstanceFromHostFiber, } from 'react-reconciler/src/ReactFiberTreeReflection'; export {detachDeletedInstance}; @@ -2599,6 +2606,7 @@ export type FragmentInstanceType = { getRootNode(getRootNodeOptions?: { composed: boolean, }): Document | ShadowRoot | FragmentInstanceType, + compareDocumentPosition(otherNode: Instance): number, }; function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { @@ -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(child); + instance.addEventListener(type, listener, optionsOrUseCapture); return false; } // $FlowFixMe[prop-missing] @@ -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(child); + instance.removeEventListener(type, listener, optionsOrUseCapture); return false; } // $FlowFixMe[prop-missing] @@ -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(fiber); + return setFocusIfFocusable(instance, focusOptions); +} // $FlowFixMe[prop-missing] FragmentInstance.prototype.focusLast = function ( this: FragmentInstanceType, focusOptions?: FocusOptions, ): void { - const children: Array = []; + const children: Array = []; 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, -): boolean { +function collectChildren(child: Fiber, collection: Array): boolean { collection.push(child); return false; } @@ -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(child); + const ownerDocument = instance.ownerDocument; + if (instance === ownerDocument.activeElement) { // $FlowFixMe[prop-missing] - child.blur(); + instance.blur(); return true; } return false; @@ -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(child); + observer.observe(instance); return false; } // $FlowFixMe[prop-missing] @@ -2770,10 +2786,11 @@ FragmentInstance.prototype.unobserveUsing = function ( } }; function unobserveChild( - child: Instance, + child: Fiber, observer: IntersectionObserver | ResizeObserver, ) { - observer.unobserve(child); + const instance = getInstanceFromHostFiber(child); + observer.unobserve(instance); return false; } // $FlowFixMe[prop-missing] @@ -2784,9 +2801,10 @@ FragmentInstance.prototype.getClientRects = function ( traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects); return rects; }; -function collectClientRects(child: Instance, rects: Array): boolean { +function collectClientRects(child: Fiber, rects: Array): boolean { + const instance = getInstanceFromHostFiber(child); // $FlowFixMe[method-unbinding] - rects.push.apply(rects, child.getClientRects()); + rects.push.apply(rects, instance.getClientRects()); return false; } // $FlowFixMe[prop-missing] @@ -2794,15 +2812,144 @@ 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(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 = []; + traverseFragmentInstance(this._fragmentFiber, collectChildren, children); + + let result = Node.DOCUMENT_POSITION_DISCONNECTED; + if (children.length === 0) { + // If the fragment has no children, we can use the parent and + // siblings to determine a position. + const parentHostInstance = + getInstanceFromHostFiber(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(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(children[0]); + const lastElement = getInstanceFromHostFiber( + 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, diff --git a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js index b4733c7781f8a..916786128dee8 100644 --- a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js @@ -36,6 +36,7 @@ import { HostText, ScopeComponent, } from 'react-reconciler/src/ReactWorkTags'; +import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection'; import getEventTarget from './getEventTarget'; import { @@ -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, @@ -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( diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js index c3d3a9ca7e45b..50447e1eac677 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -11,6 +11,8 @@ let React; let ReactDOMClient; +let ReactDOM; +let createPortal; let act; let container; let Fragment; @@ -31,6 +33,8 @@ describe('FragmentRefs', () => { Fragment = React.Fragment; Activity = React.unstable_Activity; ReactDOMClient = require('react-dom/client'); + ReactDOM = require('react-dom'); + createPortal = ReactDOM.createPortal; act = require('internal-test-utils').act; const IntersectionMocks = require('./utils/IntersectionMocks'); mockIntersectionObserver = IntersectionMocks.mockIntersectionObserver; @@ -40,6 +44,7 @@ describe('FragmentRefs', () => { require('internal-test-utils').assertConsoleErrorDev; container = document.createElement('div'); + document.body.innerHTML = ''; document.body.appendChild(container); }); @@ -611,6 +616,39 @@ describe('FragmentRefs', () => { expect(logs).toEqual([]); }); + // @gate enableFragmentRefs + it('applies event listeners to portaled children', async () => { + const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( + +
+ {createPortal(
, document.body)} + + ); + } + + await act(() => { + root.render(); + }); + + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.id); + }); + + childARef.current.click(); + expect(logs).toEqual(['child-a']); + + logs.length = 0; + childBRef.current.click(); + expect(logs).toEqual(['child-b']); + }); + describe('with activity', () => { // @gate enableFragmentRefs && enableActivity it('does not apply event listeners to hidden trees', async () => { @@ -966,4 +1004,568 @@ describe('FragmentRefs', () => { expect(fragmentHandle.getRootNode()).toBe(fragmentHandle); }); }); + + describe('compareDocumentPosition', () => { + function expectPosition(position, spec) { + const positionResult = { + following: (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0, + preceding: (position & Node.DOCUMENT_POSITION_PRECEDING) !== 0, + contains: (position & Node.DOCUMENT_POSITION_CONTAINS) !== 0, + containedBy: (position & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0, + disconnected: (position & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0, + implementationSpecific: + (position & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0, + }; + expect(positionResult).toEqual(spec); + } + // @gate enableFragmentRefs + it('returns the relationship between the fragment instance and a given node', async () => { + const fragmentRef = React.createRef(); + const beforeRef = React.createRef(); + const afterRef = React.createRef(); + const middleChildRef = React.createRef(); + const firstChildRef = React.createRef(); + const lastChildRef = React.createRef(); + const containerRef = React.createRef(); + const disconnectedElement = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
+
+ +
+
+
+ +
+
+ ); + } + + await act(() => root.render()); + + // document.body is preceding and contains the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(document.body), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + // beforeRef is preceding the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(beforeRef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + // afterRef is following the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(afterRef.current), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + // firstChildRef is contained by the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(firstChildRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + + // middleChildRef is contained by the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(middleChildRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + + // lastChildRef is contained by the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(lastChildRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + + // containerRef preceds and contains the fragment + expectPosition( + fragmentRef.current.compareDocumentPosition(containerRef.current), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + expectPosition( + fragmentRef.current.compareDocumentPosition(disconnectedElement), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: true, + implementationSpecific: true, + }, + ); + }); + + // @gate enableFragmentRefs + it('handles fragment instances with one child', async () => { + const fragmentRef = React.createRef(); + const beforeRef = React.createRef(); + const afterRef = React.createRef(); + const containerRef = React.createRef(); + const onlyChildRef = React.createRef(); + const disconnectedElement = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
+
+
+ +
+ +
+
+
+ ); + } + + await act(() => root.render()); + expectPosition( + fragmentRef.current.compareDocumentPosition(beforeRef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(afterRef.current), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(onlyChildRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(containerRef.current), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(disconnectedElement), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: true, + implementationSpecific: true, + }, + ); + }); + + // @gate enableFragmentRefs + it('handles empty fragment instances', async () => { + const fragmentRef = React.createRef(); + const beforeParentRef = React.createRef(); + const beforeRef = React.createRef(); + const afterRef = React.createRef(); + const afterParentRef = React.createRef(); + const containerRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( + <> +
+
+
+ +
+
+
+ + ); + } + + await act(() => root.render()); + + expectPosition( + fragmentRef.current.compareDocumentPosition(document.body), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(beforeRef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(beforeParentRef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(afterRef.current), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(afterParentRef.current), + { + preceding: false, + following: true, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(containerRef.current), + { + preceding: false, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + }); + + // @gate enableFragmentRefs + it('returns disconnected for comparison with an unmounted fragment instance', async () => { + const fragmentRef = React.createRef(); + const containerRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test({mount}) { + return ( +
+ {mount && ( + +
+ + )} +
+ ); + } + + await act(() => root.render()); + + const fragmentHandle = fragmentRef.current; + + expectPosition( + fragmentHandle.compareDocumentPosition(containerRef.current), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + await act(() => { + root.render(); + }); + + expectPosition( + fragmentHandle.compareDocumentPosition(containerRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: false, + disconnected: true, + implementationSpecific: false, + }, + ); + }); + + describe('with portals', () => { + // @gate enableFragmentRefs + it('handles portaled elements', async () => { + const fragmentRef = React.createRef(); + const portaledSiblingRef = React.createRef(); + const portaledChildRef = React.createRef(); + + function Test() { + return ( +
+ {createPortal(
, document.body)} + + {createPortal(
, document.body)} +
+ +
+ ); + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + + // The sibling is preceding in both the DOM and the React tree + expectPosition( + fragmentRef.current.compareDocumentPosition( + portaledSiblingRef.current, + ), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + // The child is contained by in the React tree but not in the DOM + expectPosition( + fragmentRef.current.compareDocumentPosition(portaledChildRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + }); + + // @gate enableFragmentRefs + it('handles multiple portals to the same element', async () => { + const root = ReactDOMClient.createRoot(container); + const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const childCRef = React.createRef(); + + function Test() { + const [c, setC] = React.useState(false); + React.useEffect(() => { + setC(true); + }); + + return ( + <> + {createPortal( + +
+ {c ?
: null} + , + document.body, + )} + {createPortal(

, document.body)} + + ); + } + + await act(() => root.render()); + + // Due to effect, order is A->B->C + expect(document.body.innerHTML).toBe( + '

' + + '
' + + '

' + + '
', + ); + + expectPosition( + fragmentRef.current.compareDocumentPosition(document.body), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: false, + }, + ); + + expectPosition( + fragmentRef.current.compareDocumentPosition(childARef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(childBRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(childCRef.current), + { + preceding: false, + following: false, + contains: false, + containedBy: true, + disconnected: false, + implementationSpecific: false, + }, + ); + }); + + // @gate enableFragmentRefs + it('handles empty fragments', async () => { + const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + + function Test() { + return ( + <> +
+ {createPortal(, document.body)} +
+ + ); + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + + expectPosition( + fragmentRef.current.compareDocumentPosition(document.body), + { + preceding: true, + following: false, + contains: true, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(childARef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + expectPosition( + fragmentRef.current.compareDocumentPosition(childBRef.current), + { + preceding: true, + following: false, + contains: false, + containedBy: false, + disconnected: false, + implementationSpecific: true, + }, + ); + }); + }); + }); }); diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index afa0a9c218e53..7a06f157e668f 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -24,7 +24,10 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import {HostText} from 'react-reconciler/src/ReactWorkTags'; -import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection'; +import { + getInstanceFromHostFiber, + traverseFragmentInstance, +} from 'react-reconciler/src/ReactFiberTreeReflection'; // Modules provided by RN: import { @@ -640,7 +643,8 @@ FragmentInstance.prototype.observeUsing = function ( this._observers.add(observer); traverseFragmentInstance(this._fragmentFiber, observeChild, observer); }; -function observeChild(instance: Instance, observer: IntersectionObserver) { +function observeChild(child: Fiber, observer: IntersectionObserver) { + const instance = getInstanceFromHostFiber(child); const publicInstance = getPublicInstance(instance); if (publicInstance == null) { throw new Error('Expected to find a host node. This is a bug in React.'); @@ -666,7 +670,8 @@ FragmentInstance.prototype.unobserveUsing = function ( traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer); } }; -function unobserveChild(instance: Instance, observer: IntersectionObserver) { +function unobserveChild(child: Fiber, observer: IntersectionObserver) { + const instance = getInstanceFromHostFiber(child); const publicInstance = getPublicInstance(instance); if (publicInstance == null) { throw new Error('Expected to find a host node. This is a bug in React.'); @@ -690,7 +695,7 @@ export function updateFragmentInstanceFiber( } export function commitNewChildToFragmentInstance( - child: Instance, + child: Fiber, fragmentInstance: FragmentInstanceType, ): void { if (fragmentInstance._observers !== null) { diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index d799e2308ae47..d032d3247e475 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -12,7 +12,6 @@ import type { Container, ActivityInstance, SuspenseInstance, - Instance, } from './ReactFiberConfig'; import type {ActivityState} from './ReactFiberActivityComponent'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; @@ -345,27 +344,42 @@ export function doesFiberContain( return false; } -export function traverseFragmentInstance( +export function traverseFragmentInstance( fragmentFiber: Fiber, - fn: (I, A, B, C) => boolean, + fn: (Fiber, A, B, C) => boolean, a: A, b: B, c: C, ): void { - traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c); + traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c); } -function traverseFragmentInstanceChildren( +function traverseVisibleHostChildren( child: Fiber | null, - fn: (I, A, B, C) => boolean, + searchWithinHosts: boolean, + fn: (Fiber, A, B, C) => boolean, a: A, b: B, c: C, -): void { +): boolean { while (child !== null) { if (child.tag === HostComponent) { - if (fn(child.stateNode, a, b, c)) { - return; + if (fn(child, a, b, c)) { + return true; + } + if (searchWithinHosts) { + if ( + traverseVisibleHostChildren( + child.child, + searchWithinHosts, + fn, + a, + b, + c, + ) + ) { + return true; + } } } else if ( child.tag === OffscreenComponent && @@ -373,23 +387,222 @@ function traverseFragmentInstanceChildren( ) { // Skip hidden subtrees } else { - traverseFragmentInstanceChildren(child.child, fn, a, b, c); + if ( + traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c) + ) { + return true; + } } child = child.sibling; } + return false; } -export function getFragmentParentHostInstance(fiber: Fiber): null | Instance { +export function getFragmentParentHostFiber(fiber: Fiber): null | Fiber { let parent = fiber.return; while (parent !== null) { - if (parent.tag === HostRoot) { - return parent.stateNode.containerInfo; + if (parent.tag === HostRoot || parent.tag === HostComponent) { + return parent; } - if (parent.tag === HostComponent) { - return parent.stateNode; + parent = parent.return; + } + + return null; +} + +export function getInstanceFromHostFiber(fiber: Fiber): I { + switch (fiber.tag) { + case HostComponent: + return fiber.stateNode; + case HostRoot: + return fiber.stateNode.containerInfo; + default: + throw new Error('Expected to find a host node. This is a bug in React.'); + } +} + +let searchTarget = null; +let searchBoundary = null; +function pushSearchTarget(target: null | Fiber): void { + searchTarget = target; +} +function popSearchTarget(): null | Fiber { + return searchTarget; +} +function pushSearchBoundary(value: null | Fiber): void { + searchBoundary = value; +} +function popSearchBoundary(): null | Fiber { + return searchBoundary; +} + +export function getNextSiblingHostFiber(fiber: Fiber): null | Fiber { + traverseVisibleHostChildren(fiber.sibling, false, findNextSibling); + const sibling = popSearchTarget(); + pushSearchTarget(null); + return sibling; +} + +function findNextSibling(child: Fiber): boolean { + pushSearchTarget(child); + return true; +} + +export function isFiberContainedBy( + maybeChild: Fiber, + maybeParent: Fiber, +): boolean { + let parent = maybeParent.return; + if (parent === maybeChild || parent === maybeChild.alternate) { + return true; + } + while (parent !== null && parent !== maybeChild) { + if ( + (parent.tag === HostComponent || parent.tag === HostRoot) && + (parent.return === maybeChild || parent.return === maybeChild.alternate) + ) { + return true; } parent = parent.return; } + return false; +} + +export function isFiberPreceding(fiber: Fiber, otherFiber: Fiber): boolean { + const commonAncestor = getLowestCommonAncestor( + fiber, + otherFiber, + getParentForFragmentAncestors, + ); + if (commonAncestor === null) { + return false; + } + traverseVisibleHostChildren( + commonAncestor, + true, + isFiberPrecedingCheck, + otherFiber, + fiber, + ); + const target = popSearchTarget(); + pushSearchTarget(null); + return target !== null; +} + +function isFiberPrecedingCheck( + child: Fiber, + target: Fiber, + boundary: Fiber, +): boolean { + if (child === boundary) { + return true; + } + if (child === target) { + pushSearchTarget(child); + return true; + } + return false; +} + +export function isFiberFollowing(fiber: Fiber, otherFiber: Fiber): boolean { + const commonAncestor = getLowestCommonAncestor( + fiber, + otherFiber, + getParentForFragmentAncestors, + ); + if (commonAncestor === null) { + return false; + } + traverseVisibleHostChildren( + commonAncestor, + true, + isFiberFollowingCheck, + otherFiber, + fiber, + ); + const target = popSearchTarget(); + pushSearchTarget(null); + pushSearchBoundary(null); + return target !== null; +} + +function isFiberFollowingCheck( + child: Fiber, + target: Fiber, + boundary: Fiber, +): boolean { + if (child === boundary) { + pushSearchBoundary(child); + return false; + } + if (child === target) { + // The target is only following if we already found the boundary. + if (popSearchBoundary() !== null) { + pushSearchTarget(child); + } + return true; + } + return false; +} +function getParentForFragmentAncestors(inst: Fiber | null): Fiber | null { + if (inst === null) { + return null; + } + do { + inst = inst === null ? null : inst.return; + } while ( + inst && + inst.tag !== HostComponent && + inst.tag !== HostSingleton && + inst.tag !== HostRoot + ); + if (inst) { + return inst; + } + return null; +} + +/** + * Return the lowest common ancestor of A and B, or null if they are in + * different trees. + */ +export function getLowestCommonAncestor( + instA: Fiber, + instB: Fiber, + getParent: (inst: Fiber | null) => Fiber | null, +): 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; }