Skip to content

Commit 2d17859

Browse files
andrewdacenkofacebook-github-bot
authored andcommitted
Support getBoundingClientRect for TextShadowNode (facebook#55278)
Summary: Changelog: [General][Added] - Add DOM getBoundingClientRect() for nested text When `getBoundingClientRect()` is called on a nested `<Text>` component (TextShadowNode), return the parent paragraph's bounding rect instead of empty/invalid metrics. TextShadowNode is a virtual node that doesn't have its own layout metrics. This matches web behavior where inline elements return their container's rect. Use `getClientRects()` (added in a follow-up diff) to get the individual fragment rects for text that spans multiple lines. Differential Revision: D91087220
1 parent c502c5e commit 2d17859

File tree

6 files changed

+141
-68
lines changed

6 files changed

+141
-68
lines changed

packages/react-native/Package.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@ let reactCore = RNTarget(
423423
let reactFabric = RNTarget(
424424
name: .reactFabric,
425425
path: "ReactCommon/react/renderer",
426+
searchPaths: [
427+
"ReactCommon/react/renderer/components/text/platform/cxx", // For <react/renderer/components/text/ParagraphState.h>
428+
"ReactCommon/react/renderer/textlayoutmanager/platform/ios", // For <react/renderer/textlayoutmanager/TextLayoutManager.h>
429+
],
426430
excludedPaths: [
427431
"animations/tests",
428432
"attributedstring/tests",
@@ -456,7 +460,7 @@ let reactFabric = RNTarget(
456460
"components/root/tests",
457461
],
458462
dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga],
459-
sources: ["animationbackend", "animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/legacyviewmanagerinterop", "dom", "scheduler", "mounting", "observers/events", "observers/intersection", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency"]
463+
sources: ["animationbackend", "animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/legacyviewmanagerinterop", "scheduler", "mounting", "observers/events", "observers/intersection", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency"]
460464
)
461465

462466
let reactFabricInputAccessory = RNTarget(
@@ -488,6 +492,13 @@ let reactFabricSafeAreaView = RNTarget(
488492
dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging]
489493
)
490494

495+
let reactFabricDOM = RNTarget(
496+
name: .reactFabricDOM,
497+
path: "ReactCommon/react/renderer/dom",
498+
dependencies: [.reactNativeDependencies, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactFabricText],
499+
sources: ["."]
500+
)
501+
491502
let reactFabricTextLayoutManager = RNTarget(
492503
name: .reactFabricTextLayoutManager,
493504
path: "ReactCommon/react/renderer/textlayoutmanager",
@@ -656,6 +667,7 @@ let targets = [
656667
reactFabricImage,
657668
reactFabricInputAccessory,
658669
reactFabricModal,
670+
reactFabricDOM,
659671
reactFabricSafeAreaView,
660672
reactFabricSwitch,
661673
reactFabricTextLayoutManager,
@@ -835,6 +847,7 @@ extension String {
835847
static let reactFabric = "React-Fabric"
836848
static let reactRCTFabric = "React-RCTFabric"
837849

850+
static let reactFabricDOM = "React-FabricDOM"
838851
static let reactFabricImage = "React-FabricImage"
839852
static let reactFabricInputAccessory = "React-FabricInputAccessory"
840853
static let reactFabricModal = "React-FabricModal"

packages/react-native/ReactCommon/React-Fabric.podspec

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,6 @@ Pod::Spec.new do |s|
5555
add_rn_third_party_dependencies(s)
5656
add_rncore_dependency(s)
5757

58-
s.subspec "animated" do |ss|
59-
ss.dependency "React-Fabric/animationbackend"
60-
ss.source_files = podspec_sources("react/renderer/animated/**/*.{m,mm,cpp,h}", "react/renderer/animated/**/*.{h}")
61-
ss.exclude_files = "react/renderer/animated/tests"
62-
ss.header_dir = "react/renderer/animated"
63-
end
64-
65-
s.subspec "animations" do |ss|
66-
ss.source_files = podspec_sources("react/renderer/animations/**/*.{m,mm,cpp,h}", "react/renderer/animations/**/*.{h}")
67-
ss.exclude_files = "react/renderer/animations/tests"
68-
ss.header_dir = "react/renderer/animations"
69-
end
70-
71-
s.subspec "animationbackend" do |ss|
72-
ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}")
73-
ss.header_dir = "react/renderer/animationbackend"
74-
end
75-
7658
s.subspec "attributedstring" do |ss|
7759
ss.source_files = podspec_sources("react/renderer/attributedstring/**/*.{m,mm,cpp,h}", "react/renderer/attributedstring/**/*.{h}")
7860
ss.exclude_files = "react/renderer/attributedstring/tests"
@@ -147,23 +129,6 @@ Pod::Spec.new do |s|
147129
end
148130
end
149131

150-
s.subspec "dom" do |ss|
151-
ss.dependency "React-graphics"
152-
ss.source_files = podspec_sources("react/renderer/dom/**/*.{m,mm,cpp,h}", "react/renderer/dom/**/*.{h}")
153-
ss.exclude_files = "react/renderer/dom/tests"
154-
ss.header_dir = "react/renderer/dom"
155-
end
156-
157-
s.subspec "scheduler" do |ss|
158-
ss.source_files = podspec_sources("react/renderer/scheduler/**/*.{m,mm,cpp,h}", "react/renderer/scheduler/**/*.h")
159-
ss.header_dir = "react/renderer/scheduler"
160-
161-
ss.dependency "React-Fabric/animationbackend"
162-
ss.dependency "React-performancecdpmetrics"
163-
ss.dependency "React-performancetimeline"
164-
ss.dependency "React-Fabric/observers/events"
165-
end
166-
167132
s.subspec "imagemanager" do |ss|
168133
ss.source_files = podspec_sources("react/renderer/imagemanager/*.{m,mm,cpp,h}", "react/renderer/imagemanager/*.h")
169134
ss.header_dir = "react/renderer/imagemanager"
@@ -175,20 +140,6 @@ Pod::Spec.new do |s|
175140
ss.header_dir = "react/renderer/mounting"
176141
end
177142

178-
s.subspec "observers" do |ss|
179-
ss.subspec "events" do |sss|
180-
sss.source_files = podspec_sources("react/renderer/observers/events/**/*.{m,mm,cpp,h}", "react/renderer/observers/events/**/*.h")
181-
sss.exclude_files = "react/renderer/observers/events/tests"
182-
sss.header_dir = "react/renderer/observers/events"
183-
end
184-
185-
ss.subspec "intersection" do |sss|
186-
sss.source_files = podspec_sources("react/renderer/observers/intersection/**/*.{m,mm,cpp,h}", "react/renderer/observers/intersection/**/*.h")
187-
sss.exclude_files = "react/renderer/observers/intersection/tests"
188-
sss.header_dir = "react/renderer/observers/intersection"
189-
end
190-
end
191-
192143
s.subspec "telemetry" do |ss|
193144
ss.source_files = podspec_sources("react/renderer/telemetry/**/*.{m,mm,cpp,h}", "react/renderer/telemetry/**/*.h")
194145
ss.exclude_files = "react/renderer/telemetry/tests"
@@ -201,17 +152,6 @@ Pod::Spec.new do |s|
201152
ss.header_dir = "react/renderer/consistency"
202153
end
203154

204-
s.subspec "uimanager" do |ss|
205-
ss.subspec "consistency" do |sss|
206-
sss.source_files = podspec_sources("react/renderer/uimanager/consistency/*.{m,mm,cpp,h}", "react/renderer/uimanager/consistency/*.h")
207-
sss.header_dir = "react/renderer/uimanager/consistency"
208-
end
209-
210-
ss.dependency "React-rendererconsistency"
211-
ss.source_files = podspec_sources("react/renderer/uimanager/*.{m,mm,cpp,h}", "react/renderer/uimanager/*.h")
212-
ss.header_dir = "react/renderer/uimanager"
213-
end
214-
215155
s.subspec "leakchecker" do |ss|
216156
ss.source_files = podspec_sources("react/renderer/leakchecker/**/*.{cpp,h}", "react/renderer/leakchecker/**/*.h")
217157
ss.exclude_files = "react/renderer/leakchecker/tests"

packages/react-native/ReactCommon/React-FabricComponents.podspec

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,77 @@ Pod::Spec.new do |s|
167167
"react/renderer/textlayoutmanager/platform/cxx"
168168
ss.header_dir = "react/renderer/textlayoutmanager"
169169
end
170+
171+
s.subspec "dom" do |ss|
172+
ss.dependency "React-Fabric"
173+
ss.dependency "React-FabricComponents/components/text"
174+
ss.dependency "React-graphics"
175+
ss.source_files = podspec_sources("react/renderer/dom/**/*.{m,mm,cpp,h}", "react/renderer/dom/**/*.{h}")
176+
ss.exclude_files = "react/renderer/dom/tests"
177+
ss.header_dir = "react/renderer/dom"
178+
end
179+
180+
s.subspec "uimanager" do |ss|
181+
ss.dependency "React-Fabric"
182+
ss.dependency "React-FabricComponents/dom"
183+
ss.dependency "React-rendererconsistency"
184+
ss.subspec "consistency" do |sss|
185+
sss.source_files = podspec_sources("react/renderer/uimanager/consistency/*.{m,mm,cpp,h}", "react/renderer/uimanager/consistency/*.h")
186+
sss.header_dir = "react/renderer/uimanager/consistency"
187+
end
188+
ss.source_files = podspec_sources("react/renderer/uimanager/*.{m,mm,cpp,h}", "react/renderer/uimanager/*.h")
189+
ss.header_dir = "react/renderer/uimanager"
190+
end
191+
192+
s.subspec "animationbackend" do |ss|
193+
ss.dependency "React-Fabric"
194+
ss.dependency "React-FabricComponents/uimanager"
195+
ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}")
196+
ss.header_dir = "react/renderer/animationbackend"
197+
end
198+
199+
s.subspec "animated" do |ss|
200+
ss.dependency "React-Fabric"
201+
ss.dependency "React-FabricComponents/animationbackend"
202+
ss.dependency "React-FabricComponents/uimanager"
203+
ss.source_files = podspec_sources("react/renderer/animated/**/*.{m,mm,cpp,h}", "react/renderer/animated/**/*.{h}")
204+
ss.exclude_files = "react/renderer/animated/tests"
205+
ss.header_dir = "react/renderer/animated"
206+
end
207+
208+
s.subspec "animations" do |ss|
209+
ss.dependency "React-Fabric"
210+
ss.source_files = podspec_sources("react/renderer/animations/**/*.{m,mm,cpp,h}", "react/renderer/animations/**/*.{h}")
211+
ss.exclude_files = "react/renderer/animations/tests"
212+
ss.header_dir = "react/renderer/animations"
213+
end
214+
215+
s.subspec "observers" do |ss|
216+
ss.dependency "React-Fabric"
217+
ss.dependency "React-FabricComponents/uimanager"
218+
219+
ss.subspec "events" do |sss|
220+
sss.dependency "React-performancetimeline"
221+
sss.source_files = podspec_sources("react/renderer/observers/events/**/*.{m,mm,cpp,h}", "react/renderer/observers/events/**/*.h")
222+
sss.exclude_files = "react/renderer/observers/events/tests"
223+
sss.header_dir = "react/renderer/observers/events"
224+
end
225+
226+
ss.subspec "intersection" do |sss|
227+
sss.source_files = podspec_sources("react/renderer/observers/intersection/**/*.{m,mm,cpp,h}", "react/renderer/observers/intersection/**/*.h")
228+
sss.exclude_files = "react/renderer/observers/intersection/tests"
229+
sss.header_dir = "react/renderer/observers/intersection"
230+
end
231+
end
232+
233+
s.subspec "scheduler" do |ss|
234+
ss.dependency "React-Fabric"
235+
ss.dependency "React-FabricComponents/uimanager"
236+
ss.dependency "React-FabricComponents/animationbackend"
237+
ss.dependency "React-FabricComponents/observers/events"
238+
ss.dependency "React-performancecdpmetrics"
239+
ss.dependency "React-performancetimeline"
240+
ss.source_files = podspec_sources("react/renderer/scheduler/**/*.{m,mm,cpp,h}", "react/renderer/scheduler/**/*.h")
241+
ss.header_dir = "react/renderer/scheduler"
242+
end
170243
end

packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/React-intersectionobservernativemodule.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Pod::Spec.new do |s|
5858

5959
s.dependency "React-Fabric"
6060
s.dependency "React-Fabric/bridging"
61+
s.dependency "React-FabricComponents"
6162
s.dependency "React-runtimescheduler"
6263
add_dependency(s, "React-RCTFBReactNativeSpec")
6364
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])

packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
*/
77

88
#include "DOM.h"
9+
#include <react/renderer/components/text/ParagraphShadowNode.h>
910
#include <react/renderer/components/text/RawTextShadowNode.h>
11+
#include <react/renderer/components/text/TextShadowNode.h>
1012
#include <react/renderer/core/LayoutMetrics.h>
1113
#include <react/renderer/graphics/Point.h>
1214
#include <react/renderer/graphics/Rect.h>
@@ -280,6 +282,49 @@ DOMRect getBoundingClientRect(
280282
return DOMRect{};
281283
}
282284

285+
// Check if this is a TextShadowNode (virtual text node nested in a paragraph)
286+
auto textShadowNode = dynamic_cast<const TextShadowNode*>(&shadowNode);
287+
if (textShadowNode != nullptr) {
288+
// TextShadowNode is a virtual node that doesn't have its own layout metrics
289+
// For getBoundingClientRect, we return the parent paragraph's bounding rect
290+
// (matching web behavior where inline elements return their container's
291+
// rect) Use getClientRects() to get the individual fragment rects
292+
auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision);
293+
if (ancestors.empty()) {
294+
return DOMRect{};
295+
}
296+
297+
// Find the ParagraphShadowNode in the ancestors
298+
const ParagraphShadowNode* paragraphNode = nullptr;
299+
for (const auto& pair : ancestors) {
300+
paragraphNode =
301+
dynamic_cast<const ParagraphShadowNode*>(&pair.first.get());
302+
if (paragraphNode != nullptr) {
303+
break;
304+
}
305+
}
306+
307+
if (paragraphNode == nullptr) {
308+
return DOMRect{};
309+
}
310+
311+
// Return the paragraph's bounding rect
312+
auto paragraphLayoutMetrics = getLayoutMetricsFromRoot(
313+
*currentRevision,
314+
*paragraphNode,
315+
{.includeTransform = includeTransform, .includeViewportOffset = true});
316+
if (paragraphLayoutMetrics == EmptyLayoutMetrics) {
317+
return DOMRect{};
318+
}
319+
320+
auto frame = paragraphLayoutMetrics.frame;
321+
return DOMRect{
322+
.x = frame.origin.x,
323+
.y = frame.origin.y,
324+
.width = frame.size.width,
325+
.height = frame.size.height};
326+
}
327+
283328
auto layoutMetrics = getLayoutMetricsFromRoot(
284329
*currentRevision,
285330
shadowNode,

packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ describe('ReactNativeElement', () => {
916916
expect(textBoundingRectAfterUnmount.height).toBe(0);
917917
});
918918

919-
it('returns a DOMRect for nested Text elements', () => {
919+
it('returns a DOMRect for nested Text elements that matches parent Paragraph boundaries', () => {
920920
const outerTextRef = createRef<HostInstance>();
921921
const nestedTextRef = createRef<HostInstance>();
922922

@@ -954,15 +954,16 @@ describe('ReactNativeElement', () => {
954954
expect(outerTextBoundingRect.width).toBe(100);
955955
expect(outerTextBoundingRect.height).toBe(50);
956956

957-
// Nested text (virtual text) returns a DOMRect with zero values
958-
// since it doesn't have its own independent layout
957+
// Nested text (virtual text) returns the parent paragraph's bounding
958+
// rect, matching web behavior where inline elements return their
959+
// container's rect
959960
const nestedTextBoundingRect =
960961
nestedTextElement.getBoundingClientRect();
961962
expect(nestedTextBoundingRect).toBeInstanceOf(DOMRect);
962-
expect(nestedTextBoundingRect.x).toBe(0);
963-
expect(nestedTextBoundingRect.y).toBe(0);
964-
expect(nestedTextBoundingRect.width).toBe(0);
965-
expect(nestedTextBoundingRect.height).toBe(0);
963+
expect(nestedTextBoundingRect.x).toBe(10);
964+
expect(nestedTextBoundingRect.y).toBe(20);
965+
expect(nestedTextBoundingRect.width).toBe(100);
966+
expect(nestedTextBoundingRect.height).toBe(50);
966967

967968
// After unmounting, both should return empty DOMRects
968969
Fantom.runTask(() => {

0 commit comments

Comments
 (0)