Skip to content

Commit 71aba15

Browse files
committed
Add markers for meaningful boundaries
1 parent c5398d4 commit 71aba15

File tree

3 files changed

+71
-12
lines changed

3 files changed

+71
-12
lines changed

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
padding: 0.25rem;
111111
display: flex;
112112
flex-direction: row;
113+
align-items: flex-start;
113114
}
114115

115116
.Timeline {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
11
.SuspenseTimelineSlider {
22
width: 100%;
33
}
4+
5+
.SuspenseTimelineMarkers {
6+
display: flex;
7+
flex-direction: row;
8+
justify-content: space-between;
9+
}
10+
11+
.SuspenseTimelineMarkers > * {
12+
flex: 1 1 0;
13+
overflow: visible;
14+
visibility: hidden;
15+
width: 0
16+
}
17+
18+
.SuspenseTimelineActiveMarker {
19+
visibility: visible;
20+
}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import type {Element, SuspenseNode} from '../../../frontend/types';
1111
import type Store from '../../store';
1212

1313
import * as React from 'react';
14-
import {useContext, useLayoutEffect, useMemo, useRef} from 'react';
14+
import {
15+
useContext,
16+
useId,
17+
useLayoutEffect,
18+
useMemo,
19+
useRef,
20+
useState,
21+
} from 'react';
1522
import {BridgeContext, StoreContext} from '../context';
1623
import {TreeDispatcherContext} from '../Components/TreeContext';
1724
import {useHighlightHostInstance} from '../hooks';
@@ -87,17 +94,47 @@ export default function SuspenseTimeline(): React$Node {
8794
}, []);
8895

8996
const min = 0;
90-
const max = timeline.length - 1;
97+
const max = timeline.length > 0 ? timeline.length - 1 : 0;
98+
99+
const [value, setValue] = useState(max);
100+
if (value > max) {
101+
// TODO: Handle timeline changes
102+
setValue(max);
103+
}
104+
105+
const markersID = useId();
106+
const markers: React.Node[] = useMemo(() => {
107+
return timeline.map((suspense, index) => {
108+
const takesUpSpace =
109+
suspense.rects !== null &&
110+
suspense.rects.some(rect => {
111+
return rect.width > 0 && rect.height > 0;
112+
});
113+
114+
return takesUpSpace ? (
115+
<option
116+
key={suspense.id}
117+
className={
118+
index === value ? styles.SuspenseTimelineActiveMarker : undefined
119+
}
120+
value={index}>
121+
#{index + 1}
122+
</option>
123+
) : (
124+
<option key={suspense.id} />
125+
);
126+
});
127+
}, [timeline, value]);
91128

92129
function handleChange(event: SyntheticEvent<HTMLInputElement>) {
93-
const value = +event.currentTarget.value;
130+
const pendingValue = +event.currentTarget.value;
94131
for (let i = 0; i < timeline.length; i++) {
95-
const forceFallback = i > value;
132+
const forceFallback = i > pendingValue;
96133
const suspense = timeline[i];
97134
const elementID = suspense.id;
98135
const rendererID = store.getRendererIDForElement(elementID);
99136
if (rendererID === null) {
100-
// TODO: This sounds like a bug.
137+
// TODO: Handle disconnected elements.
101138
console.warn(
102139
`No renderer ID found for element ${elementID} in suspense timeline.`,
103140
);
@@ -110,18 +147,18 @@ export default function SuspenseTimeline(): React$Node {
110147
}
111148
}
112149

113-
const suspense = timeline[value];
150+
const suspense = timeline[pendingValue];
114151
const elementID = suspense.id;
115152
highlightHostInstance(elementID);
116153
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: elementID});
154+
setValue(pendingValue);
117155
}
118156

119157
function handleBlur() {
120158
clearHighlightHostInstance();
121159
}
122160

123-
function handleFocus(event: SyntheticEvent<HTMLInputElement>) {
124-
const value = +event.currentTarget.value;
161+
function handleFocus() {
125162
const suspense = timeline[value];
126163

127164
highlightHostInstance(suspense.id);
@@ -133,7 +170,7 @@ export default function SuspenseTimeline(): React$Node {
133170
throw new Error('Bounding box of slider is unknown.');
134171
}
135172

136-
const value = Math.max(
173+
const hoveredValue = Math.max(
137174
min,
138175
Math.min(
139176
Math.round(
@@ -142,10 +179,10 @@ export default function SuspenseTimeline(): React$Node {
142179
max,
143180
),
144181
);
145-
const suspense = timeline[value];
182+
const suspense = timeline[hoveredValue];
146183
if (suspense === undefined) {
147184
throw new Error(
148-
`Suspense node not found for value ${value} in timeline when on ${event.clientX} in bounding box ${JSON.stringify(bbox)}.`,
185+
`Suspense node not found for value ${hoveredValue} in timeline when on ${event.clientX} in bounding box ${JSON.stringify(bbox)}.`,
149186
);
150187
}
151188
highlightHostInstance(suspense.id);
@@ -158,14 +195,18 @@ export default function SuspenseTimeline(): React$Node {
158195
type="range"
159196
min={min}
160197
max={max}
161-
defaultValue={max}
198+
list={markersID}
199+
value={value}
162200
onBlur={handleBlur}
163201
onChange={handleChange}
164202
onFocus={handleFocus}
165203
onPointerMove={handlePointerMove}
166204
onPointerUp={clearHighlightHostInstance}
167205
ref={inputRef}
168206
/>
207+
<datalist id={markersID} className={styles.SuspenseTimelineMarkers}>
208+
{markers}
209+
</datalist>
169210
</div>
170211
);
171212
}

0 commit comments

Comments
 (0)