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 {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
    
  12. import {createPortal} from 'react-dom';
    
  13. import {RegistryContext} from './Contexts';
    
  14. 
    
  15. import styles from './ContextMenu.css';
    
  16. 
    
  17. import type {RegistryContextType} from './Contexts';
    
  18. 
    
  19. function repositionToFit(element: HTMLElement, pageX: number, pageY: number) {
    
  20.   const ownerWindow = element.ownerDocument.defaultView;
    
  21.   if (element !== null) {
    
  22.     if (pageY + element.offsetHeight >= ownerWindow.innerHeight) {
    
  23.       if (pageY - element.offsetHeight > 0) {
    
  24.         element.style.top = `${pageY - element.offsetHeight}px`;
    
  25.       } else {
    
  26.         element.style.top = '0px';
    
  27.       }
    
  28.     } else {
    
  29.       element.style.top = `${pageY}px`;
    
  30.     }
    
  31. 
    
  32.     if (pageX + element.offsetWidth >= ownerWindow.innerWidth) {
    
  33.       if (pageX - element.offsetWidth > 0) {
    
  34.         element.style.left = `${pageX - element.offsetWidth}px`;
    
  35.       } else {
    
  36.         element.style.left = '0px';
    
  37.       }
    
  38.     } else {
    
  39.       element.style.left = `${pageX}px`;
    
  40.     }
    
  41.   }
    
  42. }
    
  43. 
    
  44. const HIDDEN_STATE = {
    
  45.   data: null,
    
  46.   isVisible: false,
    
  47.   pageX: 0,
    
  48.   pageY: 0,
    
  49. };
    
  50. 
    
  51. type Props = {
    
  52.   children: (data: Object) => React$Node,
    
  53.   id: string,
    
  54. };
    
  55. 
    
  56. export default function ContextMenu({children, id}: Props): React.Node {
    
  57.   const {hideMenu, registerMenu} =
    
  58.     useContext<RegistryContextType>(RegistryContext);
    
  59. 
    
  60.   const [state, setState] = useState(HIDDEN_STATE);
    
  61. 
    
  62.   const bodyAccessorRef = useRef(null);
    
  63.   const containerRef = useRef(null);
    
  64.   const menuRef = useRef(null);
    
  65. 
    
  66.   useEffect(() => {
    
  67.     const element = bodyAccessorRef.current;
    
  68.     if (element !== null) {
    
  69.       const ownerDocument = element.ownerDocument;
    
  70.       containerRef.current = ownerDocument.querySelector(
    
  71.         '[data-react-devtools-portal-root]',
    
  72.       );
    
  73. 
    
  74.       if (containerRef.current == null) {
    
  75.         console.warn(
    
  76.           'DevTools tooltip root node not found; context menus will be disabled.',
    
  77.         );
    
  78.       }
    
  79.     }
    
  80.   }, []);
    
  81. 
    
  82.   useEffect(() => {
    
  83.     const showMenuFn = ({
    
  84.       data,
    
  85.       pageX,
    
  86.       pageY,
    
  87.     }: {
    
  88.       data: any,
    
  89.       pageX: number,
    
  90.       pageY: number,
    
  91.     }) => {
    
  92.       setState({data, isVisible: true, pageX, pageY});
    
  93.     };
    
  94.     const hideMenuFn = () => setState(HIDDEN_STATE);
    
  95.     return registerMenu(id, showMenuFn, hideMenuFn);
    
  96.   }, [id]);
    
  97. 
    
  98.   useLayoutEffect(() => {
    
  99.     if (!state.isVisible) {
    
  100.       return;
    
  101.     }
    
  102. 
    
  103.     const menu = ((menuRef.current: any): HTMLElement);
    
  104.     const container = containerRef.current;
    
  105.     if (container !== null) {
    
  106.       // $FlowFixMe[missing-local-annot]
    
  107.       const hideUnlessContains = event => {
    
  108.         if (!menu.contains(event.target)) {
    
  109.           hideMenu();
    
  110.         }
    
  111.       };
    
  112. 
    
  113.       const ownerDocument = container.ownerDocument;
    
  114.       ownerDocument.addEventListener('mousedown', hideUnlessContains);
    
  115.       ownerDocument.addEventListener('touchstart', hideUnlessContains);
    
  116.       ownerDocument.addEventListener('keydown', hideUnlessContains);
    
  117. 
    
  118.       const ownerWindow = ownerDocument.defaultView;
    
  119.       ownerWindow.addEventListener('resize', hideMenu);
    
  120. 
    
  121.       repositionToFit(menu, state.pageX, state.pageY);
    
  122. 
    
  123.       return () => {
    
  124.         ownerDocument.removeEventListener('mousedown', hideUnlessContains);
    
  125.         ownerDocument.removeEventListener('touchstart', hideUnlessContains);
    
  126.         ownerDocument.removeEventListener('keydown', hideUnlessContains);
    
  127. 
    
  128.         ownerWindow.removeEventListener('resize', hideMenu);
    
  129.       };
    
  130.     }
    
  131.   }, [state]);
    
  132. 
    
  133.   if (!state.isVisible) {
    
  134.     return <div ref={bodyAccessorRef} />;
    
  135.   } else {
    
  136.     const container = containerRef.current;
    
  137.     if (container !== null) {
    
  138.       return createPortal(
    
  139.         <div ref={menuRef} className={styles.ContextMenu}>
    
  140.           {children(state.data)}
    
  141.         </div>,
    
  142.         container,
    
  143.       );
    
  144.     } else {
    
  145.       return null;
    
  146.     }
    
  147.   }
    
  148. }