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);
+ });
});
});
});