diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index 60e0a0288d5..c2cde422997 100644 --- a/packages/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/packages/react-native-fantom/src/__tests__/Fantom-itest.js @@ -25,6 +25,8 @@ import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/React function getActualViewportDimensions(root: Root): { viewportWidth: number, viewportHeight: number, + viewportOffsetX: number, + viewportOffsetY: number, } { Fantom.runTask(() => { root.render(); @@ -34,6 +36,8 @@ function getActualViewportDimensions(root: Root): { return { viewportWidth: rect.width, viewportHeight: rect.height, + viewportOffsetX: rect.x, + viewportOffsetY: rect.y, }; } @@ -301,17 +305,23 @@ describe('Fantom', () => { expect(getActualViewportDimensions(rootWithDefaults)).toEqual({ viewportWidth: 390, viewportHeight: 844, + viewportOffsetX: 0, + viewportOffsetY: 0, }); const rootWithCustomWidthAndHeight = Fantom.createRoot({ viewportWidth: 200, viewportHeight: 600, + viewportOffsetX: 20, + viewportOffsetY: 102, }); expect(getActualViewportDimensions(rootWithCustomWidthAndHeight)).toEqual( { viewportWidth: 200, viewportHeight: 600, + viewportOffsetX: 20, + viewportOffsetY: 102, }, ); }); diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 30b61051efe..fcac0272e6c 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -37,6 +37,8 @@ export type RootConfig = { viewportWidth?: number, viewportHeight?: number, devicePixelRatio?: number, + viewportOffsetX?: number, + viewportOffsetY?: number, }; export {getConstants} from './Constants'; @@ -50,6 +52,8 @@ class Root { #surfaceId: number; #viewportWidth: number; #viewportHeight: number; + #viewportOffsetX: number; + #viewportOffsetY: number; #devicePixelRatio: number; #document: ?ReactNativeDocument; @@ -62,6 +66,8 @@ class Root { this.#devicePixelRatio = config?.devicePixelRatio ?? DEFAULT_DEVICE_PIXEL_RATIO; globalSurfaceIdCounter += 10; + this.#viewportOffsetX = config?.viewportOffsetX ?? 0; + this.#viewportOffsetY = config?.viewportOffsetY ?? 0; } // $FlowExpectedError[unsafe-getters-setters] @@ -88,6 +94,8 @@ class Root { this.#viewportWidth, this.#viewportHeight, this.#devicePixelRatio, + this.#viewportOffsetX, + this.#viewportOffsetY, ); this.#hasRendered = true; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 565c1a2e0f5..5aa5b2aa133 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -32,6 +32,28 @@ LayoutableShadowNode::LayoutableShadowNode( layoutMetrics_(static_cast(sourceShadowNode) .layoutMetrics_) {} +LayoutMetrics LayoutableShadowNode::computeLayoutMetricsFromRoot( + const ShadowNodeFamily& descendantNodeFamily, + const LayoutableShadowNode& rootNode, + LayoutInspectingPolicy policy) { + // Prelude. + + if (&descendantNodeFamily == &rootNode.getFamily()) { + // If calculating layout for root node + auto layoutMetrics = rootNode.getLayoutMetrics(); + if (layoutMetrics.displayType == DisplayType::None) { + return EmptyLayoutMetrics; + } + if (policy.includeTransform) { + layoutMetrics.frame = layoutMetrics.frame * rootNode.getTransform(); + } + return layoutMetrics; + } + + auto ancestors = descendantNodeFamily.getAncestors(rootNode); + return computeRelativeLayoutMetrics(ancestors, policy); +} + LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( const ShadowNodeFamily& descendantNodeFamily, const LayoutableShadowNode& ancestorNode, diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index 3f586164b03..94de62e905a 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -47,6 +47,16 @@ class LayoutableShadowNode : public ShadowNode { using UnsharedList = std::vector; + /* + * Returns layout metrics of a node represented as `descendantNodeFamily` + * from `rootNode` like `computeRelativeLayoutMetrics` but returns absolute + * transform for node if used as root. + */ + static LayoutMetrics computeLayoutMetricsFromRoot( + const ShadowNodeFamily& descendantNodeFamily, + const LayoutableShadowNode& rootNode, + LayoutInspectingPolicy policy); + /* * Returns layout metrics of a node represented as `descendantNodeFamily` * computed relatively to given `ancestorNode`. Returns `EmptyLayoutMetrics` diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp index 0dbdd2ca5ce..b006e764f1c 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -112,7 +112,7 @@ void getTextContentInShadowNode( } } -LayoutMetrics getRelativeLayoutMetrics( +LayoutMetrics getLayoutMetricsFromRoot( const ShadowNode& ancestorNode, const ShadowNode& shadowNode, LayoutableShadowNode::LayoutInspectingPolicy policy) { @@ -123,7 +123,7 @@ LayoutMetrics getRelativeLayoutMetrics( return EmptyLayoutMetrics; } - return LayoutableShadowNode::computeRelativeLayoutMetrics( + return LayoutableShadowNode::computeLayoutMetricsFromRoot( shadowNode.getFamily(), *layoutableAncestorShadowNode, policy); } @@ -261,7 +261,7 @@ DOMRect getBoundingClientRect( return DOMRect{}; } - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, shadowNode, {.includeTransform = includeTransform, .includeViewportOffset = true}); @@ -295,13 +295,13 @@ DOMOffset getOffset( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. - auto shadowNodeLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + auto shadowNodeLayoutMetricsRelativeToRoot = getLayoutMetricsFromRoot( *currentRevision, shadowNode, {.includeTransform = false}); if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { return DOMOffset{}; } - auto positionedAncestorLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + auto positionedAncestorLayoutMetricsRelativeToRoot = getLayoutMetricsFromRoot( *currentRevision, *positionedAncestorOfShadowNodeInCurrentRevision, {.includeTransform = false}); @@ -340,7 +340,7 @@ DOMPoint getScrollPosition( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = true}); @@ -374,7 +374,7 @@ DOMSizeRounded getScrollSize( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = false}); @@ -410,7 +410,7 @@ DOMSizeRounded getInnerSize( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = false}); @@ -437,7 +437,7 @@ DOMBorderWidthRounded getBorderWidth( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = false}); @@ -479,7 +479,7 @@ RNMeasureRect measure( return RNMeasureRect{}; } - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = true, .includeViewportOffset = false}); @@ -514,7 +514,7 @@ DOMRect measureInWindow( return DOMRect{}; } - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, {.includeTransform = true, .includeViewportOffset = true}); @@ -548,7 +548,7 @@ std::optional measureLayout( return std::nullopt; } - auto layoutMetrics = getRelativeLayoutMetrics( + auto layoutMetrics = getLayoutMetricsFromRoot( *relativeToShadowNodeInCurrentRevision, *shadowNodeInCurrentRevision, {.includeTransform = false}); diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js index b696691f0da..090d1495122 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js @@ -69,6 +69,8 @@ interface Spec extends TurboModule { viewportWidth: number, viewportHeight: number, devicePixelRatio: number, + viewportOffsetX?: number, + viewportOffsetY?: number, ) => void; stopSurface: (surfaceId: number) => void; enqueueNativeEvent: ( diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js index 11aeacfa63d..18f612ebef8 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js @@ -107,7 +107,12 @@ describe('ReactNativeDocument', () => { it('provides a documentElement node that behaves like a regular element', () => { const nodeRef = createRef(); - const root = Fantom.createRoot({viewportWidth: 200, viewportHeight: 100}); + const root = Fantom.createRoot({ + viewportWidth: 200, + viewportHeight: 100, + viewportOffsetX: 111, + viewportOffsetY: 222, + }); Fantom.runTask(() => { root.render(); }); @@ -118,8 +123,8 @@ describe('ReactNativeDocument', () => { const {x, y, width, height} = document.documentElement.getBoundingClientRect(); - expect(x).toBe(0); - expect(y).toBe(0); + expect(x).toBe(111); + expect(y).toBe(222); expect(width).toBe(200); expect(height).toBe(100); diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index dccd3d38895..9d4343168f9 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js @@ -833,6 +833,40 @@ describe('ReactNativeElement', () => { expect(boundingClientRectAfterUnmount.width).toBe(0); expect(boundingClientRectAfterUnmount.height).toBe(0); }); + + it('returns a DOMRect with its size and position respecting viewport offsets', () => { + const elementRef = createRef(); + + const root = Fantom.createRoot({ + viewportOffsetX: 111, + viewportOffsetY: 222, + }); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const element = ensureReactNativeElement(elementRef.current); + + const boundingClientRect = element.getBoundingClientRect(); + expect(boundingClientRect).toBeInstanceOf(DOMRect); + expect(boundingClientRect.x).toBe(116); + expect(boundingClientRect.y).toBeCloseTo(232.33); + expect(boundingClientRect.width).toBeCloseTo(50.33); + expect(boundingClientRect.height).toBeCloseTo(100.33); + }); }); describe('scrollLeft / scrollTop', () => { @@ -1111,6 +1145,61 @@ describe('ReactNativeElement', () => { expect(element.offsetLeft).toBe(0); expect(element.offsetParent).toBe(null); }); + + it('return the rounded offset values from the parent with view offsets', () => { + const parentRef = createRef(); + const elementRef = createRef(); + + const root = Fantom.createRoot({ + viewportOffsetX: 111, + viewportOffsetY: 222, + }); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + const parentElement = ensureReactNativeElement(parentRef.current); + const element = ensureReactNativeElement(elementRef.current); + + expect(element.offsetTop).toBe(11); + expect(element.offsetLeft).toBe(5); + expect(element.offsetParent).toBe(parentElement); + + expect(parentElement.offsetParent).toBe(root.document.documentElement); + }); + + it('return the rounded offset values from the root with view offsets', () => { + const elementRef = createRef(); + + const root = Fantom.createRoot({ + viewportOffsetX: 111, + viewportOffsetY: 222, + }); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const element = ensureReactNativeElement(elementRef.current); + + expect(element.offsetTop).toBe(11); + expect(element.offsetLeft).toBe(5); + }); }); }); });