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..ca22f6774f09 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,14 @@ overflow-x: auto; } +.OwnerStackFlatListContainer { + display: inline-flex; +} + +.OwnerStackFlatListSeparator { + user-select: none; +} + .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 09bdb96af01c..596113bd8631 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -77,6 +77,54 @@ function dialogReducer(state: State, action: Action) { } } +type OwnerStackFlatListProps = { + owners: Array, + selectedIndex: number, + selectOwner: SelectOwner, + setElementsTotalWidth: (width: number) => void, +}; + +function OwnerStackFlatList({ + owners, + selectedIndex, + selectOwner, + setElementsTotalWidth, +}: OwnerStackFlatListProps): React.Node { + 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) => ( + + + {index < owners.length - 1 && ( + » + )} + + ))} +
+ ); +} + 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 (
- {isOverflowing && ( + {isOverflowing ? ( )} + ) : ( + )} - {!isOverflowing && - owners.map((owner, index) => ( - - ))}
- + +
  • + +
  • + {index < lineage.length - 1 && ( + + » + + )} +
    ); }) )} ); } + +type SuspenseBreadcrumbsMenuProps = { + onItemClick: (id: SuspenseNode['id'], event: SyntheticMouseEvent) => void, + onItemPointerEnter: ( + id: SuspenseNode['id'], + scrollIntoView?: boolean, + ) => void, + onItemPointerLeave: (event: SyntheticMouseEvent) => void, +}; + +function SuspenseBreadcrumbsMenu({ + onItemClick, + onItemPointerEnter, + onItemPointerLeave, +}: SuspenseBreadcrumbsMenuProps): React$Node { + const store = useContext(StoreContext); + const {activityID} = useContext(TreeStateContext); + const {selectedSuspenseID, lineage, roots} = useContext( + SuspenseTreeStateContext, + ); + const selectedSuspenseNode = + selectedSuspenseID !== null + ? store.getSuspenseByID(selectedSuspenseID) + : null; + + 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" . + // When we're looking at a subtree selection, then the equivalent is a + // "Transition" since in that case it's really about a Transition within the page. + roots.length > 0 ? ( + + ) : null + ) : ( + <> + + + {selectedSuspenseNode != null && ( + + )} + + )} + + ); +} + +type SuspenseBreadcrumbsDropdownProps = { + lineage: $ReadOnlyArray, + selectedIndex: number, + selectElement: (id: SuspenseNode['id']) => void, +}; +function SuspenseBreadcrumbsDropdown({ + lineage, + selectElement, +}: SuspenseBreadcrumbsDropdownProps) { + const store = useContext(StoreContext); + + const menuItems = []; + for (let index = lineage.length - 1; index >= 0; index--) { + const suspenseNodeID = lineage[index]; + const node = store.getSuspenseByID(suspenseNodeID); + menuItems.push( + + {node === null ? 'Unknown' : node.name || 'Unknown'} + , + ); + } + + return ( + + + + + + + + + + {menuItems} + + + ); +} + +type SuspenseBreadcrumbsToParentButtonProps = { + lineage: $ReadOnlyArray, + selectedSuspenseID: SuspenseNode['id'] | null, + selectElement: (id: SuspenseNode['id'], event: SyntheticMouseEvent) => void, +}; +function SuspenseBreadcrumbsToParentButton({ + lineage, + selectedSuspenseID, + selectElement, +}: SuspenseBreadcrumbsToParentButtonProps) { + const store = useContext(StoreContext); + const selectedIndex = + selectedSuspenseID === null + ? lineage.length - 1 + : lineage.indexOf(selectedSuspenseID); + + if (selectedIndex <= 0) { + return null; + } + + const parentID = lineage[selectedIndex - 1]; + const parent = store.getSuspenseByID(parentID); + + return ( + + ); +} + +export default function SuspenseBreadcrumbs(): React$Node { + const treeDispatch = useContext(TreeDispatcherContext); + const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); + + const {highlightHostInstance, clearHighlightHostInstance} = + useHighlightHostInstance(); + + function handleClick(id: SuspenseNode['id'], event?: SyntheticMouseEvent) { + if (event !== undefined) { + // E.g. 3rd party component libraries might omit the event and already prevent default + // like Reach's MenuItem does. + event.preventDefault(); + } + treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id}); + suspenseTreeDispatch({type: 'SELECT_SUSPENSE_BY_ID', payload: id}); + } + + const [elementsTotalWidth, setElementsTotalWidth] = useState(0); + const containerRef = useRef(null); + const isOverflowing = useIsOverflowing(containerRef, elementsTotalWidth); + + return ( +
    + {isOverflowing ? ( + + ) : ( + + )} +
    + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css index a7915d0d9101..92935bd4a6b4 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css @@ -121,15 +121,6 @@ border-bottom: 1px solid var(--color-border); } -.SuspenseBreadcrumbs { - flex: 1; - /** - * 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; -} - .SuspenseTreeViewFooter { flex: 0 0 42px; display: flex; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js index 0b195a99c4db..3fdd9fe935a3 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js @@ -501,9 +501,7 @@ function SuspenseTab(_: {}) {
    )} -
    - -
    +
    {!hideSettings && }