Skip to content

Commit 2622487

Browse files
authored
[DevTools] Move Timeline to footer instead of header (#34617)
One thing that always bothered me is that the collapse buttons on either side of the toolbar looks like left/right buttons which would conflict with some steps buttons I plan to add. Another issue is that we'll need to add more tool buttons to the top and probably eventually a Search field. Ideally this whole section should line up vertically with the height of the title row. I also realized that all UIs that have some kind of timeline control (and play/pause/skip) do that in the bottom below the content. E.g. music players and video players all do that. We're better off playing into that structure since that's the UI analogy we're going for here. Makes it clearer what the weird timeline is for. By moving it to the bottom it also frees up the top for the collapse buttons and more controls. __Horizontal__ <img width="794" height="809" alt="Screenshot 2025-09-26 at 3 40 35 PM" src="https://github.com/user-attachments/assets/dacad9c4-d52f-4b66-9585-5cc74f230e6f" /> __Vertical__ <img width="570" height="812" alt="Screenshot 2025-09-26 at 3 40 53 PM" src="https://github.com/user-attachments/assets/db225413-849e-46f1-b764-8fbd08b395c4" />
1 parent 8a24ef3 commit 2622487

File tree

3 files changed

+134
-100
lines changed

3 files changed

+134
-100
lines changed

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@
4949
cursor: ew-resize;
5050
}
5151

52-
.TreeView footer {
53-
display: none;
54-
}
55-
5652
@container devtools (width < 600px) {
5753
.SuspenseTab {
5854
flex-direction: column;
@@ -76,13 +72,13 @@
7672
cursor: ns-resize;
7773
}
7874

79-
.TreeView footer {
80-
display: flex;
81-
justify-content: end;
82-
border-top: 1px solid var(--color-border);
75+
.ToggleInspectedElement[data-orientation="horizontal"] {
76+
display: none;
8377
}
78+
}
8479

85-
.ToggleInspectedElement[data-orientation="horizontal"] {
80+
@container devtools (width >= 600px) {
81+
.ToggleInspectedElement[data-orientation="vertical"] {
8682
display: none;
8783
}
8884
}
@@ -103,22 +99,18 @@
10399
}
104100

105101
.Rects {
106-
border-top: 1px solid var(--color-border);
107102
padding: 0.25rem;
108103
flex-grow: 1;
109104
overflow: auto;
110105
}
111106

112107
.SuspenseTreeViewHeader {
113-
padding: 0.25rem;
108+
flex: 0 0 42px;
109+
padding: 0.5rem;
114110
display: grid;
115-
grid-template-columns: auto 1fr auto;
111+
grid-template-columns: auto 1fr auto auto auto;
116112
align-items: flex-start;
117-
}
118-
119-
.SuspenseTreeViewHeaderMain {
120-
display: grid;
121-
grid-template-rows: auto auto;
113+
border-bottom: 1px solid var(--color-border);
122114
}
123115

124116
.SuspenseBreadcrumbs {
@@ -128,3 +120,14 @@
128120
*/
129121
overflow-x: auto;
130122
}
123+
124+
.SuspenseTreeViewFooter {
125+
flex: 0 0 42px;
126+
display: flex;
127+
justify-content: end;
128+
border-top: 1px solid var(--color-border);
129+
padding: 0.5rem;
130+
display: grid;
131+
grid-template-columns: 1fr auto;
132+
align-items: flex-start;
133+
}

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

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
*/
99

1010
import * as React from 'react';
11-
import {useEffect, useLayoutEffect, useReducer, useRef} from 'react';
11+
import {
12+
useContext,
13+
useEffect,
14+
useLayoutEffect,
15+
useReducer,
16+
useRef,
17+
} from 'react';
1218

1319
import {
1420
localStorageGetItem,
@@ -23,8 +29,18 @@ import SuspenseBreadcrumbs from './SuspenseBreadcrumbs';
2329
import SuspenseRects from './SuspenseRects';
2430
import SuspenseTimeline from './SuspenseTimeline';
2531
import SuspenseTreeList from './SuspenseTreeList';
32+
import {
33+
SuspenseTreeDispatcherContext,
34+
SuspenseTreeStateContext,
35+
} from './SuspenseTreeContext';
36+
import {StoreContext} from '../context';
37+
import {TreeDispatcherContext} from '../Components/TreeContext';
2638
import Button from '../Button';
27-
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
39+
import Tooltip from '../Components/reach-ui/tooltip';
40+
import typeof {
41+
SyntheticEvent,
42+
SyntheticPointerEvent,
43+
} from 'react-dom-bindings/src/events/SyntheticEvent';
2844

2945
type Orientation = 'horizontal' | 'vertical';
3046

@@ -48,6 +64,91 @@ type LayoutState = {
4864
};
4965
type LayoutDispatch = (action: LayoutAction) => void;
5066

67+
function ToggleUniqueSuspenders() {
68+
const store = useContext(StoreContext);
69+
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
70+
71+
const {selectedRootID: rootID, uniqueSuspendersOnly} = useContext(
72+
SuspenseTreeStateContext,
73+
);
74+
75+
function handleToggleUniqueSuspenders(event: SyntheticEvent) {
76+
const nextUniqueSuspendersOnly = (event.currentTarget as HTMLInputElement)
77+
.checked;
78+
const nextTimeline =
79+
rootID === null
80+
? []
81+
: // TODO: Handle different timeline modes (e.g. random order)
82+
store.getSuspendableDocumentOrderSuspense(
83+
rootID,
84+
nextUniqueSuspendersOnly,
85+
);
86+
suspenseTreeDispatch({
87+
type: 'SET_SUSPENSE_TIMELINE',
88+
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
89+
});
90+
}
91+
92+
return (
93+
<Tooltip label="Only include boundaries with unique suspenders">
94+
<input
95+
checked={uniqueSuspendersOnly}
96+
type="checkbox"
97+
onChange={handleToggleUniqueSuspenders}
98+
/>
99+
</Tooltip>
100+
);
101+
}
102+
103+
function SelectRoot() {
104+
const store = useContext(StoreContext);
105+
const {roots, selectedRootID, uniqueSuspendersOnly} = useContext(
106+
SuspenseTreeStateContext,
107+
);
108+
const treeDispatch = useContext(TreeDispatcherContext);
109+
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
110+
111+
function handleChange(event: SyntheticEvent) {
112+
const newRootID = +event.currentTarget.value;
113+
// TODO: scrollIntoView both suspense rects and host instance.
114+
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
115+
newRootID,
116+
uniqueSuspendersOnly,
117+
);
118+
suspenseTreeDispatch({
119+
type: 'SET_SUSPENSE_TIMELINE',
120+
payload: [nextTimeline, newRootID, uniqueSuspendersOnly],
121+
});
122+
if (nextTimeline.length > 0) {
123+
const milestone = nextTimeline[nextTimeline.length - 1];
124+
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: milestone});
125+
}
126+
}
127+
return (
128+
roots.length > 0 && (
129+
<select
130+
aria-label="Select Suspense Root"
131+
className={styles.SuspenseTimelineRootSwitcher}
132+
onChange={handleChange}
133+
value={selectedRootID === null ? -1 : selectedRootID}>
134+
<option disabled={true} value={-1}>
135+
----
136+
</option>
137+
{roots.map(rootID => {
138+
// TODO: Use name
139+
const name = '#' + rootID;
140+
// TODO: Highlight host on hover
141+
return (
142+
<option key={rootID} value={rootID}>
143+
{name}
144+
</option>
145+
);
146+
})}
147+
</select>
148+
)
149+
);
150+
}
151+
51152
function ToggleTreeList({
52153
dispatch,
53154
state,
@@ -314,30 +415,30 @@ function SuspenseTab(_: {}) {
314415
</div>
315416
)}
316417
<div className={styles.TreeView}>
317-
<div className={styles.SuspenseTreeViewHeader}>
418+
<header className={styles.SuspenseTreeViewHeader}>
318419
{treeListDisabled ? (
319420
<div />
320421
) : (
321422
<ToggleTreeList dispatch={dispatch} state={state} />
322423
)}
323-
<div className={styles.SuspenseTreeViewHeaderMain}>
324-
<div className={styles.SuspenseTimeline}>
325-
<SuspenseTimeline />
326-
</div>
327-
<div className={styles.SuspenseBreadcrumbs}>
328-
<SuspenseBreadcrumbs />
329-
</div>
424+
<div className={styles.SuspenseBreadcrumbs}>
425+
<SuspenseBreadcrumbs />
330426
</div>
427+
<ToggleUniqueSuspenders />
428+
<SelectRoot />
331429
<ToggleInspectedElement
332430
dispatch={dispatch}
333431
state={state}
334432
orientation="horizontal"
335433
/>
336-
</div>
434+
</header>
337435
<div className={styles.Rects}>
338436
<SuspenseRects />
339437
</div>
340-
<footer>
438+
<footer className={styles.SuspenseTreeViewFooter}>
439+
<div className={styles.SuspenseTimeline}>
440+
<SuspenseTimeline />
441+
</div>
341442
<ToggleInspectedElement
342443
dispatch={dispatch}
343444
state={state}

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

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import * as React from 'react';
1111
import {useContext, useLayoutEffect, useRef} from 'react';
1212
import {BridgeContext, StoreContext} from '../context';
1313
import {TreeDispatcherContext} from '../Components/TreeContext';
14-
import Tooltip from '../Components/reach-ui/tooltip';
1514
import {useHighlightHostInstance} from '../hooks';
1615
import {
1716
SuspenseTreeDispatcherContext,
@@ -35,26 +34,8 @@ function SuspenseTimelineInput() {
3534
selectedRootID: rootID,
3635
timeline,
3736
timelineIndex,
38-
uniqueSuspendersOnly,
3937
} = useContext(SuspenseTreeStateContext);
4038

41-
function handleToggleUniqueSuspenders(event: SyntheticEvent) {
42-
const nextUniqueSuspendersOnly = (event.currentTarget as HTMLInputElement)
43-
.checked;
44-
const nextTimeline =
45-
rootID === null
46-
? []
47-
: // TODO: Handle different timeline modes (e.g. random order)
48-
store.getSuspendableDocumentOrderSuspense(
49-
rootID,
50-
nextUniqueSuspendersOnly,
51-
);
52-
suspenseTreeDispatch({
53-
type: 'SET_SUSPENSE_TIMELINE',
54-
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
55-
});
56-
}
57-
5839
const inputRef = useRef<HTMLElement | null>(null);
5940
const inputBBox = useRef<ClientRect | null>(null);
6041
useLayoutEffect(() => {
@@ -190,66 +171,15 @@ function SuspenseTimelineInput() {
190171
ref={inputRef}
191172
/>
192173
</div>
193-
<Tooltip label="Only include boundaries with unique suspenders">
194-
<input
195-
checked={uniqueSuspendersOnly}
196-
type="checkbox"
197-
onChange={handleToggleUniqueSuspenders}
198-
/>
199-
</Tooltip>
200174
</>
201175
);
202176
}
203177

204178
export default function SuspenseTimeline(): React$Node {
205-
const store = useContext(StoreContext);
206-
const {roots, selectedRootID, uniqueSuspendersOnly} = useContext(
207-
SuspenseTreeStateContext,
208-
);
209-
const treeDispatch = useContext(TreeDispatcherContext);
210-
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
211-
212-
function handleChange(event: SyntheticEvent) {
213-
const newRootID = +event.currentTarget.value;
214-
// TODO: scrollIntoView both suspense rects and host instance.
215-
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
216-
newRootID,
217-
uniqueSuspendersOnly,
218-
);
219-
suspenseTreeDispatch({
220-
type: 'SET_SUSPENSE_TIMELINE',
221-
payload: [nextTimeline, newRootID, uniqueSuspendersOnly],
222-
});
223-
if (nextTimeline.length > 0) {
224-
const milestone = nextTimeline[nextTimeline.length - 1];
225-
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: milestone});
226-
}
227-
}
228-
179+
const {selectedRootID} = useContext(SuspenseTreeStateContext);
229180
return (
230181
<div className={styles.SuspenseTimelineContainer}>
231182
<SuspenseTimelineInput key={selectedRootID} />
232-
{roots.length > 0 && (
233-
<select
234-
aria-label="Select Suspense Root"
235-
className={styles.SuspenseTimelineRootSwitcher}
236-
onChange={handleChange}
237-
value={selectedRootID === null ? -1 : selectedRootID}>
238-
<option disabled={true} value={-1}>
239-
----
240-
</option>
241-
{roots.map(rootID => {
242-
// TODO: Use name
243-
const name = '#' + rootID;
244-
// TODO: Highlight host on hover
245-
return (
246-
<option key={rootID} value={rootID}>
247-
{name}
248-
</option>
249-
);
250-
})}
251-
</select>
252-
)}
253183
</div>
254184
);
255185
}

0 commit comments

Comments
 (0)