[DevTools] Separate breadcrumbs with » (#35705)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-02-06 00:40:31 +01:00
committed by GitHub
parent 8b276df415
commit 1c66ac740c
4 changed files with 110 additions and 78 deletions

View File

@@ -32,6 +32,14 @@
overflow-x: auto;
}
.OwnerStackFlatListContainer {
display: inline-flex;
}
.OwnerStackFlatListSeparator {
user-select: none;
}
.VRule {
flex: 0 0 auto;
height: 20px;

View File

@@ -77,6 +77,54 @@ function dialogReducer(state: State, action: Action) {
}
}
type OwnerStackFlatListProps = {
owners: Array<SerializedElement>,
selectedIndex: number,
selectOwner: SelectOwner,
setElementsTotalWidth: (width: number) => void,
};
function OwnerStackFlatList({
owners,
selectedIndex,
selectOwner,
setElementsTotalWidth,
}: OwnerStackFlatListProps): React.Node {
const containerRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const container = containerRef.current;
if (container === null) {
return;
}
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
setElementsTotalWidth(entry.contentRect.width);
});
observer.observe(container);
return observer.disconnect.bind(observer);
}, []);
return (
<div className={styles.OwnerStackFlatListContainer} ref={containerRef}>
{owners.map((owner, index) => (
<Fragment key={index}>
<ElementView
owner={owner}
isSelected={index === selectedIndex}
selectOwner={selectOwner}
/>
{index < owners.length - 1 && (
<span className={styles.OwnerStackFlatListSeparator}>»</span>
)}
</Fragment>
))}
</div>
);
}
export default function OwnerStack(): React.Node {
const read = useContext(OwnersListContext);
const {ownerID} = useContext(TreeStateContext);
@@ -135,32 +183,10 @@ export default function OwnerStack(): React.Node {
const selectedOwner = owners[selectedIndex];
useLayoutEffect(() => {
// If we're already overflowing, then we don't need to re-measure items.
// That's because once the owners stack is open, it can only get larger (by drilling in).
// A totally new stack can only be reached by exiting this mode and re-entering it.
if (elementsBarRef.current === null || isOverflowing) {
return () => {};
}
let totalWidth = 0;
for (let i = 0; i < owners.length; i++) {
const element = elementsBarRef.current.children[i];
const computedStyle = getComputedStyle(element);
totalWidth +=
element.offsetWidth +
parseInt(computedStyle.marginLeft, 10) +
parseInt(computedStyle.marginRight, 10);
}
setElementsTotalWidth(totalWidth);
}, [elementsBarRef, isOverflowing, owners.length]);
return (
<div className={styles.OwnerStack}>
<div className={styles.Bar} ref={elementsBarRef}>
{isOverflowing && (
{isOverflowing ? (
<Fragment>
<ElementsDropdown
owners={owners}
@@ -180,16 +206,14 @@ export default function OwnerStack(): React.Node {
/>
)}
</Fragment>
)}
{!isOverflowing &&
owners.map((owner, index) => (
<ElementView
key={index}
owner={owner}
isSelected={index === selectedIndex}
) : (
<OwnerStackFlatList
owners={owners}
selectedIndex={selectedIndex}
selectOwner={selectOwner}
setElementsTotalWidth={setElementsTotalWidth}
/>
))}
)}
</div>
<div className={styles.VRule} />
<Button onClick={() => selectOwner(null)} title="Back to tree view">

View File

@@ -16,6 +16,10 @@
display: inline;
}
.SuspenseBreadcrumbsListItemSeparator {
user-select: none;
}
.SuspenseBreadcrumbsListItem[aria-current="true"] .SuspenseBreadcrumbsButton {
color: var(--color-button-active);
}

View File

@@ -11,7 +11,7 @@ import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
import * as React from 'react';
import {useContext, useLayoutEffect, useRef, useState} from 'react';
import {Fragment, useContext, useLayoutEffect, useRef, useState} from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Tooltip from '../Components/reach-ui/tooltip';
@@ -40,20 +40,40 @@ type SuspenseBreadcrumbsFlatListProps = {
scrollIntoView?: boolean,
) => void,
onItemPointerLeave: (event: SyntheticMouseEvent) => void,
setElementsTotalWidth: (width: number) => void,
};
function SuspenseBreadcrumbsFlatList({
onItemClick,
onItemPointerEnter,
onItemPointerLeave,
setElementsTotalWidth,
}: SuspenseBreadcrumbsFlatListProps): React$Node {
const store = useContext(StoreContext);
const {activityID} = useContext(TreeStateContext);
const {selectedSuspenseID, lineage, roots} = useContext(
SuspenseTreeStateContext,
);
const containerRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const container = containerRef.current;
if (container === null) {
return;
}
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
setElementsTotalWidth(entry.contentRect.width);
});
observer.observe(container);
return observer.disconnect.bind(observer);
}, []);
return (
<ol className={styles.SuspenseBreadcrumbsList}>
<ol className={styles.SuspenseBreadcrumbsList} ref={containerRef}>
{lineage === null ? null : lineage.length === 0 ? (
// We selected the root. This means that we're currently viewing the Transition
// that rendered the whole screen. In laymans terms this is really "Initial Paint" .
@@ -79,8 +99,8 @@ function SuspenseBreadcrumbsFlatList({
const node = store.getSuspenseByID(id);
return (
<Fragment key={id}>
<li
key={id}
className={styles.SuspenseBreadcrumbsListItem}
aria-current={selectedSuspenseID === id}
onPointerEnter={onItemPointerEnter.bind(null, id, false)}
@@ -92,6 +112,12 @@ function SuspenseBreadcrumbsFlatList({
{node === null ? 'Unknown' : node.name || 'Unknown'}
</button>
</li>
{index < lineage.length - 1 && (
<span className={styles.SuspenseBreadcrumbsListItemSeparator}>
»
</span>
)}
</Fragment>
);
})
)}
@@ -271,37 +297,6 @@ export default function SuspenseBreadcrumbs(): React$Node {
const containerRef = useRef<HTMLDivElement | null>(null);
const isOverflowing = useIsOverflowing(containerRef, elementsTotalWidth);
useLayoutEffect(() => {
const container = containerRef.current;
if (
container === null ||
// We want to measure the size of the flat list only when it's being used.
isOverflowing
) {
return;
}
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
const observer = new ResizeObserver(() => {
let totalWidth = 0;
for (let i = 0; i < container.children.length; i++) {
const element = container.children[i];
const computedStyle = getComputedStyle(element);
totalWidth +=
element.offsetWidth +
parseInt(computedStyle.marginLeft, 10) +
parseInt(computedStyle.marginRight, 10);
}
setElementsTotalWidth(totalWidth);
});
observer.observe(container);
return observer.disconnect.bind(observer);
}, [containerRef, isOverflowing]);
return (
<div className={styles.SuspenseBreadcrumbsContainer} ref={containerRef}>
{isOverflowing ? (
@@ -315,6 +310,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
onItemClick={handleClick}
onItemPointerEnter={highlightHostInstance}
onItemPointerLeave={clearHighlightHostInstance}
setElementsTotalWidth={setElementsTotalWidth}
/>
)}
</div>