From f970d5ff325b49d3e675c9e72025834ddc86879d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 17 Oct 2025 18:52:26 -0400 Subject: [PATCH] [DevTools] Highlight the rect when the corresponding timeline bean is hovered (#34881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Screenshot 2025-10-16 at 3 11 17 PM --- .../views/SuspenseTab/SuspenseRects.css | 6 +++--- .../views/SuspenseTab/SuspenseRects.js | 19 +++++++++++++++---- .../views/SuspenseTab/SuspenseScrubber.css | 3 +-- .../views/SuspenseTab/SuspenseTimeline.js | 15 ++++++++++++--- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css index 16d385d752..69a4022307 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -8,8 +8,8 @@ background-color: color-mix(in srgb, var(--color-transition) 5%, transparent); } -.SuspenseRectsContainer:hover:not(:has(.SuspenseRectsBoundary:hover))[data-highlighted='false'] { - outline-width: 1px; +.SuspenseRectsContainer[data-hovered='true'] { + background-color: color-mix(in srgb, var(--color-transition) 15%, transparent); } .SuspenseRectsContainer[data-highlighted='true'] { @@ -65,7 +65,7 @@ } /* highlight this boundary */ -.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect { +.SuspenseRectsBoundary[data-hovered='true'] > .SuspenseRectsRect { background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 50%); transition: background-color 0.2s ease-out; } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index 4a45187d7f..fcbb5f5f74 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -37,6 +37,7 @@ function ScaledRect({ visible, suspended, selected, + hovered, adjust, ...props }: { @@ -45,6 +46,7 @@ function ScaledRect({ visible: boolean, suspended: boolean, selected?: boolean, + hovered?: boolean, adjust?: boolean, ... }): React$Node { @@ -61,6 +63,7 @@ function ScaledRect({ data-visible={visible} data-suspended={suspended} data-selected={selected} + data-hovered={hovered} style={{ // Shrink one pixel so that the bottom outline will line up with the top outline of the next one. width: adjust ? 'calc(' + width + ' - 1px)' : width, @@ -80,7 +83,9 @@ function SuspenseRects({ const store = useContext(StoreContext); const treeDispatch = useContext(TreeDispatcherContext); const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); - const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext); + const {uniqueSuspendersOnly, timeline, hoveredTimelineIndex} = useContext( + SuspenseTreeStateContext, + ); const {inspectedElementID} = useContext(TreeStateContext); @@ -148,6 +153,9 @@ function SuspenseRects({ // TODO: Use the nearest Suspense boundary const selected = inspectedElementID === suspenseID; + const hovered = + hoveredTimelineIndex > -1 && timeline[hoveredTimelineIndex] === suspenseID; + const boundingBox = getBoundingBox(suspense.rects); return ( @@ -156,7 +164,8 @@ function SuspenseRects({ className={styles.SuspenseRectsBoundary} visible={visible} selected={selected} - suspended={suspense.isSuspended}> + suspended={suspense.isSuspended} + hovered={hovered}> {visible && suspense.rects !== null && @@ -317,7 +326,7 @@ function SuspenseRectsContainer(): React$Node { const treeDispatch = useContext(TreeDispatcherContext); const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); // TODO: This relies on a full re-render of all children when the Suspense tree changes. - const {roots} = useContext(SuspenseTreeStateContext); + const {roots, hoveredTimelineIndex} = useContext(SuspenseTreeStateContext); // TODO: bbox does not consider uniqueSuspendersOnly filter const boundingBox = getDocumentBoundingRect(store, roots); @@ -361,13 +370,15 @@ function SuspenseRectsContainer(): React$Node { } const isRootSelected = roots.includes(inspectedElementID); + const isRootHovered = hoveredTimelineIndex === 0; return (
+ data-highlighted={isRootSelected} + data-hovered={isRootHovered}>
.SuspenseScrubberBead, -.SuspenseScrubberStep:hover > .SuspenseScrubberBead { +.SuspenseScrubberStepHighlight > .SuspenseScrubberBead { height: 0.75rem; transition: all 0.3s ease-out; } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 4712397632..af50a8c689 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -53,10 +53,19 @@ function SuspenseTimelineInput() { switchSuspenseNode(timelineIndex); } - function handleHoverSegment(hoveredValue: number) { - // TODO: Consider highlighting the rect instead. + function handleHoverSegment(hoveredIndex: number) { + const nextSelectedSuspenseID = timeline[hoveredIndex]; + suspenseTreeDispatch({ + type: 'HOVER_TIMELINE_FOR_ID', + payload: nextSelectedSuspenseID, + }); + } + function handleUnhoverSegment() { + suspenseTreeDispatch({ + type: 'HOVER_TIMELINE_FOR_ID', + payload: -1, + }); } - function handleUnhoverSegment() {} function skipPrevious() { const nextSelectedSuspenseID = timeline[timelineIndex - 1];