Skip to content

Commit 89a803f

Browse files
authored
[DevTools] Add breadcrumbs to Suspense tab (facebook#34312)
1 parent 8d7b5e4 commit 89a803f

File tree

5 files changed

+136
-9
lines changed

5 files changed

+136
-9
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.SuspenseBreadcrumbsList {
2+
margin: 0;
3+
padding: 0;
4+
list-style: none;
5+
display: flex;
6+
flex-direction: row;
7+
flex-wrap: nowrap;
8+
}
9+
10+
.SuspenseBreadcrumbsListItem {
11+
display: inline;
12+
}
13+
14+
.SuspenseBreadcrumbsListItem[aria-current="true"] .SuspenseBreadcrumbsButton {
15+
color: var(--color-button-active);
16+
}
17+
18+
.SuspenseBreadcrumbsButton {
19+
background: var(--color-button-background);
20+
border: none;
21+
border-radius: 0.25rem;
22+
padding: 0.25rem;
23+
white-space: nowrap;
24+
}
25+
26+
.SuspenseBreadcrumbsButton:hover {
27+
background-color: var(--color-button-background-hover);
28+
color: var(--color-button-hover);
29+
}
30+
31+
.SuspenseBreadcrumbsButton:focus-visible {
32+
background: var(--color-button-background-focus);
33+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
11+
12+
import * as React from 'react';
13+
import {useContext} from 'react';
14+
import {
15+
TreeDispatcherContext,
16+
TreeStateContext,
17+
} from '../Components/TreeContext';
18+
import {StoreContext} from '../context';
19+
import {useHighlightHostInstance} from '../hooks';
20+
import styles from './SuspenseBreadcrumbs.css';
21+
import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
22+
23+
export default function SuspenseBreadcrumbs(): React$Node {
24+
const store = useContext(StoreContext);
25+
const dispatch = useContext(TreeDispatcherContext);
26+
const {inspectedElementID} = useContext(TreeStateContext);
27+
28+
const {highlightHostInstance, clearHighlightHostInstance} =
29+
useHighlightHostInstance();
30+
31+
// TODO: Use the nearest Suspense boundary
32+
const inspectedSuspenseID = inspectedElementID;
33+
if (inspectedSuspenseID === null) {
34+
return null;
35+
}
36+
37+
const suspense = store.getSuspenseByID(inspectedSuspenseID);
38+
if (suspense === null) {
39+
return null;
40+
}
41+
42+
const lineage: SuspenseNode[] = [];
43+
let next: null | SuspenseNode = suspense;
44+
while (next !== null) {
45+
if (next.parentID === 0) {
46+
next = null;
47+
} else {
48+
lineage.unshift(next);
49+
next = store.getSuspenseByID(next.parentID);
50+
}
51+
}
52+
53+
function handleClick(node: SuspenseNode, event: SyntheticMouseEvent) {
54+
event.preventDefault();
55+
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: node.id});
56+
}
57+
58+
return (
59+
<ol className={styles.SuspenseBreadcrumbsList}>
60+
{lineage.map((node, index) => {
61+
return (
62+
<li
63+
key={node.id}
64+
className={styles.SuspenseBreadcrumbsListItem}
65+
aria-current={index === lineage.length - 1}
66+
onPointerEnter={highlightHostInstance.bind(null, node.id)}
67+
onPointerLeave={clearHighlightHostInstance}>
68+
<button
69+
className={styles.SuspenseBreadcrumbsButton}
70+
onClick={handleClick.bind(null, node)}
71+
type="button">
72+
{node.name}
73+
</button>
74+
</li>
75+
);
76+
})}
77+
</ol>
78+
);
79+
}

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,22 @@
108108
overflow: auto;
109109
}
110110

111-
.TimelineWrapper {
111+
.SuspenseTreeViewHeader {
112112
padding: 0.25rem;
113-
display: flex;
114-
flex-direction: row;
113+
display: grid;
114+
grid-template-columns: auto 1fr auto;
115115
align-items: flex-start;
116116
}
117117

118-
.Timeline {
119-
flex-grow: 1;
120-
align-self: anchor-center;
118+
.SuspenseTreeViewHeaderMain {
119+
display: grid;
120+
grid-template-rows: auto auto;
121+
}
122+
123+
.SuspenseBreadcrumbs {
124+
/**
125+
* TODO: Switch to single item view on overflow like OwnerStack does.
126+
* OwnerStack has more constraints that make it easier so it won't be a 1:1 port.
127+
*/
128+
overflow-x: auto;
121129
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBo
1919
import InspectedElement from '../Components/InspectedElement';
2020
import portaledContent from '../portaledContent';
2121
import styles from './SuspenseTab.css';
22+
import SuspenseBreadcrumbs from './SuspenseBreadcrumbs';
2223
import SuspenseRects from './SuspenseRects';
2324
import SuspenseTimeline from './SuspenseTimeline';
2425
import SuspenseTreeList from './SuspenseTreeList';
@@ -304,10 +305,15 @@ function SuspenseTab(_: {}) {
304305
/>
305306
</div>
306307
<div className={styles.TreeView}>
307-
<div className={styles.TimelineWrapper}>
308+
<div className={styles.SuspenseTreeViewHeader}>
308309
<ToggleTreeList dispatch={dispatch} state={state} />
309-
<div className={styles.Timeline}>
310-
<SuspenseTimeline />
310+
<div className={styles.SuspenseTreeViewHeaderMain}>
311+
<div className={styles.SuspenseTimeline}>
312+
<SuspenseTimeline />
313+
</div>
314+
<div className={styles.SuspenseBreadcrumbs}>
315+
<SuspenseBreadcrumbs />
316+
</div>
311317
</div>
312318
<ToggleInspectedElement
313319
dispatch={dispatch}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
width: 100%;
33
display: flex;
44
flex-direction: row;
5+
padding: 0 0.25rem;
56
}
67

78
.SuspenseTimelineInput {

0 commit comments

Comments
 (0)