Skip to content

Commit 632176d

Browse files
committed
Fix WhatChanged scrolling out of view in profiler
Previously, WhatChanged and InspectedElementBadges were rendered inside the same scrollable Content div as the commit list. When a component had many render timestamps, scrolling down to pick one caused the "what changed" context to scroll out of view, requiring the user to scroll back up to see it. While the profiler already exposes render details via hover tooltips on fibers, those are transient and pointer-dependent. This change introduces a sticky header section at the top of the Content div that holds the current-commit summary, badges, and WhatChanged. The section is collapsible (click or Enter/Space) so users can reclaim vertical space for the commit list when needed. The commit list now sits below the sticky header as a sibling, so it scrolls independently while the summary remains always visible. References: - https://github.com/facebook/react/blob/main/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js#L130-L147
1 parent 733d3aa commit 632176d

File tree

5 files changed

+160
-15
lines changed

5 files changed

+160
-15
lines changed
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
.Root {
22
user-select: none;
3-
display: inline-flex;
4-
}
5-
6-
.Root *:not(:first-child) {
7-
margin-left: 0.25rem;
3+
flex: 1 1 auto;
4+
min-width: 0;
5+
display: flex;
6+
flex-wrap: wrap;
7+
justify-content: start;
8+
gap: 0.25rem;
89
}

packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
border-bottom: 1px solid var(--color-border);
88
}
99

10+
.CurrentRenderInfo strong {
11+
color: var(--color-text-primary);
12+
}
13+
1014
.Content {
11-
padding: 0.5rem;
15+
padding: 0 0.5rem 0.5rem;
1216
user-select: none;
13-
overflow-y: auto;
1417
display: flex;
1518
flex-direction: column;
1619
gap: 0.5rem;

packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ButtonIcon from '../ButtonIcon';
1919
import InspectedElementBadges from '../Components/InspectedElementBadges';
2020

2121
import styles from './SidebarSelectedFiberInfo.css';
22+
import StickyCollapsibleHeader from './StickyCollapsibleHeader';
2223

2324
export default function SidebarSelectedFiberInfo(): React.Node {
2425
const {profilerStore} = useContext(StoreContext);
@@ -114,6 +115,18 @@ export default function SidebarSelectedFiberInfo(): React.Node {
114115
);
115116
}
116117

118+
let selectedCommitTime = 0;
119+
let selectedCommitDuration = 0;
120+
121+
if (selectedCommitIndex !== null && rootID !== null) {
122+
const commitData = profilerStore.getCommitData(
123+
((rootID: any): number),
124+
selectedCommitIndex,
125+
);
126+
selectedCommitTime = commitData.timestamp;
127+
selectedCommitDuration = commitData.duration;
128+
}
129+
117130
return (
118131
<Fragment>
119132
<div className={styles.Toolbar}>
@@ -127,14 +140,24 @@ export default function SidebarSelectedFiberInfo(): React.Node {
127140
<ButtonIcon type="close" />
128141
</Button>
129142
</div>
130-
<div className={styles.Content} onKeyDown={handleKeyDown} tabIndex={0}>
131-
{node != null && (
132-
<InspectedElementBadges
133-
hocDisplayNames={node.hocDisplayNames}
134-
compiledWithForget={node.compiledWithForget}
135-
/>
136-
)}
137-
<WhatChanged fiberID={((selectedFiberID: any): number)} />
143+
<div className={styles.Content} onKeyDown={handleKeyDown}>
144+
<StickyCollapsibleHeader
145+
summary={
146+
<span className={styles.CurrentRenderInfo}>
147+
<strong>
148+
{formatTime(selectedCommitTime)}s for{' '}
149+
{formatDuration(selectedCommitDuration)}ms
150+
</strong>
151+
</span>
152+
}>
153+
{node != null && (
154+
<InspectedElementBadges
155+
hocDisplayNames={node.hocDisplayNames}
156+
compiledWithForget={node.compiledWithForget}
157+
/>
158+
)}
159+
<WhatChanged fiberID={((selectedFiberID: any): number)} />
160+
</StickyCollapsibleHeader>
138161
{listItems.length > 0 && (
139162
<div>
140163
<label className={styles.Label}>Rendered at: </label>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.StickyHeader {
2+
position: sticky;
3+
top: 0;
4+
z-index: 1;
5+
display: flex;
6+
flex-direction: column;
7+
gap: 0.5rem;
8+
padding: 0.5rem 0;
9+
background: var(--color-background);
10+
}
11+
12+
.StickyHeader::after {
13+
content: "";
14+
position: absolute;
15+
inset-inline: -0.5rem;
16+
bottom: 0;
17+
height: 1px;
18+
background: var(--color-border);
19+
}
20+
21+
.HeaderRow {
22+
display: flex;
23+
justify-content: space-between;
24+
align-items: center;
25+
gap: 0.5rem;
26+
cursor: pointer;
27+
user-select: none;
28+
border-radius: 4px;
29+
padding: 0.25rem 0;
30+
}
31+
32+
.HeaderRow:hover .RenderSummary {
33+
text-decoration: underline;
34+
}
35+
36+
.HeaderRow:focus-visible {
37+
background: var(--color-button-background-focus);
38+
outline: none;
39+
}
40+
41+
.HeaderRowControls {
42+
flex: 0 0 auto;
43+
}
44+
45+
.RenderSummary {
46+
flex: 1;
47+
min-width: 0;
48+
color: var(--color-text-secondary);
49+
}
50+
51+
.CollapsibleContent {
52+
display: flex;
53+
flex-direction: column;
54+
gap: 0.75rem;
55+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import {useState} from 'react';
12+
13+
import ButtonIcon from '../ButtonIcon';
14+
15+
import styles from './StickyCollapsibleHeader.css';
16+
17+
type Props = {
18+
summary: React.Node,
19+
children: React.Node,
20+
};
21+
22+
export default function StickyCollapsibleHeader({
23+
summary,
24+
children,
25+
}: Props): React.Node {
26+
const [isCollapsed, setIsCollapsed] = useState(false);
27+
28+
const toggleCollapsedVal = () =>
29+
setIsCollapsed(prevIsCollapsed => !prevIsCollapsed);
30+
31+
// $FlowFixMe[missing-local-annot]
32+
const handleKeyDown = event => {
33+
if (event.key === 'Enter' || event.key === ' ') {
34+
// Prevent the browser from scrolling down when Space is pressed
35+
if (event.key === ' ') {
36+
event.preventDefault();
37+
}
38+
toggleCollapsedVal();
39+
}
40+
};
41+
42+
return (
43+
<div className={styles.StickyHeader}>
44+
<div
45+
className={styles.HeaderRow}
46+
onClick={toggleCollapsedVal}
47+
tabIndex={0}
48+
role="button"
49+
aria-expanded={!isCollapsed}
50+
onKeyDown={handleKeyDown}>
51+
<div className={styles.RenderSummary}>{summary}</div>
52+
53+
<div className={styles.HeaderRowControls}>
54+
<ButtonIcon type={isCollapsed ? 'expanded' : 'collapsed'} />
55+
</div>
56+
</div>
57+
58+
{!isCollapsed && (
59+
<div className={styles.CollapsibleContent}>{children}</div>
60+
)}
61+
</div>
62+
);
63+
}

0 commit comments

Comments
 (0)