Skip to content

Commit c8e0cfb

Browse files
andrewdacenkofacebook-github-bot
authored andcommitted
Add DOM example for getBoundingClientRect and getClientRects
Summary: Adds an interactive example to RNTester demonstrating the DOM APIs: - getBoundingClientRect() for View and Text elements - getClientRects() for nested Text components spanning multiple lines The example includes visual overlays showing the bounding rectangles returned by getClientRects(), making it easy to verify the API behavior. Differential Revision: D91087224
1 parent e4bd884 commit c8e0cfb

3 files changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
12+
13+
import RNTesterText from '../../components/RNTesterText';
14+
import * as React from 'react';
15+
import {useState} from 'react';
16+
import {StyleSheet, Text, View} from 'react-native';
17+
import DOMRect from 'react-native/src/private/webapis/geometry/DOMRect';
18+
19+
function GetBoundingClientRectExample(): React.Node {
20+
const [viewRect, setViewRect] = useState<?DOMRect>(null);
21+
const [textRect, setTextRect] = useState<?DOMRect>(null);
22+
23+
return (
24+
<View style={styles.container}>
25+
<View
26+
style={styles.box}
27+
ref={el => {
28+
const rect = el?.getBoundingClientRect();
29+
if (rect != null && !rectsEqual(rect, viewRect)) {
30+
setViewRect(rect);
31+
}
32+
}}>
33+
<Text>View Element</Text>
34+
</View>
35+
{viewRect != null && (
36+
<RNTesterText style={styles.result}>
37+
View: x={viewRect.x.toFixed(2)}, y={viewRect.y.toFixed(2)}, width=
38+
{viewRect.width.toFixed(2)}, height={viewRect.height.toFixed(2)}
39+
</RNTesterText>
40+
)}
41+
42+
<Text
43+
style={styles.textBox}
44+
ref={el => {
45+
const rect = el?.getBoundingClientRect();
46+
if (rect != null && !rectsEqual(rect, textRect)) {
47+
setTextRect(rect);
48+
}
49+
}}>
50+
Text Element
51+
</Text>
52+
{textRect != null && (
53+
<RNTesterText style={styles.result}>
54+
Text: x={textRect.x.toFixed(2)}, y={textRect.y.toFixed(2)}, width=
55+
{textRect.width.toFixed(2)}, height={textRect.height.toFixed(2)}
56+
</RNTesterText>
57+
)}
58+
</View>
59+
);
60+
}
61+
62+
function GetClientRectsNestedTextExample(): React.Node {
63+
const [paragraphRect, setParagraphRect] = useState<?DOMRect>(null);
64+
const [nestedRect, setNestedRect] = useState<?DOMRect>(null);
65+
const [clientRects, setClientRects] = useState<$ReadOnlyArray<DOMRect>>([]);
66+
67+
// Calculate the base Y offset from the first fragment to position overlays correctly
68+
// The fragment rects have a consistent offset that we correct by using relative positioning
69+
const firstFragmentY = clientRects.length > 0 ? clientRects[0].y : 0;
70+
71+
return (
72+
<View style={styles.container}>
73+
<RNTesterText style={styles.description}>
74+
This example demonstrates getClientRects() for nested Text components.
75+
The nested text spans multiple lines and getClientRects() returns a rect
76+
for each line fragment. Red overlays show each fragment.
77+
</RNTesterText>
78+
79+
<View style={styles.paragraphWrapper}>
80+
<Text
81+
style={styles.paragraph}
82+
ref={el => {
83+
const rect = el?.getBoundingClientRect();
84+
if (rect != null && !rectsEqual(rect, paragraphRect)) {
85+
setParagraphRect(rect);
86+
}
87+
}}>
88+
This is the start of the paragraph.{' '}
89+
<Text
90+
style={styles.nestedText}
91+
ref={el => {
92+
const rect = el?.getBoundingClientRect();
93+
if (rect != null && !rectsEqual(rect, nestedRect)) {
94+
setNestedRect(rect);
95+
}
96+
// $FlowFixMe[prop-missing] - getClientRects is newly added
97+
const rects = el?.getClientRects();
98+
if (rects != null && rects.length !== clientRects.length) {
99+
setClientRects(rects);
100+
}
101+
}}>
102+
This nested text wraps across multiple lines and each line gets its
103+
own client rect from getClientRects
104+
</Text>{' '}
105+
and this is the end.
106+
</Text>
107+
{paragraphRect != null &&
108+
clientRects.map((rect, index) => (
109+
<View
110+
key={index}
111+
style={[
112+
styles.fragmentOverlay,
113+
{
114+
// Position relative to paragraph, using first fragment as Y reference
115+
left: rect.x - paragraphRect.x,
116+
top: rect.y - firstFragmentY,
117+
width: rect.width,
118+
height: rect.height,
119+
},
120+
]}
121+
/>
122+
))}
123+
</View>
124+
125+
<RNTesterText style={styles.sectionHeader}>
126+
getBoundingClientRect() Results
127+
</RNTesterText>
128+
{paragraphRect != null && (
129+
<RNTesterText style={styles.result}>
130+
Paragraph: x={paragraphRect.x.toFixed(2)}, y=
131+
{paragraphRect.y.toFixed(2)}, w={paragraphRect.width.toFixed(2)}, h=
132+
{paragraphRect.height.toFixed(2)}
133+
</RNTesterText>
134+
)}
135+
{nestedRect != null && (
136+
<RNTesterText style={styles.result}>
137+
Nested: x={nestedRect.x.toFixed(2)}, y={nestedRect.y.toFixed(2)}, w=
138+
{nestedRect.width.toFixed(2)}, h={nestedRect.height.toFixed(2)}
139+
</RNTesterText>
140+
)}
141+
<RNTesterText style={styles.note}>
142+
Note: getBoundingClientRect() returns the same rect for nested text as
143+
the parent paragraph.
144+
</RNTesterText>
145+
146+
<RNTesterText style={styles.sectionHeader}>
147+
getClientRects() Results
148+
</RNTesterText>
149+
<RNTesterText style={styles.result}>
150+
Fragment count: {clientRects.length}
151+
</RNTesterText>
152+
{clientRects.map((rect, index) => (
153+
<RNTesterText key={index} style={styles.result}>
154+
Fragment {index}: x={rect.x.toFixed(2)}, y={rect.y.toFixed(2)}, w=
155+
{rect.width.toFixed(2)}, h={rect.height.toFixed(2)}
156+
</RNTesterText>
157+
))}
158+
<RNTesterText style={styles.note}>
159+
Each fragment represents a separate line of the nested text. Red
160+
overlays visualize the fragment boundaries.
161+
</RNTesterText>
162+
</View>
163+
);
164+
}
165+
166+
function GetClientRectsViewExample(): React.Node {
167+
const [containerRect, setContainerRect] = useState<?DOMRect>(null);
168+
const [viewRect, setViewRect] = useState<?DOMRect>(null);
169+
const [clientRects, setClientRects] = useState<$ReadOnlyArray<DOMRect>>([]);
170+
171+
return (
172+
<View
173+
style={styles.container}
174+
ref={el => {
175+
const rect = el?.getBoundingClientRect();
176+
if (rect != null && !rectsEqual(rect, containerRect)) {
177+
setContainerRect(rect);
178+
}
179+
}}>
180+
<RNTesterText style={styles.description}>
181+
For View elements, getClientRects() returns a single rect equivalent to
182+
getBoundingClientRect().
183+
</RNTesterText>
184+
<View
185+
style={styles.box}
186+
ref={el => {
187+
const rect = el?.getBoundingClientRect();
188+
if (rect != null && !rectsEqual(rect, viewRect)) {
189+
setViewRect(rect);
190+
}
191+
// $FlowFixMe[prop-missing] - getClientRects is newly added
192+
const rects = el?.getClientRects();
193+
if (rects != null && rects.length !== clientRects.length) {
194+
setClientRects(rects);
195+
}
196+
}}>
197+
<Text>View with getClientRects()</Text>
198+
</View>
199+
200+
{viewRect != null && (
201+
<RNTesterText style={styles.result}>
202+
getBoundingClientRect(): x={viewRect.x.toFixed(2)}, y=
203+
{viewRect.y.toFixed(2)}, w={viewRect.width.toFixed(2)}, h=
204+
{viewRect.height.toFixed(2)}
205+
</RNTesterText>
206+
)}
207+
208+
<RNTesterText style={styles.result}>
209+
getClientRects() count: {clientRects.length}
210+
</RNTesterText>
211+
{clientRects.map((rect, index) => (
212+
<RNTesterText key={index} style={styles.result}>
213+
Rect {index}: x={rect.x.toFixed(2)}, y={rect.y.toFixed(2)}, w=
214+
{rect.width.toFixed(2)}, h={rect.height.toFixed(2)}
215+
</RNTesterText>
216+
))}
217+
</View>
218+
);
219+
}
220+
221+
function rectsEqual(a: ?DOMRect, b: ?DOMRect): boolean {
222+
if (a == null || b == null) {
223+
return a === b;
224+
}
225+
return (
226+
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
227+
);
228+
}
229+
230+
const styles = StyleSheet.create({
231+
container: {
232+
padding: 10,
233+
},
234+
box: {
235+
backgroundColor: '#e0e0e0',
236+
padding: 20,
237+
marginBottom: 10,
238+
borderRadius: 4,
239+
},
240+
textBox: {
241+
backgroundColor: '#d0e0f0',
242+
padding: 10,
243+
marginBottom: 10,
244+
},
245+
paragraph: {
246+
fontSize: 16,
247+
lineHeight: 24,
248+
marginBottom: 16,
249+
},
250+
nestedText: {
251+
textDecorationLine: 'underline',
252+
color: '#0066cc',
253+
},
254+
description: {
255+
marginBottom: 12,
256+
color: '#666',
257+
},
258+
sectionHeader: {
259+
fontWeight: 'bold',
260+
marginTop: 16,
261+
marginBottom: 8,
262+
},
263+
result: {
264+
fontFamily: 'monospace',
265+
fontSize: 12,
266+
marginBottom: 4,
267+
},
268+
note: {
269+
fontStyle: 'italic',
270+
color: '#888',
271+
marginTop: 8,
272+
fontSize: 12,
273+
},
274+
paragraphWrapper: {
275+
// Container for paragraph and overlays
276+
},
277+
fragmentOverlay: {
278+
backgroundColor: 'rgba(255, 0, 0, 0.3)',
279+
position: 'absolute',
280+
borderWidth: 1,
281+
borderColor: 'rgba(255, 0, 0, 0.5)',
282+
},
283+
});
284+
285+
exports.title = 'DOM';
286+
exports.category = 'Basic';
287+
exports.documentationURL = 'https://reactnative.dev/docs/direct-manipulation';
288+
exports.description = 'DOM APIs for measuring and querying elements';
289+
exports.examples = ([
290+
{
291+
title: 'getBoundingClientRect()',
292+
name: 'getBoundingClientRect',
293+
description:
294+
'Returns the bounding rectangle of an element relative to the viewport.',
295+
render(): React.Node {
296+
return <GetBoundingClientRectExample />;
297+
},
298+
},
299+
{
300+
title: 'getClientRects() - Nested Text',
301+
name: 'getClientRectsNestedText',
302+
description:
303+
'Returns individual rectangles for each line fragment of nested text.',
304+
render(): React.Node {
305+
return <GetClientRectsNestedTextExample />;
306+
},
307+
},
308+
{
309+
title: 'getClientRects() - View',
310+
name: 'getClientRectsView',
311+
description:
312+
'For View elements, getClientRects() returns a single rect matching getBoundingClientRect().',
313+
render(): React.Node {
314+
return <GetClientRectsViewExample />;
315+
},
316+
},
317+
]: Array<RNTesterModuleExample>);

packages/rn-tester/js/utils/RNTesterList.android.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ const APIs: Array<RNTesterModuleInfo> = ([
219219
category: 'UI',
220220
module: require('../examples/Dimensions/DimensionsExample'),
221221
},
222+
{
223+
key: 'DOMExample',
224+
category: 'Basic',
225+
module: require('../examples/DOM/DOMExample'),
226+
},
222227
{
223228
key: 'DisplayContentsExample',
224229
category: 'UI',

packages/rn-tester/js/utils/RNTesterList.ios.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ const APIs: Array<RNTesterModuleInfo> = ([
221221
key: 'Dimensions',
222222
module: require('../examples/Dimensions/DimensionsExample'),
223223
},
224+
{
225+
key: 'DOMExample',
226+
category: 'Basic',
227+
module: require('../examples/DOM/DOMExample'),
228+
},
224229
{
225230
key: 'DisplayContentsExample',
226231
category: 'UI',

0 commit comments

Comments
 (0)