Skip to content

Commit 476f538

Browse files
authored
Add getClientRects to fragment instances (#32660)
Adds `getClientRects()` to fragment instances with a fixture test case. `Element.getClientRect` returns a collection of `DOMRect`s (see example of multiline span returning two `DOMRect` boxes). `fragmentInstance.getClientRects` here flattens those collections into an array of rects.
1 parent c69a5fc commit 476f538

File tree

10 files changed

+266
-76
lines changed

10 files changed

+266
-76
lines changed

fixtures/dom/src/components/Fixture.js

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ class Fixture extends React.Component {
1616
Fixture.propTypes = propTypes;
1717

1818
export default Fixture;
19+
20+
Fixture.Controls = function FixtureControls({children}) {
21+
return <div className="test-fixture__controls">{children}</div>;
22+
};

fixtures/dom/src/components/fixtures/fragment-refs/EventListenerCase.js

+19-19
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default function EventListenerCase() {
4949
</TestCase.ExpectedResult>
5050

5151
<Fixture>
52-
<div className="control-box">
52+
<Fixture.Controls>
5353
<div>Target count: {extraChildCount + 3}</div>
5454
<button
5555
onClick={() => {
@@ -69,26 +69,26 @@ export default function EventListenerCase() {
6969
}}>
7070
Remove click event listeners
7171
</button>
72-
<div class="card-container">
73-
<Fragment ref={fragmentRef}>
74-
<div className="card" id="child-a">
75-
Child A
72+
</Fixture.Controls>
73+
<div className="card-container">
74+
<Fragment ref={fragmentRef}>
75+
<div className="card" id="child-a">
76+
Child A
77+
</div>
78+
<div className="card" id="child-b">
79+
Child B
80+
</div>
81+
<WrapperComponent>
82+
<div className="card" id="child-c">
83+
Child C
7684
</div>
77-
<div className="card" id="child-b">
78-
Child B
79-
</div>
80-
<WrapperComponent>
81-
<div className="card" id="child-c">
82-
Child C
85+
{Array.from({length: extraChildCount}).map((_, index) => (
86+
<div className="card" id={'extra-child-' + index} key={index}>
87+
Extra Child {index}
8388
</div>
84-
{Array.from({length: extraChildCount}).map((_, index) => (
85-
<div className="card" id={'extra-child-' + index} key={index}>
86-
Extra Child {index}
87-
</div>
88-
))}
89-
</WrapperComponent>
90-
</Fragment>
91-
</div>
89+
))}
90+
</WrapperComponent>
91+
</Fragment>
9292
</div>
9393
</Fixture>
9494
</TestCase>

fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@ export default function FocusCase() {
3131
</p>
3232
</TestCase.ExpectedResult>
3333

34-
<button onClick={() => fragmentRef.current.focus()}>
35-
Focus first child
36-
</button>
37-
<button onClick={() => fragmentRef.current.focusLast()}>
38-
Focus last child
39-
</button>
40-
<button onClick={() => fragmentRef.current.blur()}>Blur</button>
41-
4234
<Fixture>
35+
<Fixture.Controls>
36+
<button onClick={() => fragmentRef.current.focus()}>
37+
Focus first child
38+
</button>
39+
<button onClick={() => fragmentRef.current.focusLast()}>
40+
Focus last child
41+
</button>
42+
<button onClick={() => fragmentRef.current.blur()}>Blur</button>
43+
</Fixture.Controls>
4344
<div className="highlight-focused-children" style={{display: 'flex'}}>
4445
<Fragment ref={fragmentRef}>
4546
<div style={{outline: '1px solid black'}}>Unfocusable div</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import TestCase from '../../TestCase';
2+
import Fixture from '../../Fixture';
3+
4+
const React = window.React;
5+
const {Fragment, useEffect, useRef, useState} = React;
6+
7+
export default function GetClientRectsCase() {
8+
const fragmentRef = useRef(null);
9+
const [rects, setRects] = useState([]);
10+
const getRects = () => {
11+
const rects = fragmentRef.current.getClientRects();
12+
setRects(rects);
13+
};
14+
15+
return (
16+
<TestCase title="getClientRects">
17+
<TestCase.Steps>
18+
<li>
19+
Click the "Print Rects" button to get the client rects of the
20+
elements.
21+
</li>
22+
</TestCase.Steps>
23+
<TestCase.ExpectedResult>
24+
Calling getClientRects on the fragment instance will return a list of a
25+
DOMRectList for each child node.
26+
</TestCase.ExpectedResult>
27+
<Fixture>
28+
<Fixture.Controls>
29+
<button onClick={getRects}>Print Rects</button>
30+
<div style={{display: 'flex'}}>
31+
<div
32+
style={{
33+
position: 'relative',
34+
width: '30vw',
35+
height: '30vh',
36+
border: '1px solid black',
37+
}}>
38+
{rects.map(({x, y, width, height}, index) => {
39+
const scale = 0.3;
40+
41+
return (
42+
<div
43+
key={index}
44+
style={{
45+
position: 'absolute',
46+
top: y * scale,
47+
left: x * scale,
48+
width: width * scale,
49+
height: height * scale,
50+
border: '1px solid red',
51+
boxSizing: 'border-box',
52+
}}></div>
53+
);
54+
})}
55+
</div>
56+
<div>
57+
{rects.map(({x, y, width, height}, index) => {
58+
return (
59+
<div>
60+
{index} :: {`{`}x: {x}, y: {y}, width: {width}, height:{' '}
61+
{height}
62+
{`}`}
63+
</div>
64+
);
65+
})}
66+
</div>
67+
</div>
68+
</Fixture.Controls>
69+
<Fragment ref={fragmentRef}>
70+
<span
71+
style={{
72+
width: '300px',
73+
height: '250px',
74+
backgroundColor: 'lightblue',
75+
fontSize: 20,
76+
border: '1px solid black',
77+
marginBottom: '10px',
78+
}}>
79+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
80+
eiusmod tempor incididunt ut labore et dolore magna aliqua.
81+
</span>
82+
<div
83+
style={{
84+
width: '150px',
85+
height: '100px',
86+
backgroundColor: 'lightgreen',
87+
border: '1px solid black',
88+
}}></div>
89+
<div
90+
style={{
91+
width: '500px',
92+
height: '50px',
93+
backgroundColor: 'lightpink',
94+
border: '1px solid black',
95+
}}></div>
96+
</Fragment>
97+
</Fixture>
98+
</TestCase>
99+
);
100+
}

fixtures/dom/src/components/fixtures/fragment-refs/IntersectionObserverCase.js

+49-47
Original file line numberDiff line numberDiff line change
@@ -90,53 +90,55 @@ export default function IntersectionObserverCase() {
9090
</p>
9191
</TestCase.ExpectedResult>
9292
<Fixture>
93-
<button
94-
onClick={() => {
95-
setItems(prev => [
96-
...prev,
97-
[`Extra child: ${prev.length + 1}`, false],
98-
]);
99-
}}>
100-
Add Child
101-
</button>
102-
<button
103-
onClick={() => {
104-
setItems(prev => {
105-
if (prev.length === 3) {
106-
return prev;
107-
}
108-
return prev.slice(0, prev.length - 1);
109-
});
110-
}}>
111-
Remove Child
112-
</button>
113-
<button
114-
onClick={() => {
115-
fragmentRef.current.observeUsing(observerRef.current);
116-
}}>
117-
Observe
118-
</button>
119-
<button
120-
onClick={() => {
121-
fragmentRef.current.unobserveUsing(observerRef.current);
122-
setItems(prev => {
123-
return prev.map(item => [item[0], false]);
124-
});
125-
}}>
126-
Unobserve
127-
</button>
128-
{anyOnScreen && (
129-
<div className="fixed-sidebar card-container">
130-
<p>
131-
<strong>Children on screen:</strong>
132-
</p>
133-
{items.map(item => (
134-
<div className={`card ${item[1] ? 'onscreen' : null}`}>
135-
{item[0]}
136-
</div>
137-
))}
138-
</div>
139-
)}
93+
<Fixture.Controls>
94+
<button
95+
onClick={() => {
96+
setItems(prev => [
97+
...prev,
98+
[`Extra child: ${prev.length + 1}`, false],
99+
]);
100+
}}>
101+
Add Child
102+
</button>
103+
<button
104+
onClick={() => {
105+
setItems(prev => {
106+
if (prev.length === 3) {
107+
return prev;
108+
}
109+
return prev.slice(0, prev.length - 1);
110+
});
111+
}}>
112+
Remove Child
113+
</button>
114+
<button
115+
onClick={() => {
116+
fragmentRef.current.observeUsing(observerRef.current);
117+
}}>
118+
Observe
119+
</button>
120+
<button
121+
onClick={() => {
122+
fragmentRef.current.unobserveUsing(observerRef.current);
123+
setItems(prev => {
124+
return prev.map(item => [item[0], false]);
125+
});
126+
}}>
127+
Unobserve
128+
</button>
129+
{anyOnScreen && (
130+
<div className="fixed-sidebar card-container">
131+
<p>
132+
<strong>Children on screen:</strong>
133+
</p>
134+
{items.map(item => (
135+
<div className={`card ${item[1] ? 'onscreen' : null}`}>
136+
{item[0]}
137+
</div>
138+
))}
139+
</div>
140+
)}
141+
</Fixture.Controls>
140142
<Fragment ref={fragmentRef}>
141143
<ObservedChild id="A" />
142144
<WrapperComponent>

fixtures/dom/src/components/fixtures/fragment-refs/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import EventListenerCase from './EventListenerCase';
33
import IntersectionObserverCase from './IntersectionObserverCase';
44
import ResizeObserverCase from './ResizeObserverCase';
55
import FocusCase from './FocusCase';
6+
import GetClientRectsCase from './GetClientRectsCase';
67

78
const React = window.React;
89

@@ -13,6 +14,7 @@ export default function FragmentRefsPage() {
1314
<IntersectionObserverCase />
1415
<ResizeObserverCase />
1516
<FocusCase />
17+
<GetClientRectsCase />
1618
</FixtureSet>
1719
);
1820
}

fixtures/dom/src/style.css

+8-2
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ p {
224224
}
225225

226226
.test-case__body {
227-
padding: 10px;
227+
padding: 10px 10px 0 10px;
228228
}
229229

230230
.test-case__desc {
@@ -280,11 +280,17 @@ p {
280280

281281
.test-fixture {
282282
padding: 20px;
283-
margin: 0 -15px; /* opposite of .test-case padding */
283+
margin: 0 -10px; /* opposite of .test-case padding */
284284
background-color: #f4f4f4;
285285
border-top: 1px solid #d9d9d9;
286286
}
287287

288+
.test-fixture__controls {
289+
margin: -20px -20px 20px -20px;
290+
padding: 20px;
291+
border: 1px solid #444;
292+
}
293+
288294
.field-group {
289295
overflow: hidden;
290296
}

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+14
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,7 @@ export type FragmentInstanceType = {
22292229
blur(): void,
22302230
observeUsing(observer: IntersectionObserver | ResizeObserver): void,
22312231
unobserveUsing(observer: IntersectionObserver | ResizeObserver): void,
2232+
getClientRects(): Array<DOMRect>,
22322233
};
22332234

22342235
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -2406,6 +2407,19 @@ function unobserveChild(
24062407
observer.unobserve(child);
24072408
return false;
24082409
}
2410+
// $FlowFixMe[prop-missing]
2411+
FragmentInstance.prototype.getClientRects = function (
2412+
this: FragmentInstanceType,
2413+
): Array<DOMRect> {
2414+
const rects: Array<DOMRect> = [];
2415+
traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects);
2416+
return rects;
2417+
};
2418+
function collectClientRects(child: Instance, rects: Array<DOMRect>): boolean {
2419+
// $FlowFixMe[method-unbinding]
2420+
rects.push.apply(rects, child.getClientRects());
2421+
return false;
2422+
}
24092423

24102424
function normalizeListenerOptions(
24112425
opts: ?EventListenerOptionsOrUseCapture,

0 commit comments

Comments
 (0)