/** * 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 typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; import * as React 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'; import { Menu, MenuList, MenuButton, MenuItem, } from '../Components/reach-ui/menu-button'; import { TreeDispatcherContext, TreeStateContext, } from '../Components/TreeContext'; import {StoreContext} from '../context'; import {useHighlightHostInstance, useIsOverflowing} from '../hooks'; import styles from './SuspenseBreadcrumbs.css'; import { SuspenseTreeStateContext, SuspenseTreeDispatcherContext, } from './SuspenseTreeContext'; type SuspenseBreadcrumbsFlatListProps = { onItemClick: (id: SuspenseNode['id'], event: SyntheticMouseEvent) => void, onItemPointerEnter: ( id: SuspenseNode['id'], 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" . // 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 ? (
  1. ) : null ) : ( lineage.map((id, index) => { const node = store.getSuspenseByID(id); return (
  2. {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 ? ( ) : ( )}
); }