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 }