Support root offsets for Fantom and getBoundingClientRect (#51817)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/51817

Changelog:
[General][Fixed] - Support viewport offsets for Fantom root and fix `getBoundingClientRect` to respect viewport offsets

Reviewed By: rubennorte

Differential Revision: D75632540

fbshipit-source-id: a3989f1470156dd6be83ad7b03de8cb4de1a56ac
This commit is contained in:
Luna Wei
2025-06-06 09:58:30 -07:00
committed by Facebook GitHub Bot
parent ac8d677cb4
commit b5c62f52d1
8 changed files with 161 additions and 15 deletions
@@ -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(<View />);
@@ -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,
},
);
});
+8
View File
@@ -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;
}
@@ -32,6 +32,28 @@ LayoutableShadowNode::LayoutableShadowNode(
layoutMetrics_(static_cast<const LayoutableShadowNode&>(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,
@@ -47,6 +47,16 @@ class LayoutableShadowNode : public ShadowNode {
using UnsharedList = std::vector<LayoutableShadowNode*>;
/*
* 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`
@@ -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<DOMRect> measureLayout(
return std::nullopt;
}
auto layoutMetrics = getRelativeLayoutMetrics(
auto layoutMetrics = getLayoutMetricsFromRoot(
*relativeToShadowNodeInCurrentRevision,
*shadowNodeInCurrentRevision,
{.includeTransform = false});
@@ -69,6 +69,8 @@ interface Spec extends TurboModule {
viewportWidth: number,
viewportHeight: number,
devicePixelRatio: number,
viewportOffsetX?: number,
viewportOffsetY?: number,
) => void;
stopSurface: (surfaceId: number) => void;
enqueueNativeEvent: (
@@ -107,7 +107,12 @@ describe('ReactNativeDocument', () => {
it('provides a documentElement node that behaves like a regular element', () => {
const nodeRef = createRef<HostInstance>();
const root = Fantom.createRoot({viewportWidth: 200, viewportHeight: 100});
const root = Fantom.createRoot({
viewportWidth: 200,
viewportHeight: 100,
viewportOffsetX: 111,
viewportOffsetY: 222,
});
Fantom.runTask(() => {
root.render(<View ref={nodeRef} />);
});
@@ -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);
@@ -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<HostInstance>();
const root = Fantom.createRoot({
viewportOffsetX: 111,
viewportOffsetY: 222,
});
Fantom.runTask(() => {
root.render(
<View
key="parent"
style={{
position: 'absolute',
left: 5.1,
top: 10.2,
width: 50.3,
height: 100.4,
}}
ref={elementRef}
/>,
);
});
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<HostInstance>();
const elementRef = createRef<HostInstance>();
const root = Fantom.createRoot({
viewportOffsetX: 111,
viewportOffsetY: 222,
});
Fantom.runTask(() => {
root.render(
<View key="parent" ref={parentRef}>
<View
key="child"
style={{marginTop: 10.6, marginLeft: 5.1}}
ref={elementRef}
/>
</View>,
);
});
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<HostInstance>();
const root = Fantom.createRoot({
viewportOffsetX: 111,
viewportOffsetY: 222,
});
Fantom.runTask(() => {
root.render(
<View
key="child"
style={{marginTop: 10.6, marginLeft: 5.1}}
ref={elementRef}
/>,
);
});
const element = ensureReactNativeElement(elementRef.current);
expect(element.offsetTop).toBe(11);
expect(element.offsetLeft).toBe(5);
});
});
});
});