github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/ui/Popover.tsx (about)

     1  import React, {
     2    useRef,
     3    useState,
     4    useLayoutEffect,
     5    SetStateAction,
     6    Dispatch,
     7    ReactNode,
     8  } from 'react';
     9  import classnames from 'classnames';
    10  import OutsideClickHandler from 'react-outside-click-handler';
    11  import { useWindowWidth } from '@react-hook/window-size';
    12  import styles from './Popover.module.scss';
    13  
    14  export interface PopoverProps {
    15    isModalOpen: boolean;
    16    setModalOpenStatus: Dispatch<SetStateAction<boolean>>;
    17    children: ReactNode;
    18    className?: string;
    19  
    20    /** where to position the popover on the page */
    21    anchorPoint: {
    22      x: number | string;
    23      y: number;
    24    };
    25  }
    26  
    27  export function Popover({
    28    isModalOpen,
    29    setModalOpenStatus,
    30    className,
    31    children,
    32    anchorPoint,
    33  }: PopoverProps) {
    34    const popoverRef = useRef<HTMLDivElement>(null);
    35    const [popoverPosition, setPopoverPosition] = useState<React.CSSProperties>({
    36      display: 'hidden',
    37    });
    38    const windowWidth = useWindowWidth();
    39  
    40    useLayoutEffect(() => {
    41      if (isModalOpen && popoverRef.current) {
    42        const pos = getPopoverPosition(
    43          popoverRef.current.clientWidth,
    44          windowWidth,
    45          anchorPoint
    46        );
    47        setPopoverPosition(pos);
    48      }
    49    }, [isModalOpen, popoverRef.current?.clientWidth, windowWidth, anchorPoint]);
    50  
    51    return (
    52      <OutsideClickHandler onOutsideClick={() => setModalOpenStatus(false)}>
    53        <div
    54          className={styles.container}
    55          style={popoverPosition}
    56          ref={popoverRef}
    57        >
    58          {isModalOpen && (
    59            <div className={classnames(styles.popover, className)}>
    60              {children}
    61            </div>
    62          )}
    63        </div>
    64      </OutsideClickHandler>
    65    );
    66  }
    67  
    68  function getPopoverPosition(
    69    popoverWidth: number,
    70    windowWidth: number,
    71    anchorPoint: PopoverProps['anchorPoint']
    72  ) {
    73    // Give some room between popover end and the window edge
    74    const marginToWindowEdge = 30;
    75    const defaultProps = {
    76      top: `${anchorPoint.y}px`,
    77      position: 'absolute' as const,
    78    };
    79  
    80    if (typeof anchorPoint.x === 'string') {
    81      return {
    82        ...defaultProps,
    83        left: anchorPoint.x,
    84      };
    85    }
    86  
    87    if (anchorPoint.x + popoverWidth + marginToWindowEdge >= windowWidth) {
    88      // position to the left
    89      return {
    90        ...defaultProps,
    91        left: `${windowWidth - popoverWidth - marginToWindowEdge}px`,
    92      };
    93    }
    94  
    95    // position to the right
    96    return {
    97      ...defaultProps,
    98      left: `${anchorPoint.x}px`,
    99    };
   100  }
   101  interface PopoverMemberProps {
   102    children: ReactNode;
   103    className?: string;
   104  }
   105  
   106  export function PopoverHeader({ children, className }: PopoverMemberProps) {
   107    return <div className={classnames(styles.header, className)}>{children}</div>;
   108  }
   109  
   110  export function PopoverBody({ children, className }: PopoverMemberProps) {
   111    return <div className={classnames(styles.body, className)}>{children}</div>;
   112  }
   113  
   114  export function PopoverFooter({ children, className }: PopoverMemberProps) {
   115    return <div className={classnames(styles.footer, className)}>{children}</div>;
   116  }