[DevTools] Add breadcrumbs to Suspense tab (#34312)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-08-28 16:03:54 +02:00 committed by GitHub
parent 8d7b5e4903
commit 89a803fcec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 9 deletions

View File

@ -0,0 +1,33 @@
.SuspenseBreadcrumbsList {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.SuspenseBreadcrumbsListItem {
display: inline;
}
.SuspenseBreadcrumbsListItem[aria-current="true"] .SuspenseBreadcrumbsButton {
color: var(--color-button-active);
}
.SuspenseBreadcrumbsButton {
background: var(--color-button-background);
border: none;
border-radius: 0.25rem;
padding: 0.25rem;
white-space: nowrap;
}
.SuspenseBreadcrumbsButton:hover {
background-color: var(--color-button-background-hover);
color: var(--color-button-hover);
}
.SuspenseBreadcrumbsButton:focus-visible {
background: var(--color-button-background-focus);
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
import * as React from 'react';
import {useContext} from 'react';
import {
TreeDispatcherContext,
TreeStateContext,
} from '../Components/TreeContext';
import {StoreContext} from '../context';
import {useHighlightHostInstance} from '../hooks';
import styles from './SuspenseBreadcrumbs.css';
import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
export default function SuspenseBreadcrumbs(): React$Node {
const store = useContext(StoreContext);
const dispatch = useContext(TreeDispatcherContext);
const {inspectedElementID} = useContext(TreeStateContext);
const {highlightHostInstance, clearHighlightHostInstance} =
useHighlightHostInstance();
// TODO: Use the nearest Suspense boundary
const inspectedSuspenseID = inspectedElementID;
if (inspectedSuspenseID === null) {
return null;
}
const suspense = store.getSuspenseByID(inspectedSuspenseID);
if (suspense === null) {
return null;
}
const lineage: SuspenseNode[] = [];
let next: null | SuspenseNode = suspense;
while (next !== null) {
if (next.parentID === 0) {
next = null;
} else {
lineage.unshift(next);
next = store.getSuspenseByID(next.parentID);
}
}
function handleClick(node: SuspenseNode, event: SyntheticMouseEvent) {
event.preventDefault();
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: node.id});
}
return (
<ol className={styles.SuspenseBreadcrumbsList}>
{lineage.map((node, index) => {
return (
<li
key={node.id}
className={styles.SuspenseBreadcrumbsListItem}
aria-current={index === lineage.length - 1}
onPointerEnter={highlightHostInstance.bind(null, node.id)}
onPointerLeave={clearHighlightHostInstance}>
<button
className={styles.SuspenseBreadcrumbsButton}
onClick={handleClick.bind(null, node)}
type="button">
{node.name}
</button>
</li>
);
})}
</ol>
);
}

View File

@ -108,14 +108,22 @@
overflow: auto;
}
.TimelineWrapper {
.SuspenseTreeViewHeader {
padding: 0.25rem;
display: flex;
flex-direction: row;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: flex-start;
}
.Timeline {
flex-grow: 1;
align-self: anchor-center;
.SuspenseTreeViewHeaderMain {
display: grid;
grid-template-rows: auto auto;
}
.SuspenseBreadcrumbs {
/**
* TODO: Switch to single item view on overflow like OwnerStack does.
* OwnerStack has more constraints that make it easier so it won't be a 1:1 port.
*/
overflow-x: auto;
}

View File

@ -19,6 +19,7 @@ import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBo
import InspectedElement from '../Components/InspectedElement';
import portaledContent from '../portaledContent';
import styles from './SuspenseTab.css';
import SuspenseBreadcrumbs from './SuspenseBreadcrumbs';
import SuspenseRects from './SuspenseRects';
import SuspenseTimeline from './SuspenseTimeline';
import SuspenseTreeList from './SuspenseTreeList';
@ -304,10 +305,15 @@ function SuspenseTab(_: {}) {
/>
</div>
<div className={styles.TreeView}>
<div className={styles.TimelineWrapper}>
<div className={styles.SuspenseTreeViewHeader}>
<ToggleTreeList dispatch={dispatch} state={state} />
<div className={styles.Timeline}>
<SuspenseTimeline />
<div className={styles.SuspenseTreeViewHeaderMain}>
<div className={styles.SuspenseTimeline}>
<SuspenseTimeline />
</div>
<div className={styles.SuspenseBreadcrumbs}>
<SuspenseBreadcrumbs />
</div>
</div>
<ToggleInspectedElement
dispatch={dispatch}

View File

@ -2,6 +2,7 @@
width: 100%;
display: flex;
flex-direction: row;
padding: 0 0.25rem;
}
.SuspenseTimelineInput {