From d069652c608fe075221b541a5de78482ceae467b Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 5 Feb 2026 17:28:30 +0100 Subject: [PATCH 1/4] explicit ternary --- .../src/devtools/views/Components/OwnersStack.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index 09bdb96af01c..2fa9278ab6f4 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -160,7 +160,7 @@ export default function OwnerStack(): React.Node { return (
- {isOverflowing && ( + {isOverflowing ? ( )} - )} - {!isOverflowing && + ) : ( owners.map((owner, index) => ( - ))} + )) + )}
From b52380c1e87a655c4f302137abd36d640c9bfd61 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 5 Feb 2026 17:45:54 +0100 Subject: [PATCH 3/4] Decouple flatlist measurement from children hierarchy --- .../devtools/views/Components/OwnersStack.css | 4 ++ .../devtools/views/Components/OwnersStack.js | 62 ++++++++++--------- .../views/SuspenseTab/SuspenseBreadcrumbs.js | 54 +++++++--------- 3 files changed, 58 insertions(+), 62 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css index 293faa98376a..b9cf3e1d978d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css @@ -32,6 +32,10 @@ overflow-x: auto; } +.OwnerStackFlatListContainer { + display: inline-flex; +} + .VRule { flex: 0 0 auto; height: 20px; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index bbb36d9c9f9c..f7e426ab4dbd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -81,21 +81,44 @@ type OwnerStackFlatListProps = { owners: Array, selectedIndex: number, selectOwner: SelectOwner, + setElementsTotalWidth: (width: number) => void, }; function OwnerStackFlatList({ owners, selectedIndex, selectOwner, + setElementsTotalWidth, }: OwnerStackFlatListProps): React.Node { - return owners.map((owner, index) => ( - - )); + const containerRef = useRef(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 ( +
+ {owners.map((owner, index) => ( + + ))} +
+ ); } export default function OwnerStack(): React.Node { @@ -156,28 +179,6 @@ 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 (
@@ -206,6 +207,7 @@ export default function OwnerStack(): React.Node { owners={owners} selectedIndex={selectedIndex} selectOwner={selectOwner} + setElementsTotalWidth={setElementsTotalWidth} /> )}
diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js index 53a10ed0dc22..652f175fd0e8 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js @@ -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(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 ( -
    +
      {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" . @@ -271,37 +291,6 @@ export default function SuspenseBreadcrumbs(): React$Node { const containerRef = useRef(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 (
      {isOverflowing ? ( @@ -315,6 +304,7 @@ export default function SuspenseBreadcrumbs(): React$Node { onItemClick={handleClick} onItemPointerEnter={highlightHostInstance} onItemPointerLeave={clearHighlightHostInstance} + setElementsTotalWidth={setElementsTotalWidth} /> )}
      From dec687f58e47728c825504210761bdccc0976d4f Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 5 Feb 2026 17:57:19 +0100 Subject: [PATCH 4/4] =?UTF-8?q?[DevTools]=20Separate=20breadcrumbs=20with?= =?UTF-8?q?=20`=C2=BB`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devtools/views/Components/OwnersStack.css | 6 +++- .../devtools/views/Components/OwnersStack.js | 16 +++++---- .../views/SuspenseTab/SuspenseBreadcrumbs.css | 4 +++ .../views/SuspenseTab/SuspenseBreadcrumbs.js | 34 +++++++++++-------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css index b9cf3e1d978d..ca22f6774f09 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css @@ -34,7 +34,11 @@ .OwnerStackFlatListContainer { display: inline-flex; -} +} + +.OwnerStackFlatListSeparator { + user-select: none; +} .VRule { flex: 0 0 auto; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index f7e426ab4dbd..596113bd8631 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -110,12 +110,16 @@ function OwnerStackFlatList({ return (
      {owners.map((owner, index) => ( - + + + {index < owners.length - 1 && ( + » + )} + ))}
      ); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.css index 3bd6a2519161..940eeedad464 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.css @@ -16,6 +16,10 @@ display: inline; } +.SuspenseBreadcrumbsListItemSeparator { + user-select: none; +} + .SuspenseBreadcrumbsListItem[aria-current="true"] .SuspenseBreadcrumbsButton { color: var(--color-button-active); } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js index 652f175fd0e8..c542858b3016 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js @@ -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'; @@ -99,19 +99,25 @@ function SuspenseBreadcrumbsFlatList({ const node = store.getSuspenseByID(id); return ( -
    1. - -
    2. + +
    3. + +
    4. + {index < lineage.length - 1 && ( + + » + + )} +
      ); }) )}