1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. import * as React from 'react';
    
  10. import {
    
  11.   Fragment,
    
  12.   useCallback,
    
  13.   useContext,
    
  14.   useLayoutEffect,
    
  15.   useReducer,
    
  16.   useRef,
    
  17.   useState,
    
  18. } from 'react';
    
  19. import Button from '../Button';
    
  20. import ButtonIcon from '../ButtonIcon';
    
  21. import Toggle from '../Toggle';
    
  22. import Badge from './Badge';
    
  23. import {OwnersListContext} from './OwnersListContext';
    
  24. import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
    
  25. import {useIsOverflowing} from '../hooks';
    
  26. import {StoreContext} from '../context';
    
  27. import Tooltip from '../Components/reach-ui/tooltip';
    
  28. import {
    
  29.   Menu,
    
  30.   MenuList,
    
  31.   MenuButton,
    
  32.   MenuItem,
    
  33. } from '../Components/reach-ui/menu-button';
    
  34. 
    
  35. import type {SerializedElement} from 'react-devtools-shared/src/frontend/types';
    
  36. 
    
  37. import styles from './OwnersStack.css';
    
  38. 
    
  39. type SelectOwner = (owner: SerializedElement | null) => void;
    
  40. 
    
  41. type ACTION_UPDATE_OWNER_ID = {
    
  42.   type: 'UPDATE_OWNER_ID',
    
  43.   ownerID: number | null,
    
  44.   owners: Array<SerializedElement>,
    
  45. };
    
  46. type ACTION_UPDATE_SELECTED_INDEX = {
    
  47.   type: 'UPDATE_SELECTED_INDEX',
    
  48.   selectedIndex: number,
    
  49. };
    
  50. 
    
  51. type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
    
  52. 
    
  53. type State = {
    
  54.   ownerID: number | null,
    
  55.   owners: Array<SerializedElement>,
    
  56.   selectedIndex: number,
    
  57. };
    
  58. 
    
  59. function dialogReducer(state: State, action: Action) {
    
  60.   switch (action.type) {
    
  61.     case 'UPDATE_OWNER_ID':
    
  62.       const selectedIndex = action.owners.findIndex(
    
  63.         owner => owner.id === action.ownerID,
    
  64.       );
    
  65.       return {
    
  66.         ownerID: action.ownerID,
    
  67.         owners: action.owners,
    
  68.         selectedIndex,
    
  69.       };
    
  70.     case 'UPDATE_SELECTED_INDEX':
    
  71.       return {
    
  72.         ...state,
    
  73.         selectedIndex: action.selectedIndex,
    
  74.       };
    
  75.     default:
    
  76.       throw new Error(`Invalid action "${action.type}"`);
    
  77.   }
    
  78. }
    
  79. 
    
  80. export default function OwnerStack(): React.Node {
    
  81.   const read = useContext(OwnersListContext);
    
  82.   const {ownerID} = useContext(TreeStateContext);
    
  83.   const treeDispatch = useContext(TreeDispatcherContext);
    
  84. 
    
  85.   const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
    
  86.     ownerID: null,
    
  87.     owners: [],
    
  88.     selectedIndex: 0,
    
  89.   });
    
  90. 
    
  91.   // When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners.
    
  92.   // We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed.
    
  93.   if (ownerID === null) {
    
  94.     dispatch({
    
  95.       type: 'UPDATE_OWNER_ID',
    
  96.       ownerID: null,
    
  97.       owners: [],
    
  98.     });
    
  99.   } else if (ownerID !== state.ownerID) {
    
  100.     const isInStore =
    
  101.       state.owners.findIndex(owner => owner.id === ownerID) >= 0;
    
  102.     dispatch({
    
  103.       type: 'UPDATE_OWNER_ID',
    
  104.       ownerID,
    
  105.       owners: isInStore ? state.owners : read(ownerID) || [],
    
  106.     });
    
  107.   }
    
  108. 
    
  109.   const {owners, selectedIndex} = state;
    
  110. 
    
  111.   const selectOwner = useCallback<SelectOwner>(
    
  112.     (owner: SerializedElement | null) => {
    
  113.       if (owner !== null) {
    
  114.         const index = owners.indexOf(owner);
    
  115.         dispatch({
    
  116.           type: 'UPDATE_SELECTED_INDEX',
    
  117.           selectedIndex: index >= 0 ? index : 0,
    
  118.         });
    
  119.         treeDispatch({type: 'SELECT_OWNER', payload: owner.id});
    
  120.       } else {
    
  121.         dispatch({
    
  122.           type: 'UPDATE_SELECTED_INDEX',
    
  123.           selectedIndex: 0,
    
  124.         });
    
  125.         treeDispatch({type: 'RESET_OWNER_STACK'});
    
  126.       }
    
  127.     },
    
  128.     [owners, treeDispatch],
    
  129.   );
    
  130. 
    
  131.   const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
    
  132.   const elementsBarRef = useRef<HTMLDivElement | null>(null);
    
  133.   const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
    
  134. 
    
  135.   const selectedOwner = owners[selectedIndex];
    
  136. 
    
  137.   useLayoutEffect(() => {
    
  138.     // If we're already overflowing, then we don't need to re-measure items.
    
  139.     // That's because once the owners stack is open, it can only get larger (by drilling in).
    
  140.     // A totally new stack can only be reached by exiting this mode and re-entering it.
    
  141.     if (elementsBarRef.current === null || isOverflowing) {
    
  142.       return () => {};
    
  143.     }
    
  144. 
    
  145.     let totalWidth = 0;
    
  146.     for (let i = 0; i < owners.length; i++) {
    
  147.       const element = elementsBarRef.current.children[i];
    
  148.       const computedStyle = getComputedStyle(element);
    
  149. 
    
  150.       totalWidth +=
    
  151.         element.offsetWidth +
    
  152.         parseInt(computedStyle.marginLeft, 10) +
    
  153.         parseInt(computedStyle.marginRight, 10);
    
  154.     }
    
  155. 
    
  156.     setElementsTotalWidth(totalWidth);
    
  157.   }, [elementsBarRef, isOverflowing, owners.length]);
    
  158. 
    
  159.   return (
    
  160.     <div className={styles.OwnerStack}>
    
  161.       <div className={styles.Bar} ref={elementsBarRef}>
    
  162.         {isOverflowing && (
    
  163.           <Fragment>
    
  164.             <ElementsDropdown
    
  165.               owners={owners}
    
  166.               selectedIndex={selectedIndex}
    
  167.               selectOwner={selectOwner}
    
  168.             />
    
  169.             <BackToOwnerButton
    
  170.               owners={owners}
    
  171.               selectedIndex={selectedIndex}
    
  172.               selectOwner={selectOwner}
    
  173.             />
    
  174.             {selectedOwner != null && (
    
  175.               <ElementView
    
  176.                 owner={selectedOwner}
    
  177.                 isSelected={true}
    
  178.                 selectOwner={selectOwner}
    
  179.               />
    
  180.             )}
    
  181.           </Fragment>
    
  182.         )}
    
  183.         {!isOverflowing &&
    
  184.           owners.map((owner, index) => (
    
  185.             <ElementView
    
  186.               key={index}
    
  187.               owner={owner}
    
  188.               isSelected={index === selectedIndex}
    
  189.               selectOwner={selectOwner}
    
  190.             />
    
  191.           ))}
    
  192.       </div>
    
  193.       <div className={styles.VRule} />
    
  194.       <Button onClick={() => selectOwner(null)} title="Back to tree view">
    
  195.         <ButtonIcon type="close" />
    
  196.       </Button>
    
  197.     </div>
    
  198.   );
    
  199. }
    
  200. 
    
  201. type ElementsDropdownProps = {
    
  202.   owners: Array<SerializedElement>,
    
  203.   selectedIndex: number,
    
  204.   selectOwner: SelectOwner,
    
  205.   ...
    
  206. };
    
  207. function ElementsDropdown({
    
  208.   owners,
    
  209.   selectedIndex,
    
  210.   selectOwner,
    
  211. }: ElementsDropdownProps) {
    
  212.   const store = useContext(StoreContext);
    
  213. 
    
  214.   const menuItems = [];
    
  215.   for (let index = owners.length - 1; index >= 0; index--) {
    
  216.     const owner = owners[index];
    
  217.     const isInStore = store.containsElement(owner.id);
    
  218.     menuItems.push(
    
  219.       <MenuItem
    
  220.         key={owner.id}
    
  221.         className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
    
  222.         onSelect={() => (isInStore ? selectOwner(owner) : null)}>
    
  223.         {owner.displayName}
    
  224. 
    
  225.         <Badge
    
  226.           className={styles.Badge}
    
  227.           hocDisplayNames={owner.hocDisplayNames}
    
  228.           type={owner.type}
    
  229.         />
    
  230.       </MenuItem>,
    
  231.     );
    
  232.   }
    
  233. 
    
  234.   return (
    
  235.     <Menu>
    
  236.       <MenuButton className={styles.MenuButton}>
    
  237.         <Tooltip label="Open elements dropdown">
    
  238.           <span className={styles.MenuButtonContent} tabIndex={-1}>
    
  239.             <ButtonIcon type="more" />
    
  240.           </span>
    
  241.         </Tooltip>
    
  242.       </MenuButton>
    
  243.       <MenuList className={styles.Modal}>{menuItems}</MenuList>
    
  244.     </Menu>
    
  245.   );
    
  246. }
    
  247. 
    
  248. type ElementViewProps = {
    
  249.   isSelected: boolean,
    
  250.   owner: SerializedElement,
    
  251.   selectOwner: SelectOwner,
    
  252.   ...
    
  253. };
    
  254. function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
    
  255.   const store = useContext(StoreContext);
    
  256. 
    
  257.   const {displayName, hocDisplayNames, type} = owner;
    
  258.   const isInStore = store.containsElement(owner.id);
    
  259. 
    
  260.   const handleChange = useCallback(() => {
    
  261.     if (isInStore) {
    
  262.       selectOwner(owner);
    
  263.     }
    
  264.   }, [isInStore, selectOwner, owner]);
    
  265. 
    
  266.   return (
    
  267.     <Toggle
    
  268.       className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
    
  269.       isChecked={isSelected}
    
  270.       onChange={handleChange}>
    
  271.       {displayName}
    
  272. 
    
  273.       <Badge
    
  274.         className={styles.Badge}
    
  275.         hocDisplayNames={hocDisplayNames}
    
  276.         type={type}
    
  277.       />
    
  278.     </Toggle>
    
  279.   );
    
  280. }
    
  281. 
    
  282. type BackToOwnerButtonProps = {
    
  283.   owners: Array<SerializedElement>,
    
  284.   selectedIndex: number,
    
  285.   selectOwner: SelectOwner,
    
  286. };
    
  287. function BackToOwnerButton({
    
  288.   owners,
    
  289.   selectedIndex,
    
  290.   selectOwner,
    
  291. }: BackToOwnerButtonProps) {
    
  292.   const store = useContext(StoreContext);
    
  293. 
    
  294.   if (selectedIndex <= 0) {
    
  295.     return null;
    
  296.   }
    
  297. 
    
  298.   const owner = owners[selectedIndex - 1];
    
  299.   const isInStore = store.containsElement(owner.id);
    
  300. 
    
  301.   return (
    
  302.     <Button
    
  303.       className={isInStore ? undefined : styles.NotInStore}
    
  304.       onClick={() => (isInStore ? selectOwner(owner) : null)}
    
  305.       title={`Up to ${owner.displayName || 'owner'}`}>
    
  306.       <ButtonIcon type="previous" />
    
  307.     </Button>
    
  308.   );
    
  309. }