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. 
    
  10. import * as React from 'react';
    
  11. import {
    
  12.   Fragment,
    
  13.   Suspense,
    
  14.   useCallback,
    
  15.   useContext,
    
  16.   useEffect,
    
  17.   useMemo,
    
  18.   useRef,
    
  19.   useState,
    
  20. } from 'react';
    
  21. import AutoSizer from 'react-virtualized-auto-sizer';
    
  22. import {FixedSizeList} from 'react-window';
    
  23. import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
    
  24. import Icon from '../Icon';
    
  25. import {SettingsContext} from '../Settings/SettingsContext';
    
  26. import {BridgeContext, StoreContext, OptionsContext} from '../context';
    
  27. import Element from './Element';
    
  28. import InspectHostNodesToggle from './InspectHostNodesToggle';
    
  29. import OwnersStack from './OwnersStack';
    
  30. import ComponentSearchInput from './ComponentSearchInput';
    
  31. import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
    
  32. import SelectedTreeHighlight from './SelectedTreeHighlight';
    
  33. import TreeFocusedContext from './TreeFocusedContext';
    
  34. import {useHighlightNativeElement, useSubscription} from '../hooks';
    
  35. import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI';
    
  36. import styles from './Tree.css';
    
  37. import ButtonIcon from '../ButtonIcon';
    
  38. import Button from '../Button';
    
  39. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  40. 
    
  41. // Never indent more than this number of pixels (even if we have the room).
    
  42. const DEFAULT_INDENTATION_SIZE = 12;
    
  43. 
    
  44. export type ItemData = {
    
  45.   numElements: number,
    
  46.   isNavigatingWithKeyboard: boolean,
    
  47.   lastScrolledIDRef: {current: number | null, ...},
    
  48.   onElementMouseEnter: (id: number) => void,
    
  49.   treeFocused: boolean,
    
  50. };
    
  51. 
    
  52. type Props = {};
    
  53. 
    
  54. export default function Tree(props: Props): React.Node {
    
  55.   const dispatch = useContext(TreeDispatcherContext);
    
  56.   const {
    
  57.     numElements,
    
  58.     ownerID,
    
  59.     searchIndex,
    
  60.     searchResults,
    
  61.     selectedElementID,
    
  62.     selectedElementIndex,
    
  63.   } = useContext(TreeStateContext);
    
  64.   const bridge = useContext(BridgeContext);
    
  65.   const store = useContext(StoreContext);
    
  66.   const {hideSettings} = useContext(OptionsContext);
    
  67.   const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] =
    
  68.     useState(false);
    
  69.   const {highlightNativeElement, clearHighlightNativeElement} =
    
  70.     useHighlightNativeElement();
    
  71.   const treeRef = useRef<HTMLDivElement | null>(null);
    
  72.   const focusTargetRef = useRef<HTMLDivElement | null>(null);
    
  73. 
    
  74.   const [treeFocused, setTreeFocused] = useState<boolean>(false);
    
  75. 
    
  76.   const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext);
    
  77. 
    
  78.   // Make sure a newly selected element is visible in the list.
    
  79.   // This is helpful for things like the owners list and search.
    
  80.   //
    
  81.   // TRICKY:
    
  82.   // It's important to use a callback ref for this, rather than a ref object and an effect.
    
  83.   // As an optimization, the AutoSizer component does not render children when their size would be 0.
    
  84.   // This means that in some cases (if the browser panel size is initially really small),
    
  85.   // the Tree component might render without rendering an inner List.
    
  86.   // In this case, the list ref would be null on mount (when the scroll effect runs),
    
  87.   // meaning the scroll action would be skipped (since ref updates don't re-run effects).
    
  88.   // Using a callback ref accounts for this case...
    
  89.   const listCallbackRef = useCallback(
    
  90.     (list: $FlowFixMe) => {
    
  91.       if (list != null && selectedElementIndex !== null) {
    
  92.         list.scrollToItem(selectedElementIndex, 'smart');
    
  93.       }
    
  94.     },
    
  95.     [selectedElementIndex],
    
  96.   );
    
  97. 
    
  98.   // Picking an element in the inspector should put focus into the tree.
    
  99.   // This ensures that keyboard navigation works right after picking a node.
    
  100.   useEffect(() => {
    
  101.     function handleStopInspectingNative(didSelectNode: boolean) {
    
  102.       if (didSelectNode && focusTargetRef.current !== null) {
    
  103.         focusTargetRef.current.focus();
    
  104.         logEvent({
    
  105.           event_name: 'select-element',
    
  106.           metadata: {source: 'inspector'},
    
  107.         });
    
  108.       }
    
  109.     }
    
  110.     bridge.addListener('stopInspectingNative', handleStopInspectingNative);
    
  111.     return () =>
    
  112.       bridge.removeListener('stopInspectingNative', handleStopInspectingNative);
    
  113.   }, [bridge]);
    
  114. 
    
  115.   // This ref is passed down the context to elements.
    
  116.   // It lets them avoid autoscrolling to the same item many times
    
  117.   // when a selected virtual row goes in and out of the viewport.
    
  118.   const lastScrolledIDRef = useRef<number | null>(null);
    
  119. 
    
  120.   // Navigate the tree with up/down arrow keys.
    
  121.   useEffect(() => {
    
  122.     if (treeRef.current === null) {
    
  123.       return () => {};
    
  124.     }
    
  125. 
    
  126.     const handleKeyDown = (event: KeyboardEvent) => {
    
  127.       if ((event: any).target.tagName === 'INPUT' || event.defaultPrevented) {
    
  128.         return;
    
  129.       }
    
  130. 
    
  131.       let element;
    
  132.       switch (event.key) {
    
  133.         case 'ArrowDown':
    
  134.           event.preventDefault();
    
  135.           if (event.altKey) {
    
  136.             dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'});
    
  137.           } else {
    
  138.             dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'});
    
  139.           }
    
  140.           break;
    
  141.         case 'ArrowLeft':
    
  142.           event.preventDefault();
    
  143.           element =
    
  144.             selectedElementID !== null
    
  145.               ? store.getElementByID(selectedElementID)
    
  146.               : null;
    
  147.           if (element !== null) {
    
  148.             if (event.altKey) {
    
  149.               if (element.ownerID !== null) {
    
  150.                 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'});
    
  151.               }
    
  152.             } else {
    
  153.               if (element.children.length > 0 && !element.isCollapsed) {
    
  154.                 store.toggleIsCollapsed(element.id, true);
    
  155.               } else {
    
  156.                 dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'});
    
  157.               }
    
  158.             }
    
  159.           }
    
  160.           break;
    
  161.         case 'ArrowRight':
    
  162.           event.preventDefault();
    
  163.           element =
    
  164.             selectedElementID !== null
    
  165.               ? store.getElementByID(selectedElementID)
    
  166.               : null;
    
  167.           if (element !== null) {
    
  168.             if (event.altKey) {
    
  169.               dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'});
    
  170.             } else {
    
  171.               if (element.children.length > 0 && element.isCollapsed) {
    
  172.                 store.toggleIsCollapsed(element.id, false);
    
  173.               } else {
    
  174.                 dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'});
    
  175.               }
    
  176.             }
    
  177.           }
    
  178.           break;
    
  179.         case 'ArrowUp':
    
  180.           event.preventDefault();
    
  181.           if (event.altKey) {
    
  182.             dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'});
    
  183.           } else {
    
  184.             dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'});
    
  185.           }
    
  186.           break;
    
  187.         default:
    
  188.           return;
    
  189.       }
    
  190.       setIsNavigatingWithKeyboard(true);
    
  191.     };
    
  192. 
    
  193.     // We used to listen to at the document level for this event.
    
  194.     // That allowed us to listen to up/down arrow key events while another section
    
  195.     // of DevTools (like the search input) was focused.
    
  196.     // This was a minor UX positive.
    
  197.     //
    
  198.     // (We had to use ownerDocument rather than document for this, because the
    
  199.     // DevTools extension renders the Components and Profiler tabs into portals.)
    
  200.     //
    
  201.     // This approach caused a problem though: it meant that a react-devtools-inline
    
  202.     // instance could steal (and prevent/block) keyboard events from other JavaScript
    
  203.     // on the page– which could even include other react-devtools-inline instances.
    
  204.     // This is a potential major UX negative.
    
  205.     //
    
  206.     // Given the above trade offs, we now listen on the root of the Tree itself.
    
  207.     const container = treeRef.current;
    
  208.     container.addEventListener('keydown', handleKeyDown);
    
  209. 
    
  210.     return () => {
    
  211.       container.removeEventListener('keydown', handleKeyDown);
    
  212.     };
    
  213.   }, [dispatch, selectedElementID, store]);
    
  214. 
    
  215.   // Focus management.
    
  216.   const handleBlur = useCallback(() => setTreeFocused(false), []);
    
  217.   const handleFocus = useCallback(() => {
    
  218.     setTreeFocused(true);
    
  219. 
    
  220.     if (selectedElementIndex === null && numElements > 0) {
    
  221.       dispatch({
    
  222.         type: 'SELECT_ELEMENT_AT_INDEX',
    
  223.         payload: 0,
    
  224.       });
    
  225.     }
    
  226.   }, [dispatch, numElements, selectedElementIndex]);
    
  227. 
    
  228.   const handleKeyPress = useCallback(
    
  229.     (event: $FlowFixMe) => {
    
  230.       switch (event.key) {
    
  231.         case 'Enter':
    
  232.         case ' ':
    
  233.           if (selectedElementID !== null) {
    
  234.             dispatch({type: 'SELECT_OWNER', payload: selectedElementID});
    
  235.           }
    
  236.           break;
    
  237.         default:
    
  238.           break;
    
  239.       }
    
  240.     },
    
  241.     [dispatch, selectedElementID],
    
  242.   );
    
  243. 
    
  244.   // If we switch the selected element while using the keyboard,
    
  245.   // start highlighting it in the DOM instead of the last hovered node.
    
  246.   const searchRef = useRef({searchIndex, searchResults});
    
  247.   useEffect(() => {
    
  248.     let didSelectNewSearchResult = false;
    
  249.     if (
    
  250.       searchRef.current.searchIndex !== searchIndex ||
    
  251.       searchRef.current.searchResults !== searchResults
    
  252.     ) {
    
  253.       searchRef.current.searchIndex = searchIndex;
    
  254.       searchRef.current.searchResults = searchResults;
    
  255.       didSelectNewSearchResult = true;
    
  256.     }
    
  257.     if (isNavigatingWithKeyboard || didSelectNewSearchResult) {
    
  258.       if (selectedElementID !== null) {
    
  259.         highlightNativeElement(selectedElementID);
    
  260.       } else {
    
  261.         clearHighlightNativeElement();
    
  262.       }
    
  263.     }
    
  264.   }, [
    
  265.     bridge,
    
  266.     isNavigatingWithKeyboard,
    
  267.     highlightNativeElement,
    
  268.     searchIndex,
    
  269.     searchResults,
    
  270.     selectedElementID,
    
  271.   ]);
    
  272. 
    
  273.   // Highlight last hovered element.
    
  274.   const handleElementMouseEnter = useCallback(
    
  275.     (id: $FlowFixMe) => {
    
  276.       // Ignore hover while we're navigating with keyboard.
    
  277.       // This avoids flicker from the hovered nodes under the mouse.
    
  278.       if (!isNavigatingWithKeyboard) {
    
  279.         highlightNativeElement(id);
    
  280.       }
    
  281.     },
    
  282.     [isNavigatingWithKeyboard, highlightNativeElement],
    
  283.   );
    
  284. 
    
  285.   const handleMouseMove = useCallback(() => {
    
  286.     // We started using the mouse again.
    
  287.     // This will enable hover styles in individual rows.
    
  288.     setIsNavigatingWithKeyboard(false);
    
  289.   }, []);
    
  290. 
    
  291.   const handleMouseLeave = clearHighlightNativeElement;
    
  292. 
    
  293.   // Let react-window know to re-render any time the underlying tree data changes.
    
  294.   // This includes the owner context, since it controls a filtered view of the tree.
    
  295.   const itemData = useMemo<ItemData>(
    
  296.     () => ({
    
  297.       numElements,
    
  298.       isNavigatingWithKeyboard,
    
  299.       onElementMouseEnter: handleElementMouseEnter,
    
  300.       lastScrolledIDRef,
    
  301.       treeFocused,
    
  302.     }),
    
  303.     [
    
  304.       numElements,
    
  305.       isNavigatingWithKeyboard,
    
  306.       handleElementMouseEnter,
    
  307.       lastScrolledIDRef,
    
  308.       treeFocused,
    
  309.     ],
    
  310.   );
    
  311. 
    
  312.   const itemKey = useCallback(
    
  313.     (index: number) => store.getElementIDAtIndex(index),
    
  314.     [store],
    
  315.   );
    
  316. 
    
  317.   const handlePreviousErrorOrWarningClick = React.useCallback(() => {
    
  318.     dispatch({type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
    
  319.   }, []);
    
  320. 
    
  321.   const handleNextErrorOrWarningClick = React.useCallback(() => {
    
  322.     dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
    
  323.   }, []);
    
  324. 
    
  325.   const errorsOrWarningsSubscription = useMemo(
    
  326.     () => ({
    
  327.       getCurrentValue: () => ({
    
  328.         errors: store.errorCount,
    
  329.         warnings: store.warningCount,
    
  330.       }),
    
  331.       subscribe: (callback: Function) => {
    
  332.         store.addListener('mutated', callback);
    
  333.         return () => store.removeListener('mutated', callback);
    
  334.       },
    
  335.     }),
    
  336.     [store],
    
  337.   );
    
  338.   const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
    
  339. 
    
  340.   const clearErrorsAndWarnings = () => {
    
  341.     clearErrorsAndWarningsAPI({bridge, store});
    
  342.   };
    
  343. 
    
  344.   const zeroElementsNotice = (
    
  345.     <div className={styles.ZeroElementsNotice}>
    
  346.       <p>Loading React Element Tree...</p>
    
  347.       <p>
    
  348.         If this seems stuck, please follow the{' '}
    
  349.         <a
    
  350.           className={styles.Link}
    
  351.           href="https://github.com/facebook/react/blob/main/packages/react-devtools/README.md#the-react-tab-shows-no-components"
    
  352.           target="_blank">
    
  353.           troubleshooting instructions
    
  354.         </a>
    
  355.         .
    
  356.       </p>
    
  357.     </div>
    
  358.   );
    
  359. 
    
  360.   return (
    
  361.     <TreeFocusedContext.Provider value={treeFocused}>
    
  362.       <div className={styles.Tree} ref={treeRef}>
    
  363.         <div className={styles.SearchInput}>
    
  364.           {store.supportsNativeInspection && (
    
  365.             <Fragment>
    
  366.               <InspectHostNodesToggle />
    
  367.               <div className={styles.VRule} />
    
  368.             </Fragment>
    
  369.           )}
    
  370.           <Suspense fallback={<Loading />}>
    
  371.             {ownerID !== null ? <OwnersStack /> : <ComponentSearchInput />}
    
  372.           </Suspense>
    
  373.           {showInlineWarningsAndErrors &&
    
  374.             ownerID === null &&
    
  375.             (errors > 0 || warnings > 0) && (
    
  376.               <React.Fragment>
    
  377.                 <div className={styles.VRule} />
    
  378.                 {errors > 0 && (
    
  379.                   <div className={styles.IconAndCount}>
    
  380.                     <Icon className={styles.ErrorIcon} type="error" />
    
  381.                     {errors}
    
  382.                   </div>
    
  383.                 )}
    
  384.                 {warnings > 0 && (
    
  385.                   <div className={styles.IconAndCount}>
    
  386.                     <Icon className={styles.WarningIcon} type="warning" />
    
  387.                     {warnings}
    
  388.                   </div>
    
  389.                 )}
    
  390.                 <Button
    
  391.                   onClick={handlePreviousErrorOrWarningClick}
    
  392.                   title="Scroll to previous error or warning">
    
  393.                   <ButtonIcon type="up" />
    
  394.                 </Button>
    
  395.                 <Button
    
  396.                   onClick={handleNextErrorOrWarningClick}
    
  397.                   title="Scroll to next error or warning">
    
  398.                   <ButtonIcon type="down" />
    
  399.                 </Button>
    
  400.                 <Button
    
  401.                   onClick={clearErrorsAndWarnings}
    
  402.                   title="Clear all errors and warnings">
    
  403.                   <ButtonIcon type="clear" />
    
  404.                 </Button>
    
  405.               </React.Fragment>
    
  406.             )}
    
  407.           {!hideSettings && (
    
  408.             <Fragment>
    
  409.               <div className={styles.VRule} />
    
  410.               <SettingsModalContextToggle />
    
  411.             </Fragment>
    
  412.           )}
    
  413.         </div>
    
  414.         {numElements === 0 ? (
    
  415.           zeroElementsNotice
    
  416.         ) : (
    
  417.           <div
    
  418.             className={styles.AutoSizerWrapper}
    
  419.             onBlur={handleBlur}
    
  420.             onFocus={handleFocus}
    
  421.             onKeyPress={handleKeyPress}
    
  422.             onMouseMove={handleMouseMove}
    
  423.             onMouseLeave={handleMouseLeave}
    
  424.             ref={focusTargetRef}
    
  425.             tabIndex={0}>
    
  426.             <AutoSizer>
    
  427.               {({height, width}) => (
    
  428.                 <FixedSizeList
    
  429.                   className={styles.List}
    
  430.                   height={height}
    
  431.                   innerElementType={InnerElementType}
    
  432.                   itemCount={numElements}
    
  433.                   itemData={itemData}
    
  434.                   itemKey={itemKey}
    
  435.                   itemSize={lineHeight}
    
  436.                   ref={listCallbackRef}
    
  437.                   width={width}>
    
  438.                   {Element}
    
  439.                 </FixedSizeList>
    
  440.               )}
    
  441.             </AutoSizer>
    
  442.           </div>
    
  443.         )}
    
  444.       </div>
    
  445.     </TreeFocusedContext.Provider>
    
  446.   );
    
  447. }
    
  448. 
    
  449. // Indentation size can be adjusted but child width is fixed.
    
  450. // We need to adjust indentations so the widest child can fit without overflowing.
    
  451. // Sometimes the widest child is also the deepest in the tree:
    
  452. //   ┏----------------------┓
    
  453. //   ┆ <Foo>                ┆
    
  454. //   ┆ ••••<Foobar>         ┆
    
  455. //   ┆ ••••••••<Baz>        ┆
    
  456. //   ┗----------------------┛
    
  457. //
    
  458. // But this is not always the case.
    
  459. // Even with the above example, a change in indentation may change the overall widest child:
    
  460. //   ┏----------------------┓
    
  461. //   ┆ <Foo>                ┆
    
  462. //   ┆ ••<Foobar>           ┆
    
  463. //   ┆ ••••<Baz>            ┆
    
  464. //   ┗----------------------┛
    
  465. //
    
  466. // In extreme cases this difference can be important:
    
  467. //   ┏----------------------┓
    
  468. //   ┆ <ReallyLongName>     ┆
    
  469. //   ┆ ••<Foo>              ┆
    
  470. //   ┆ ••••<Bar>            ┆
    
  471. //   ┆ ••••••<Baz>          ┆
    
  472. //   ┆ ••••••••<Qux>        ┆
    
  473. //   ┗----------------------┛
    
  474. //
    
  475. // In the above example, the current indentation is fine,
    
  476. // but if we naively assumed that the widest element is also the deepest element,
    
  477. // we would end up compressing the indentation unnecessarily:
    
  478. //   ┏----------------------┓
    
  479. //   ┆ <ReallyLongName>     ┆
    
  480. //   ┆ •<Foo>               ┆
    
  481. //   ┆ ••<Bar>              ┆
    
  482. //   ┆ •••<Baz>             ┆
    
  483. //   ┆ ••••<Qux>            ┆
    
  484. //   ┗----------------------┛
    
  485. //
    
  486. // The way we deal with this is to compute the max indentation size that can fit each child,
    
  487. // given the child's fixed width and depth within the tree.
    
  488. // Then we take the smallest of these indentation sizes...
    
  489. function updateIndentationSizeVar(
    
  490.   innerDiv: HTMLDivElement,
    
  491.   cachedChildWidths: WeakMap<HTMLElement, number>,
    
  492.   indentationSizeRef: {current: number},
    
  493.   prevListWidthRef: {current: number},
    
  494. ): void {
    
  495.   const list = ((innerDiv.parentElement: any): HTMLDivElement);
    
  496.   const listWidth = list.clientWidth;
    
  497. 
    
  498.   // Skip measurements when the Components panel is hidden.
    
  499.   if (listWidth === 0) {
    
  500.     return;
    
  501.   }
    
  502. 
    
  503.   // Reset the max indentation size if the width of the tree has increased.
    
  504.   if (listWidth > prevListWidthRef.current) {
    
  505.     indentationSizeRef.current = DEFAULT_INDENTATION_SIZE;
    
  506.   }
    
  507.   prevListWidthRef.current = listWidth;
    
  508. 
    
  509.   let maxIndentationSize: number = indentationSizeRef.current;
    
  510. 
    
  511.   // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    
  512.   for (const child of innerDiv.children) {
    
  513.     const depth = parseInt(child.getAttribute('data-depth'), 10) || 0;
    
  514. 
    
  515.     let childWidth: number = 0;
    
  516. 
    
  517.     const cachedChildWidth = cachedChildWidths.get(child);
    
  518.     if (cachedChildWidth != null) {
    
  519.       childWidth = cachedChildWidth;
    
  520.     } else {
    
  521.       const {firstElementChild} = child;
    
  522. 
    
  523.       // Skip over e.g. the guideline element
    
  524.       if (firstElementChild != null) {
    
  525.         childWidth = firstElementChild.clientWidth;
    
  526.         cachedChildWidths.set(child, childWidth);
    
  527.       }
    
  528.     }
    
  529. 
    
  530.     const remainingWidth = Math.max(0, listWidth - childWidth);
    
  531. 
    
  532.     maxIndentationSize = Math.min(maxIndentationSize, remainingWidth / depth);
    
  533.   }
    
  534. 
    
  535.   indentationSizeRef.current = maxIndentationSize;
    
  536. 
    
  537.   list.style.setProperty('--indentation-size', `${maxIndentationSize}px`);
    
  538. }
    
  539. 
    
  540. // $FlowFixMe[missing-local-annot]
    
  541. function InnerElementType({children, style, ...rest}) {
    
  542.   const {ownerID} = useContext(TreeStateContext);
    
  543. 
    
  544.   const cachedChildWidths = useMemo<WeakMap<HTMLElement, number>>(
    
  545.     () => new WeakMap(),
    
  546.     [],
    
  547.   );
    
  548. 
    
  549.   // This ref tracks the current indentation size.
    
  550.   // We decrease indentation to fit wider/deeper trees.
    
  551.   // We intentionally do not increase it again afterward, to avoid the perception of content "jumping"
    
  552.   // e.g. clicking to toggle/collapse a row might otherwise jump horizontally beneath your cursor,
    
  553.   // e.g. scrolling a wide row off screen could cause narrower rows to jump to the right some.
    
  554.   //
    
  555.   // There are two exceptions for this:
    
  556.   // 1. The first is when the width of the tree increases.
    
  557.   // The user may have resized the window specifically to make more room for DevTools.
    
  558.   // In either case, this should reset our max indentation size logic.
    
  559.   // 2. The second is when the user enters or exits an owner tree.
    
  560.   const indentationSizeRef = useRef<number>(DEFAULT_INDENTATION_SIZE);
    
  561.   const prevListWidthRef = useRef<number>(0);
    
  562.   const prevOwnerIDRef = useRef<number | null>(ownerID);
    
  563.   const divRef = useRef<HTMLDivElement | null>(null);
    
  564. 
    
  565.   // We shouldn't retain this width across different conceptual trees though,
    
  566.   // so when the user opens the "owners tree" view, we should discard the previous width.
    
  567.   if (ownerID !== prevOwnerIDRef.current) {
    
  568.     prevOwnerIDRef.current = ownerID;
    
  569.     indentationSizeRef.current = DEFAULT_INDENTATION_SIZE;
    
  570.   }
    
  571. 
    
  572.   // When we render new content, measure to see if we need to shrink indentation to fit it.
    
  573.   useEffect(() => {
    
  574.     if (divRef.current !== null) {
    
  575.       updateIndentationSizeVar(
    
  576.         divRef.current,
    
  577.         cachedChildWidths,
    
  578.         indentationSizeRef,
    
  579.         prevListWidthRef,
    
  580.       );
    
  581.     }
    
  582.   });
    
  583. 
    
  584.   // This style override enables the background color to fill the full visible width,
    
  585.   // when combined with the CSS tweaks in Element.
    
  586.   // A lot of options were considered; this seemed the one that requires the least code.
    
  587.   // See https://github.com/bvaughn/react-devtools-experimental/issues/9
    
  588.   return (
    
  589.     <div
    
  590.       className={styles.InnerElementType}
    
  591.       ref={divRef}
    
  592.       style={style}
    
  593.       {...rest}>
    
  594.       <SelectedTreeHighlight />
    
  595.       {children}
    
  596.     </div>
    
  597.   );
    
  598. }
    
  599. 
    
  600. function Loading() {
    
  601.   return <div className={styles.Loading}>Loading...</div>;
    
  602. }