From 845d93742fb090e7a35abea409a55e2a14613255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 6 May 2025 10:33:03 -0400 Subject: [PATCH 1/3] Remove useId semantics from View Transition name generation (#33094) Originally I thought it was important that SSR used the same View Transition name as the client so that the Fizz runtime could emit those names and then the client could pick up and take over. However, I no longer believe that approach is feasible. Instead, the names can be generated only during that particular animation. Therefore we can simplify the auto name assignment to not have to consider the hydration. --- .../src/ReactFiberBeginWork.js | 13 ------ .../src/ReactFiberViewTransitionComponent.js | 44 +++++-------------- .../src/ReactFiberWorkLoop.js | 4 ++ packages/react-server/src/ReactFizzServer.js | 18 +------- 4 files changed, 15 insertions(+), 64 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 4b9e7dee25119..975313a99f8b1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -36,8 +36,6 @@ import type { OffscreenQueue, OffscreenInstance, } from './ReactFiberOffscreenComponent'; -import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; -import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent'; import type { Cache, CacheComponentState, @@ -3538,7 +3536,6 @@ function updateViewTransition( renderLanes: Lanes, ) { const pendingProps: ViewTransitionProps = workInProgress.pendingProps; - const instance: ViewTransitionState = workInProgress.stateNode; if (pendingProps.name != null && pendingProps.name !== 'auto') { // Explicitly named boundary. We track it so that we can pair it up with another explicit // boundary if we get deleted. @@ -3546,16 +3543,6 @@ function updateViewTransition( current === null ? ViewTransitionNamedMount | ViewTransitionNamedStatic : ViewTransitionNamedStatic; - } else { - // Assign an auto generated name using the useId algorthim if an explicit one is not provided. - // We don't need the name yet but we do it here to allow hydration state to be used. - // We might end up needing these to line up if we want to Transition from dehydrated fallback - // to client rendered content. If we don't end up using that we could just assign an incremeting - // counter in the commit phase instead. - assignViewTransitionAutoName(pendingProps, instance); - if (getIsHydrating()) { - pushMaterializedTreeId(workInProgress); - } } if (__DEV__) { // $FlowFixMe[prop-missing] diff --git a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js index eda56b0cc13e7..cda13a174a3aa 100644 --- a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js +++ b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js @@ -12,14 +12,10 @@ import type {FiberRoot} from './ReactInternalTypes'; import type {ViewTransitionInstance, Instance} from './ReactFiberConfig'; import { - getWorkInProgressRoot, + getCommittingRoot, getPendingTransitionTypes, } from './ReactFiberWorkLoop'; -import {getIsHydrating} from './ReactFiberHydrationContext'; - -import {getTreeId} from './ReactFiberTreeContext'; - export type ViewTransitionState = { autoName: null | string, // the view-transition-name to use when an explicit one is not specified paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance @@ -29,47 +25,27 @@ export type ViewTransitionState = { let globalClientIdCounter: number = 0; -export function assignViewTransitionAutoName( +export function getViewTransitionName( props: ViewTransitionProps, instance: ViewTransitionState, ): string { + if (props.name != null && props.name !== 'auto') { + return props.name; + } if (instance.autoName !== null) { return instance.autoName; } - const root = ((getWorkInProgressRoot(): any): FiberRoot); + // We assume we always call this in the commit phase. + const root = ((getCommittingRoot(): any): FiberRoot); const identifierPrefix = root.identifierPrefix; - - let name; - if (getIsHydrating()) { - const treeId = getTreeId(); - // Use a captial R prefix for server-generated ids. - name = '\u00AB' + identifierPrefix + 'T' + treeId + '\u00BB'; - } else { - // Use a lowercase r prefix for client-generated ids. - const globalClientId = globalClientIdCounter++; - name = - '\u00AB' + - identifierPrefix + - 't' + - globalClientId.toString(32) + - '\u00BB'; - } + const globalClientId = globalClientIdCounter++; + const name = + '\u00AB' + identifierPrefix + 't' + globalClientId.toString(32) + '\u00BB'; instance.autoName = name; return name; } -export function getViewTransitionName( - props: ViewTransitionProps, - instance: ViewTransitionState, -): string { - if (props.name != null && props.name !== 'auto') { - return props.name; - } - // We should have assigned a name by now. - return (instance.autoName: any); -} - function getClassNameByType(classByType: ?ViewTransitionClass): ?string { if (classByType == null || typeof classByType === 'string') { return classByType; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f5c7a1e525905..80d8f27951a18 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -700,6 +700,10 @@ export function getWorkInProgressRoot(): FiberRoot | null { return workInProgressRoot; } +export function getCommittingRoot(): FiberRoot | null { + return pendingEffectsRoot; +} + export function getWorkInProgressRootRenderLanes(): Lanes { return workInProgressRootRenderLanes; } diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 155bb7fe3ff3a..47f67d1eb46f1 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2274,23 +2274,7 @@ function renderViewTransition( ) { const prevKeyPath = task.keyPath; task.keyPath = keyPath; - if (props.name != null && props.name !== 'auto') { - renderNodeDestructive(request, task, props.children, -1); - } else { - // This will be auto-assigned a name which claims a "useId" slot. - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - const prevTreeContext = task.treeContext; - const totalChildren = 1; - const index = 0; - // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, props.children, -1); - // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - task.treeContext = prevTreeContext; - } + renderNodeDestructive(request, task, props.children, -1); task.keyPath = prevKeyPath; } From 7a2c7045aed222b1ece44a18db6326f2f10c89e3 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes <57368278+jorge-cab@users.noreply.github.com> Date: Tue, 6 May 2025 08:50:40 -0700 Subject: [PATCH 2/3] [mcp] Add proper web-vitals metric collection (#33109) Multiple things here: - Improve the mean calculation for metrics so we don't report 0 when web-vitals fail to be retrieved - improve ui chaos monkey to use puppeteer APIs since only those trigger INP/CLS metrics since we need emulated mouse clicks - Add logic to navigate to a temp page after render since some web-vitals metrics are only calculated when the page is backgrounded - Some readability improvements --- .../packages/react-mcp-server/src/index.ts | 19 +- .../react-mcp-server/src/tools/runtimePerf.ts | 175 ++++++++++++------ 2 files changed, 130 insertions(+), 64 deletions(-) diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 138dc57dc14a9..2ec747eac4dfd 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -22,6 +22,12 @@ import assertExhaustive from './utils/assertExhaustive'; import {convert} from 'html-to-text'; import {measurePerformance} from './tools/runtimePerf'; +function calculateMean(values: number[]): string { + return values.length > 0 + ? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms' + : 'could not collect'; +} + const server = new McpServer({ name: 'React', version: '0.0.0', @@ -326,17 +332,16 @@ server.tool( # React Component Performance Results ## Mean Render Time -${results.renderTime / iterations}ms +${calculateMean(results.renderTime)} ## Mean Web Vitals -- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms -- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms -- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms -- First Input Delay (FID): ${results.webVitals.fid / iterations}ms +- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)} +- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)} +- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)} ## Mean React Profiler -- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms -- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms +- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)} +- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)} `; return { diff --git a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts index 7f4d0a1efecd8..30badc833d68c 100644 --- a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts +++ b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts @@ -8,25 +8,51 @@ import * as babelPresetEnv from '@babel/preset-env'; import * as babelPresetReact from '@babel/preset-react'; type PerformanceResults = { - renderTime: number; + renderTime: number[]; webVitals: { - cls: number; - lcp: number; - inp: number; - fid: number; - ttfb: number; + cls: number[]; + lcp: number[]; + inp: number[]; + fid: number[]; + ttfb: number[]; }; reactProfiler: { - id: number; - phase: number; - actualDuration: number; - baseDuration: number; - startTime: number; - commitTime: number; + id: number[]; + phase: number[]; + actualDuration: number[]; + baseDuration: number[]; + startTime: number[]; + commitTime: number[]; }; error: Error | null; }; +type EvaluationResults = { + renderTime: number | null; + webVitals: { + cls: number | null; + lcp: number | null; + inp: number | null; + fid: number | null; + ttfb: number | null; + }; + reactProfiler: { + id: number | null; + phase: number | null; + actualDuration: number | null; + baseDuration: number | null; + startTime: number | null; + commitTime: number | null; + }; + error: Error | null; +}; + +function delay(time: number) { + return new Promise(function (resolve) { + setTimeout(resolve, time); + }); +} + export async function measurePerformance( code: string, iterations: number, @@ -72,21 +98,21 @@ export async function measurePerformance( const html = buildHtml(transpiled); let performanceResults: PerformanceResults = { - renderTime: 0, + renderTime: [], webVitals: { - cls: 0, - lcp: 0, - inp: 0, - fid: 0, - ttfb: 0, + cls: [], + lcp: [], + inp: [], + fid: [], + ttfb: [], }, reactProfiler: { - id: 0, - phase: 0, - actualDuration: 0, - baseDuration: 0, - startTime: 0, - commitTime: 0, + id: [], + phase: [], + actualDuration: [], + baseDuration: [], + startTime: [], + commitTime: [], }, error: null, }; @@ -96,38 +122,73 @@ export async function measurePerformance( await page.waitForFunction( 'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)', ); + // ui chaos monkey - await page.waitForFunction(`window.__RESULT__ !== undefined && (function() { - for (const el of [...document.querySelectorAll('a'), ...document.querySelectorAll('button')]) { - console.log(el); - el.click(); + const selectors = await page.evaluate(() => { + window.__INTERACTABLE_SELECTORS__ = []; + const elements = Array.from(document.querySelectorAll('a')).concat( + Array.from(document.querySelectorAll('button')), + ); + for (const el of elements) { + window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase()); } - return true; - })() `); - const evaluationResult: PerformanceResults = await page.evaluate(() => { + return window.__INTERACTABLE_SELECTORS__; + }); + + await Promise.all( + selectors.map(async (selector: string) => { + try { + await page.click(selector); + } catch (e) { + console.log(`warning: Could not click ${selector}: ${e.message}`); + } + }), + ); + await delay(500); + + // Visit a new page for 1s to background the current page so that WebVitals can finish being calculated + const tempPage = await browser.newPage(); + await tempPage.evaluate(() => { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, 1000); + }); + }); + await tempPage.close(); + + const evaluationResult: EvaluationResults = await page.evaluate(() => { return (window as any).__RESULT__; }); - // TODO: investigate why webvital metrics are not populating correctly - performanceResults.renderTime += evaluationResult.renderTime; - performanceResults.webVitals.cls += evaluationResult.webVitals.cls || 0; - performanceResults.webVitals.lcp += evaluationResult.webVitals.lcp || 0; - performanceResults.webVitals.inp += evaluationResult.webVitals.inp || 0; - performanceResults.webVitals.fid += evaluationResult.webVitals.fid || 0; - performanceResults.webVitals.ttfb += evaluationResult.webVitals.ttfb || 0; - - performanceResults.reactProfiler.id += - evaluationResult.reactProfiler.actualDuration || 0; - performanceResults.reactProfiler.phase += - evaluationResult.reactProfiler.phase || 0; - performanceResults.reactProfiler.actualDuration += - evaluationResult.reactProfiler.actualDuration || 0; - performanceResults.reactProfiler.baseDuration += - evaluationResult.reactProfiler.baseDuration || 0; - performanceResults.reactProfiler.startTime += - evaluationResult.reactProfiler.startTime || 0; - performanceResults.reactProfiler.commitTime += - evaluationResult.reactProfiler.commitTime || 0; + if (evaluationResult.renderTime !== null) { + performanceResults.renderTime.push(evaluationResult.renderTime); + } + + const webVitalMetrics = ['cls', 'lcp', 'inp', 'fid', 'ttfb'] as const; + for (const metric of webVitalMetrics) { + if (evaluationResult.webVitals[metric] !== null) { + performanceResults.webVitals[metric].push( + evaluationResult.webVitals[metric], + ); + } + } + + const profilerMetrics = [ + 'id', + 'phase', + 'actualDuration', + 'baseDuration', + 'startTime', + 'commitTime', + ] as const; + for (const metric of profilerMetrics) { + if (evaluationResult.reactProfiler[metric] !== null) { + performanceResults.reactProfiler[metric].push( + evaluationResult.reactProfiler[metric], + ); + } + } performanceResults.error = evaluationResult.error; } @@ -159,14 +220,14 @@ function buildHtml(transpiled: string) { renderTime: null, webVitals: {}, reactProfiler: {}, - error: null + error: null, }; - webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; }); - webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; }); - webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; }); - webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; }); - webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; }); + webVitals.onCLS(({value}) => { window.__RESULT__.webVitals.cls = value; }); + webVitals.onLCP(({value}) => { window.__RESULT__.webVitals.lcp = value; }); + webVitals.onINP(({value}) => { window.__RESULT__.webVitals.inp = value; }); + webVitals.onFID(({value}) => { window.__RESULT__.webVitals.fid = value; }); + webVitals.onTTFB(({value}) => { window.__RESULT__.webVitals.ttfb = value; }); try { ${transpiled} From e5a8de81e57181692d33ce916dfd6aa23638ec92 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Tue, 6 May 2025 13:01:40 -0400 Subject: [PATCH 3/3] Add compareDocumentPosition to fragment instances (#32722) This adds `compareDocumentPosition(otherNode)` to fragment instances. The semantics implemented are meant to match typical element positioning, with some fragment specifics. See the unit tests for all expectations. - An element preceding a fragment is `Node.DOCUMENT_POSITION_PRECEDING` - An element after a fragment is `Node.DOCUMENT_POSITION_FOLLOWING` - An element containing the fragment is `Node.DOCUMENT_POSITION_PRECEDING` and `Node.DOCUMENT_POSITION_CONTAINING` - An element within the fragment is `Node.DOCUMENT_POSITION_CONTAINED_BY` - An element compared against an empty fragment will result in `Node.DOCUMENT_POSITION_DISCONNECTED` and `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` Since we assume a fragment instances target children are DOM siblings and we want to compare the full fragment as a pseudo container, we can compare against the first target child outside of handling the special cases (empty fragments and contained elements). --- .../src/client/ReactFiberConfigDOM.js | 195 +++++- .../src/events/DOMPluginEventSystem.js | 44 +- .../__tests__/ReactDOMFragmentRefs-test.js | 602 ++++++++++++++++++ .../src/ReactFiberConfigFabric.js | 13 +- .../src/ReactFiberTreeReflection.js | 243 ++++++- 5 files changed, 1013 insertions(+), 84 deletions(-) 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; }