Skip to content

Commit d15d7fd

Browse files
authored
[DevTools] Double click a Suspense Rect to jump to its position in the timeline (#34642)
When you double click it will hide or show by jumping to the selected index or one step before the selected. Let's you go from a suspense boundary into the timeline to find its position. I also highlight the step in the timeline when you hover the rect. This only works if it's in the selected root but all of those should be merged into one single timeline. One thing that's weird about the SuspenseNodes now is that they sometimes gets deleted but not always when they're resupended. Nested ones maybe? This means that if you double click to hide it, you can't double click again to show it. This seems like an unrelated bug that we should fix. We could potentially repurpose the existing "Suspend" button in the toolbar to do this too, or maybe add another icon there.
1 parent 8674c3b commit d15d7fd

File tree

5 files changed

+88
-2
lines changed

5 files changed

+88
-2
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,29 @@ function SuspenseRects({
9898
});
9999
}
100100

101+
function handleDoubleClick(event: SyntheticMouseEvent) {
102+
if (event.defaultPrevented) {
103+
// Already clicked on an inner rect
104+
return;
105+
}
106+
event.preventDefault();
107+
suspenseTreeDispatch({
108+
type: 'TOGGLE_TIMELINE_FOR_ID',
109+
payload: suspenseID,
110+
});
111+
}
112+
101113
function handlePointerOver(event: SyntheticPointerEvent) {
102114
if (event.defaultPrevented) {
103115
// Already hovered an inner rect
104116
return;
105117
}
106118
event.preventDefault();
107119
highlightHostInstance(suspenseID);
120+
suspenseTreeDispatch({
121+
type: 'HOVER_TIMELINE_FOR_ID',
122+
payload: suspenseID,
123+
});
108124
}
109125

110126
function handlePointerLeave(event: SyntheticPointerEvent) {
@@ -114,6 +130,10 @@ function SuspenseRects({
114130
}
115131
event.preventDefault();
116132
clearHighlightHostInstance();
133+
suspenseTreeDispatch({
134+
type: 'HOVER_TIMELINE_FOR_ID',
135+
payload: -1,
136+
});
117137
}
118138

119139
// TODO: Use the nearest Suspense boundary
@@ -137,6 +157,7 @@ function SuspenseRects({
137157
rect={rect}
138158
data-highlighted={selected}
139159
onClick={handleClick}
160+
onDoubleClick={handleDoubleClick}
140161
onPointerOver={handlePointerOver}
141162
onPointerLeave={handlePointerLeave}
142163
// Reach-UI tooltip will go out of bounds of parent scroll container.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
background: var(--color-background-selected);
5252
}
5353

54+
.SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
55+
.SuspenseScrubberStepHighlight > .SuspenseScrubberBeadSelected,
5456
.SuspenseScrubberStep:hover > .SuspenseScrubberBead,
5557
.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected {
5658
height: 0.75rem;

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default function SuspenseScrubber({
1818
min,
1919
max,
2020
value,
21+
highlight,
2122
onBlur,
2223
onChange,
2324
onFocus,
@@ -27,6 +28,7 @@ export default function SuspenseScrubber({
2728
min: number,
2829
max: number,
2930
value: number,
31+
highlight: number,
3032
onBlur: () => void,
3133
onChange: (index: number) => void,
3234
onFocus: () => void,
@@ -53,7 +55,12 @@ export default function SuspenseScrubber({
5355
steps.push(
5456
<div
5557
key={index}
56-
className={styles.SuspenseScrubberStep}
58+
className={
59+
styles.SuspenseScrubberStep +
60+
(highlight === index
61+
? ' ' + styles.SuspenseScrubberStepHighlight
62+
: '')
63+
}
5764
onPointerDown={handlePress.bind(null, index)}
5865
onMouseEnter={onHoverSegment.bind(null, index)}>
5966
<div

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function SuspenseTimelineInput() {
3333
selectedRootID: rootID,
3434
timeline,
3535
timelineIndex,
36+
hoveredTimelineIndex,
3637
playing,
3738
} = useContext(SuspenseTreeStateContext);
3839

@@ -202,6 +203,7 @@ function SuspenseTimelineInput() {
202203
min={min}
203204
max={max}
204205
value={timelineIndex}
206+
highlight={hoveredTimelineIndex}
205207
onBlur={handleBlur}
206208
onChange={handleChange}
207209
onFocus={handleFocus}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type SuspenseTreeState = {
3131
selectedSuspenseID: SuspenseNode['id'] | null,
3232
timeline: $ReadOnlyArray<SuspenseNode['id']>,
3333
timelineIndex: number | -1,
34+
hoveredTimelineIndex: number | -1,
3435
uniqueSuspendersOnly: boolean,
3536
playing: boolean,
3637
};
@@ -72,6 +73,14 @@ type ACTION_SUSPENSE_PLAY_PAUSE = {
7273
type ACTION_SUSPENSE_PLAY_TICK = {
7374
type: 'SUSPENSE_PLAY_TICK',
7475
};
76+
type ACTION_TOGGLE_TIMELINE_FOR_ID = {
77+
type: 'TOGGLE_TIMELINE_FOR_ID',
78+
payload: SuspenseNode['id'],
79+
};
80+
type ACTION_HOVER_TIMELINE_FOR_ID = {
81+
type: 'HOVER_TIMELINE_FOR_ID',
82+
payload: SuspenseNode['id'],
83+
};
7584

7685
export type SuspenseTreeAction =
7786
| ACTION_SUSPENSE_TREE_MUTATION
@@ -81,7 +90,9 @@ export type SuspenseTreeAction =
8190
| ACTION_SUSPENSE_SET_TIMELINE_INDEX
8291
| ACTION_SUSPENSE_SKIP_TIMELINE_INDEX
8392
| ACTION_SUSPENSE_PLAY_PAUSE
84-
| ACTION_SUSPENSE_PLAY_TICK;
93+
| ACTION_SUSPENSE_PLAY_TICK
94+
| ACTION_TOGGLE_TIMELINE_FOR_ID
95+
| ACTION_HOVER_TIMELINE_FOR_ID;
8596
export type SuspenseTreeDispatch = (action: SuspenseTreeAction) => void;
8697

8798
const SuspenseTreeStateContext: ReactContext<SuspenseTreeState> =
@@ -122,6 +133,7 @@ function getInitialState(store: Store): SuspenseTreeState {
122133
selectedRootID,
123134
timeline: [],
124135
timelineIndex: -1,
136+
hoveredTimelineIndex: -1,
125137
uniqueSuspendersOnly,
126138
playing: false,
127139
};
@@ -144,6 +156,7 @@ function getInitialState(store: Store): SuspenseTreeState {
144156
selectedRootID,
145157
timeline,
146158
timelineIndex,
159+
hoveredTimelineIndex: -1,
147160
uniqueSuspendersOnly,
148161
playing: false,
149162
};
@@ -397,6 +410,47 @@ function SuspenseTreeContextController({children}: Props): React.Node {
397410
playing: nextPlaying,
398411
};
399412
}
413+
case 'TOGGLE_TIMELINE_FOR_ID': {
414+
const suspenseID = action.payload;
415+
const timelineIndexForSuspenseID =
416+
state.timeline.indexOf(suspenseID);
417+
if (timelineIndexForSuspenseID === -1) {
418+
// This boundary is no longer in the timeline.
419+
return state;
420+
}
421+
const nextTimelineIndex =
422+
timelineIndexForSuspenseID === 0
423+
? // For roots, there's no toggling. It's always just jump to beginning.
424+
0
425+
: // For boundaries, we'll either jump to before or after its reveal depending
426+
// on if we're currently displaying it or not according to the timeline.
427+
state.timelineIndex < timelineIndexForSuspenseID
428+
? // We're currently before this suspense boundary has been revealed so we
429+
// should jump ahead to reveal it.
430+
timelineIndexForSuspenseID
431+
: // Otherwise, if we're currently showing it, jump to right before to hide it.
432+
timelineIndexForSuspenseID - 1;
433+
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
434+
const nextLineage = store.getSuspenseLineage(
435+
nextSelectedSuspenseID,
436+
);
437+
return {
438+
...state,
439+
lineage: nextLineage,
440+
selectedSuspenseID: nextSelectedSuspenseID,
441+
timelineIndex: nextTimelineIndex,
442+
playing: false, // pause
443+
};
444+
}
445+
case 'HOVER_TIMELINE_FOR_ID': {
446+
const suspenseID = action.payload;
447+
const timelineIndexForSuspenseID =
448+
state.timeline.indexOf(suspenseID);
449+
return {
450+
...state,
451+
hoveredTimelineIndex: timelineIndexForSuspenseID,
452+
};
453+
}
400454
default:
401455
throw new Error(`Unrecognized action "${action.type}"`);
402456
}

0 commit comments

Comments
 (0)