@samitouri / QOS-React-1 / commits / f970d5ff32

[DevTools] Highlight the rect when the corresponding timeline bean is hovered (#34881)

Stacked on #34880. In #34861 I removed the highlight of the real view when hovering the timeline since it was disruptive to stepping through the visuals. This makes it so that when we hover the timeline we highlight the rect with the subtle hover effect added in #34880. We can now just use the one shared state for this and don't need the CSS psuedo-selectors. <img width="603" height="813" alt="Screenshot 2025-10-16 at 3 11 17 PM" src="https://github.com/user-attachments/assets/a018b5ce-dd4d-4e77-ad47-b4ea068f1976" />

Sebastian Markbåge committed Oct 17, 2025 at 18:52 UTC f970d5ff325b49d3e675c9e72025834ddc86879d
4 files changed +31 -12
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
+3 -3
@@ -8,8 +8,8 @@
8 background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
9 }
10
11 -.SuspenseRectsContainer:hover:not(:has(.SuspenseRectsBoundary:hover))[data-highlighted='false'] {
12 - outline-width: 1px;
11 +.SuspenseRectsContainer[data-hovered='true'] {
12 + background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
13 }
14
15 .SuspenseRectsContainer[data-highlighted='true'] {
@@ -65,7 +65,7 @@
65 }
66
67 /* highlight this boundary */
68 -.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect {
68 +.SuspenseRectsBoundary[data-hovered='true'] > .SuspenseRectsRect {
69 background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 50%);
70 transition: background-color 0.2s ease-out;
71 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
+15 -4
@@ -37,6 +37,7 @@ function ScaledRect({
37 visible,
38 suspended,
39 selected,
40 + hovered,
41 adjust,
42 ...props
43 }: {
@@ -45,6 +46,7 @@ function ScaledRect({
46 visible: boolean,
47 suspended: boolean,
48 selected?: boolean,
49 + hovered?: boolean,
50 adjust?: boolean,
51 ...
52 }): React$Node {
@@ -61,6 +63,7 @@ function ScaledRect({
63 data-visible={visible}
64 data-suspended={suspended}
65 data-selected={selected}
66 + data-hovered={hovered}
67 style={{
68 // Shrink one pixel so that the bottom outline will line up with the top outline of the next one.
69 width: adjust ? 'calc(' + width + ' - 1px)' : width,
@@ -80,7 +83,9 @@ function SuspenseRects({
83 const store = useContext(StoreContext);
84 const treeDispatch = useContext(TreeDispatcherContext);
85 const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
83 - const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
86 + const {uniqueSuspendersOnly, timeline, hoveredTimelineIndex} = useContext(
87 + SuspenseTreeStateContext,
88 + );
89
90 const {inspectedElementID} = useContext(TreeStateContext);
91
@@ -148,6 +153,9 @@ function SuspenseRects({
153 // TODO: Use the nearest Suspense boundary
154 const selected = inspectedElementID === suspenseID;
155
156 + const hovered =
157 + hoveredTimelineIndex > -1 && timeline[hoveredTimelineIndex] === suspenseID;
158 +
159 const boundingBox = getBoundingBox(suspense.rects);
160
161 return (
@@ -156,7 +164,8 @@ function SuspenseRects({
164 className={styles.SuspenseRectsBoundary}
165 visible={visible}
166 selected={selected}
159 - suspended={suspense.isSuspended}>
167 + suspended={suspense.isSuspended}
168 + hovered={hovered}>
169 <ViewBox.Provider value={boundingBox}>
170 {visible &&
171 suspense.rects !== null &&
@@ -317,7 +326,7 @@ function SuspenseRectsContainer(): React$Node {
326 const treeDispatch = useContext(TreeDispatcherContext);
327 const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
328 // TODO: This relies on a full re-render of all children when the Suspense tree changes.
320 - const {roots} = useContext(SuspenseTreeStateContext);
329 + const {roots, hoveredTimelineIndex} = useContext(SuspenseTreeStateContext);
330
331 // TODO: bbox does not consider uniqueSuspendersOnly filter
332 const boundingBox = getDocumentBoundingRect(store, roots);
@@ -361,13 +370,15 @@ function SuspenseRectsContainer(): React$Node {
370 }
371
372 const isRootSelected = roots.includes(inspectedElementID);
373 + const isRootHovered = hoveredTimelineIndex === 0;
374
375 return (
376 <div
377 className={styles.SuspenseRectsContainer}
378 onClick={handleClick}
379 onDoubleClick={handleDoubleClick}
370 - data-highlighted={isRootSelected}>
380 + data-highlighted={isRootSelected}
381 + data-hovered={isRootHovered}>
382 <ViewBox.Provider value={boundingBox}>
383 <div
384 className={styles.SuspenseRectsViewBox}
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css
+1 -2
@@ -54,8 +54,7 @@
54 background: var(--color-transition);
55 }
56
57 -.SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
58 -.SuspenseScrubberStep:hover > .SuspenseScrubberBead {
57 +.SuspenseScrubberStepHighlight > .SuspenseScrubberBead {
58 height: 0.75rem;
59 transition: all 0.3s ease-out;
60 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js
+12 -3
@@ -53,10 +53,19 @@ function SuspenseTimelineInput() {
53 switchSuspenseNode(timelineIndex);
54 }
55
56 - function handleHoverSegment(hoveredValue: number) {
57 - // TODO: Consider highlighting the rect instead.
56 + function handleHoverSegment(hoveredIndex: number) {
57 + const nextSelectedSuspenseID = timeline[hoveredIndex];
58 + suspenseTreeDispatch({
59 + type: 'HOVER_TIMELINE_FOR_ID',
60 + payload: nextSelectedSuspenseID,
61 + });
62 + }
63 + function handleUnhoverSegment() {
64 + suspenseTreeDispatch({
65 + type: 'HOVER_TIMELINE_FOR_ID',
66 + payload: -1,
67 + });
68 }
59 - function handleUnhoverSegment() {}
69
70 function skipPrevious() {
71 const nextSelectedSuspenseID = timeline[timelineIndex - 1];