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 {Fragment, useContext, useMemo, useState} from 'react';
    
  12. import Store from 'react-devtools-shared/src/devtools/store';
    
  13. import Badge from './Badge';
    
  14. import ButtonIcon from '../ButtonIcon';
    
  15. import {createRegExp} from '../utils';
    
  16. import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
    
  17. import {SettingsContext} from '../Settings/SettingsContext';
    
  18. import {StoreContext} from '../context';
    
  19. import {useSubscription} from '../hooks';
    
  20. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  21. 
    
  22. import type {ItemData} from './Tree';
    
  23. import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
    
  24. 
    
  25. import styles from './Element.css';
    
  26. import Icon from '../Icon';
    
  27. 
    
  28. type Props = {
    
  29.   data: ItemData,
    
  30.   index: number,
    
  31.   style: Object,
    
  32.   ...
    
  33. };
    
  34. 
    
  35. export default function Element({data, index, style}: Props): React.Node {
    
  36.   const store = useContext(StoreContext);
    
  37.   const {ownerFlatTree, ownerID, selectedElementID} =
    
  38.     useContext(TreeStateContext);
    
  39.   const dispatch = useContext(TreeDispatcherContext);
    
  40.   const {showInlineWarningsAndErrors} = React.useContext(SettingsContext);
    
  41. 
    
  42.   const element =
    
  43.     ownerFlatTree !== null
    
  44.       ? ownerFlatTree[index]
    
  45.       : store.getElementAtIndex(index);
    
  46. 
    
  47.   const [isHovered, setIsHovered] = useState(false);
    
  48. 
    
  49.   const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
    
  50.   const id = element === null ? null : element.id;
    
  51.   const isSelected = selectedElementID === id;
    
  52. 
    
  53.   const errorsAndWarningsSubscription = useMemo(
    
  54.     () => ({
    
  55.       getCurrentValue: () =>
    
  56.         element === null
    
  57.           ? {errorCount: 0, warningCount: 0}
    
  58.           : store.getErrorAndWarningCountForElementID(element.id),
    
  59.       subscribe: (callback: Function) => {
    
  60.         store.addListener('mutated', callback);
    
  61.         return () => store.removeListener('mutated', callback);
    
  62.       },
    
  63.     }),
    
  64.     [store, element],
    
  65.   );
    
  66.   const {errorCount, warningCount} = useSubscription<{
    
  67.     errorCount: number,
    
  68.     warningCount: number,
    
  69.   }>(errorsAndWarningsSubscription);
    
  70. 
    
  71.   const handleDoubleClick = () => {
    
  72.     if (id !== null) {
    
  73.       dispatch({type: 'SELECT_OWNER', payload: id});
    
  74.     }
    
  75.   };
    
  76. 
    
  77.   // $FlowFixMe[missing-local-annot]
    
  78.   const handleClick = ({metaKey}) => {
    
  79.     if (id !== null) {
    
  80.       logEvent({
    
  81.         event_name: 'select-element',
    
  82.         metadata: {source: 'click-element'},
    
  83.       });
    
  84.       dispatch({
    
  85.         type: 'SELECT_ELEMENT_BY_ID',
    
  86.         payload: metaKey ? null : id,
    
  87.       });
    
  88.     }
    
  89.   };
    
  90. 
    
  91.   const handleMouseEnter = () => {
    
  92.     setIsHovered(true);
    
  93.     if (id !== null) {
    
  94.       onElementMouseEnter(id);
    
  95.     }
    
  96.   };
    
  97. 
    
  98.   const handleMouseLeave = () => {
    
  99.     setIsHovered(false);
    
  100.   };
    
  101. 
    
  102.   // $FlowFixMe[missing-local-annot]
    
  103.   const handleKeyDoubleClick = event => {
    
  104.     // Double clicks on key value are used for text selection (if the text has been truncated).
    
  105.     // They should not enter the owners tree view.
    
  106.     event.stopPropagation();
    
  107.     event.preventDefault();
    
  108.   };
    
  109. 
    
  110.   // Handle elements that are removed from the tree while an async render is in progress.
    
  111.   if (element == null) {
    
  112.     console.warn(`<Element> Could not find element at index ${index}`);
    
  113. 
    
  114.     // This return needs to happen after hooks, since hooks can't be conditional.
    
  115.     return null;
    
  116.   }
    
  117. 
    
  118.   const {
    
  119.     depth,
    
  120.     displayName,
    
  121.     hocDisplayNames,
    
  122.     isStrictModeNonCompliant,
    
  123.     key,
    
  124.     type,
    
  125.   } = ((element: any): ElementType);
    
  126. 
    
  127.   // Only show strict mode non-compliance badges for top level elements.
    
  128.   // Showing an inline badge for every element in the tree would be noisy.
    
  129.   const showStrictModeBadge = isStrictModeNonCompliant && depth === 0;
    
  130. 
    
  131.   let className = styles.Element;
    
  132.   if (isSelected) {
    
  133.     className = treeFocused
    
  134.       ? styles.SelectedElement
    
  135.       : styles.InactiveSelectedElement;
    
  136.   } else if (isHovered && !isNavigatingWithKeyboard) {
    
  137.     className = styles.HoveredElement;
    
  138.   }
    
  139. 
    
  140.   return (
    
  141.     <div
    
  142.       className={className}
    
  143.       onMouseEnter={handleMouseEnter}
    
  144.       onMouseLeave={handleMouseLeave}
    
  145.       onClick={handleClick}
    
  146.       onDoubleClick={handleDoubleClick}
    
  147.       style={style}
    
  148.       data-testname="ComponentTreeListItem"
    
  149.       data-depth={depth}>
    
  150.       {/* This wrapper is used by Tree for measurement purposes. */}
    
  151.       <div
    
  152.         className={styles.Wrapper}
    
  153.         style={{
    
  154.           // Left offset presents the appearance of a nested tree structure.
    
  155.           // We must use padding rather than margin/left because of the selected background color.
    
  156.           transform: `translateX(calc(${depth} * var(--indentation-size)))`,
    
  157.         }}>
    
  158.         {ownerID === null ? (
    
  159.           <ExpandCollapseToggle element={element} store={store} />
    
  160.         ) : null}
    
  161. 
    
  162.         <DisplayName displayName={displayName} id={((id: any): number)} />
    
  163. 
    
  164.         {key && (
    
  165.           <Fragment>
    
  166.             &nbsp;<span className={styles.KeyName}>key</span>="
    
  167.             <span
    
  168.               className={styles.KeyValue}
    
  169.               title={key}
    
  170.               onDoubleClick={handleKeyDoubleClick}>
    
  171.               {key}
    
  172.             </span>
    
  173.             "
    
  174.           </Fragment>
    
  175.         )}
    
  176.         {hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
    
  177.           <Badge
    
  178.             className={styles.Badge}
    
  179.             hocDisplayNames={hocDisplayNames}
    
  180.             type={type}>
    
  181.             <DisplayName
    
  182.               displayName={hocDisplayNames[0]}
    
  183.               id={((id: any): number)}
    
  184.             />
    
  185.           </Badge>
    
  186.         ) : null}
    
  187.         {showInlineWarningsAndErrors && errorCount > 0 && (
    
  188.           <Icon
    
  189.             type="error"
    
  190.             className={
    
  191.               isSelected && treeFocused
    
  192.                 ? styles.ErrorIconContrast
    
  193.                 : styles.ErrorIcon
    
  194.             }
    
  195.           />
    
  196.         )}
    
  197.         {showInlineWarningsAndErrors && warningCount > 0 && (
    
  198.           <Icon
    
  199.             type="warning"
    
  200.             className={
    
  201.               isSelected && treeFocused
    
  202.                 ? styles.WarningIconContrast
    
  203.                 : styles.WarningIcon
    
  204.             }
    
  205.           />
    
  206.         )}
    
  207.         {showStrictModeBadge && (
    
  208.           <Icon
    
  209.             className={
    
  210.               isSelected && treeFocused
    
  211.                 ? styles.StrictModeContrast
    
  212.                 : styles.StrictMode
    
  213.             }
    
  214.             title="This component is not running in StrictMode."
    
  215.             type="strict-mode-non-compliant"
    
  216.           />
    
  217.         )}
    
  218.       </div>
    
  219.     </div>
    
  220.   );
    
  221. }
    
  222. 
    
  223. // Prevent double clicks on toggle from drilling into the owner list.
    
  224. // $FlowFixMe[missing-local-annot]
    
  225. const swallowDoubleClick = event => {
    
  226.   event.preventDefault();
    
  227.   event.stopPropagation();
    
  228. };
    
  229. 
    
  230. type ExpandCollapseToggleProps = {
    
  231.   element: ElementType,
    
  232.   store: Store,
    
  233. };
    
  234. 
    
  235. function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
    
  236.   const {children, id, isCollapsed} = element;
    
  237. 
    
  238.   // $FlowFixMe[missing-local-annot]
    
  239.   const toggleCollapsed = event => {
    
  240.     event.preventDefault();
    
  241.     event.stopPropagation();
    
  242. 
    
  243.     store.toggleIsCollapsed(id, !isCollapsed);
    
  244.   };
    
  245. 
    
  246.   // $FlowFixMe[missing-local-annot]
    
  247.   const stopPropagation = event => {
    
  248.     // Prevent the row from selecting
    
  249.     event.stopPropagation();
    
  250.   };
    
  251. 
    
  252.   if (children.length === 0) {
    
  253.     return <div className={styles.ExpandCollapseToggle} />;
    
  254.   }
    
  255. 
    
  256.   return (
    
  257.     <div
    
  258.       className={styles.ExpandCollapseToggle}
    
  259.       onMouseDown={stopPropagation}
    
  260.       onClick={toggleCollapsed}
    
  261.       onDoubleClick={swallowDoubleClick}>
    
  262.       <ButtonIcon type={isCollapsed ? 'collapsed' : 'expanded'} />
    
  263.     </div>
    
  264.   );
    
  265. }
    
  266. 
    
  267. type DisplayNameProps = {
    
  268.   displayName: string | null,
    
  269.   id: number,
    
  270. };
    
  271. 
    
  272. function DisplayName({displayName, id}: DisplayNameProps) {
    
  273.   const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
    
  274.   const isSearchResult = useMemo(() => {
    
  275.     return searchResults.includes(id);
    
  276.   }, [id, searchResults]);
    
  277.   const isCurrentResult =
    
  278.     searchIndex !== null && id === searchResults[searchIndex];
    
  279. 
    
  280.   if (!isSearchResult || displayName === null) {
    
  281.     return displayName;
    
  282.   }
    
  283. 
    
  284.   const match = createRegExp(searchText).exec(displayName);
    
  285. 
    
  286.   if (match === null) {
    
  287.     return displayName;
    
  288.   }
    
  289. 
    
  290.   const startIndex = match.index;
    
  291.   const stopIndex = startIndex + match[0].length;
    
  292. 
    
  293.   const children = [];
    
  294.   if (startIndex > 0) {
    
  295.     children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
    
  296.   }
    
  297.   children.push(
    
  298.     <mark
    
  299.       key="middle"
    
  300.       className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
    
  301.       {displayName.slice(startIndex, stopIndex)}
    
  302.     </mark>,
    
  303.   );
    
  304.   if (stopIndex < displayName.length) {
    
  305.     children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
    
  306.   }
    
  307. 
    
  308.   return children;
    
  309. }