Skip to content

Commit 1c66ac7

Browse files
authored
[DevTools] Separate breadcrumbs with » (facebook#35705)
1 parent 8b276df commit 1c66ac7

4 files changed

Lines changed: 110 additions & 78 deletions

File tree

packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
overflow-x: auto;
3333
}
3434

35+
.OwnerStackFlatListContainer {
36+
display: inline-flex;
37+
}
38+
39+
.OwnerStackFlatListSeparator {
40+
user-select: none;
41+
}
42+
3543
.VRule {
3644
flex: 0 0 auto;
3745
height: 20px;

packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,54 @@ function dialogReducer(state: State, action: Action) {
7777
}
7878
}
7979

80+
type OwnerStackFlatListProps = {
81+
owners: Array<SerializedElement>,
82+
selectedIndex: number,
83+
selectOwner: SelectOwner,
84+
setElementsTotalWidth: (width: number) => void,
85+
};
86+
87+
function OwnerStackFlatList({
88+
owners,
89+
selectedIndex,
90+
selectOwner,
91+
setElementsTotalWidth,
92+
}: OwnerStackFlatListProps): React.Node {
93+
const containerRef = useRef<HTMLDivElement | null>(null);
94+
useLayoutEffect(() => {
95+
const container = containerRef.current;
96+
if (container === null) {
97+
return;
98+
}
99+
100+
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
101+
const observer = new ResizeObserver(entries => {
102+
const entry = entries[0];
103+
setElementsTotalWidth(entry.contentRect.width);
104+
});
105+
106+
observer.observe(container);
107+
return observer.disconnect.bind(observer);
108+
}, []);
109+
110+
return (
111+
<div className={styles.OwnerStackFlatListContainer} ref={containerRef}>
112+
{owners.map((owner, index) => (
113+
<Fragment key={index}>
114+
<ElementView
115+
owner={owner}
116+
isSelected={index === selectedIndex}
117+
selectOwner={selectOwner}
118+
/>
119+
{index < owners.length - 1 && (
120+
<span className={styles.OwnerStackFlatListSeparator}>»</span>
121+
)}
122+
</Fragment>
123+
))}
124+
</div>
125+
);
126+
}
127+
80128
export default function OwnerStack(): React.Node {
81129
const read = useContext(OwnersListContext);
82130
const {ownerID} = useContext(TreeStateContext);
@@ -135,32 +183,10 @@ export default function OwnerStack(): React.Node {
135183

136184
const selectedOwner = owners[selectedIndex];
137185

138-
useLayoutEffect(() => {
139-
// If we're already overflowing, then we don't need to re-measure items.
140-
// That's because once the owners stack is open, it can only get larger (by drilling in).
141-
// A totally new stack can only be reached by exiting this mode and re-entering it.
142-
if (elementsBarRef.current === null || isOverflowing) {
143-
return () => {};
144-
}
145-
146-
let totalWidth = 0;
147-
for (let i = 0; i < owners.length; i++) {
148-
const element = elementsBarRef.current.children[i];
149-
const computedStyle = getComputedStyle(element);
150-
151-
totalWidth +=
152-
element.offsetWidth +
153-
parseInt(computedStyle.marginLeft, 10) +
154-
parseInt(computedStyle.marginRight, 10);
155-
}
156-
157-
setElementsTotalWidth(totalWidth);
158-
}, [elementsBarRef, isOverflowing, owners.length]);
159-
160186
return (
161187
<div className={styles.OwnerStack}>
162188
<div className={styles.Bar} ref={elementsBarRef}>
163-
{isOverflowing && (
189+
{isOverflowing ? (
164190
<Fragment>
165191
<ElementsDropdown
166192
owners={owners}
@@ -180,16 +206,14 @@ export default function OwnerStack(): React.Node {
180206
/>
181207
)}
182208
</Fragment>
209+
) : (
210+
<OwnerStackFlatList
211+
owners={owners}
212+
selectedIndex={selectedIndex}
213+
selectOwner={selectOwner}
214+
setElementsTotalWidth={setElementsTotalWidth}
215+
/>
183216
)}
184-
{!isOverflowing &&
185-
owners.map((owner, index) => (
186-
<ElementView
187-
key={index}
188-
owner={owner}
189-
isSelected={index === selectedIndex}
190-
selectOwner={selectOwner}
191-
/>
192-
))}
193217
</div>
194218
<div className={styles.VRule} />
195219
<Button onClick={() => selectOwner(null)} title="Back to tree view">

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
display: inline;
1717
}
1818

19+
.SuspenseBreadcrumbsListItemSeparator {
20+
user-select: none;
21+
}
22+
1923
.SuspenseBreadcrumbsListItem[aria-current="true"] .SuspenseBreadcrumbsButton {
2024
color: var(--color-button-active);
2125
}

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

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
1111
import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
1212

1313
import * as React from 'react';
14-
import {useContext, useLayoutEffect, useRef, useState} from 'react';
14+
import {Fragment, useContext, useLayoutEffect, useRef, useState} from 'react';
1515
import Button from '../Button';
1616
import ButtonIcon from '../ButtonIcon';
1717
import Tooltip from '../Components/reach-ui/tooltip';
@@ -40,20 +40,40 @@ type SuspenseBreadcrumbsFlatListProps = {
4040
scrollIntoView?: boolean,
4141
) => void,
4242
onItemPointerLeave: (event: SyntheticMouseEvent) => void,
43+
setElementsTotalWidth: (width: number) => void,
4344
};
4445

4546
function SuspenseBreadcrumbsFlatList({
4647
onItemClick,
4748
onItemPointerEnter,
4849
onItemPointerLeave,
50+
setElementsTotalWidth,
4951
}: SuspenseBreadcrumbsFlatListProps): React$Node {
5052
const store = useContext(StoreContext);
5153
const {activityID} = useContext(TreeStateContext);
5254
const {selectedSuspenseID, lineage, roots} = useContext(
5355
SuspenseTreeStateContext,
5456
);
57+
58+
const containerRef = useRef<HTMLDivElement | null>(null);
59+
useLayoutEffect(() => {
60+
const container = containerRef.current;
61+
if (container === null) {
62+
return;
63+
}
64+
65+
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
66+
const observer = new ResizeObserver(entries => {
67+
const entry = entries[0];
68+
setElementsTotalWidth(entry.contentRect.width);
69+
});
70+
71+
observer.observe(container);
72+
return observer.disconnect.bind(observer);
73+
}, []);
74+
5575
return (
56-
<ol className={styles.SuspenseBreadcrumbsList}>
76+
<ol className={styles.SuspenseBreadcrumbsList} ref={containerRef}>
5777
{lineage === null ? null : lineage.length === 0 ? (
5878
// We selected the root. This means that we're currently viewing the Transition
5979
// that rendered the whole screen. In laymans terms this is really "Initial Paint" .
@@ -79,19 +99,25 @@ function SuspenseBreadcrumbsFlatList({
7999
const node = store.getSuspenseByID(id);
80100

81101
return (
82-
<li
83-
key={id}
84-
className={styles.SuspenseBreadcrumbsListItem}
85-
aria-current={selectedSuspenseID === id}
86-
onPointerEnter={onItemPointerEnter.bind(null, id, false)}
87-
onPointerLeave={onItemPointerLeave}>
88-
<button
89-
className={styles.SuspenseBreadcrumbsButton}
90-
onClick={onItemClick.bind(null, id)}
91-
type="button">
92-
{node === null ? 'Unknown' : node.name || 'Unknown'}
93-
</button>
94-
</li>
102+
<Fragment key={id}>
103+
<li
104+
className={styles.SuspenseBreadcrumbsListItem}
105+
aria-current={selectedSuspenseID === id}
106+
onPointerEnter={onItemPointerEnter.bind(null, id, false)}
107+
onPointerLeave={onItemPointerLeave}>
108+
<button
109+
className={styles.SuspenseBreadcrumbsButton}
110+
onClick={onItemClick.bind(null, id)}
111+
type="button">
112+
{node === null ? 'Unknown' : node.name || 'Unknown'}
113+
</button>
114+
</li>
115+
{index < lineage.length - 1 && (
116+
<span className={styles.SuspenseBreadcrumbsListItemSeparator}>
117+
»
118+
</span>
119+
)}
120+
</Fragment>
95121
);
96122
})
97123
)}
@@ -271,37 +297,6 @@ export default function SuspenseBreadcrumbs(): React$Node {
271297
const containerRef = useRef<HTMLDivElement | null>(null);
272298
const isOverflowing = useIsOverflowing(containerRef, elementsTotalWidth);
273299

274-
useLayoutEffect(() => {
275-
const container = containerRef.current;
276-
277-
if (
278-
container === null ||
279-
// We want to measure the size of the flat list only when it's being used.
280-
isOverflowing
281-
) {
282-
return;
283-
}
284-
285-
const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
286-
const observer = new ResizeObserver(() => {
287-
let totalWidth = 0;
288-
for (let i = 0; i < container.children.length; i++) {
289-
const element = container.children[i];
290-
const computedStyle = getComputedStyle(element);
291-
292-
totalWidth +=
293-
element.offsetWidth +
294-
parseInt(computedStyle.marginLeft, 10) +
295-
parseInt(computedStyle.marginRight, 10);
296-
}
297-
setElementsTotalWidth(totalWidth);
298-
});
299-
300-
observer.observe(container);
301-
302-
return observer.disconnect.bind(observer);
303-
}, [containerRef, isOverflowing]);
304-
305300
return (
306301
<div className={styles.SuspenseBreadcrumbsContainer} ref={containerRef}>
307302
{isOverflowing ? (
@@ -315,6 +310,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
315310
onItemClick={handleClick}
316311
onItemPointerEnter={highlightHostInstance}
317312
onItemPointerLeave={clearHighlightHostInstance}
313+
setElementsTotalWidth={setElementsTotalWidth}
318314
/>
319315
)}
320316
</div>

0 commit comments

Comments
 (0)