Popover.tsx 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. import React, { useLayoutEffect, useRef, useEffect } from "react";
  2. import "./Popover.scss";
  3. import { unstable_batchedUpdates } from "react-dom";
  4. type Props = {
  5. top?: number;
  6. left?: number;
  7. children?: React.ReactNode;
  8. onCloseRequest?(event: PointerEvent): void;
  9. fitInViewport?: boolean;
  10. offsetLeft?: number;
  11. offsetTop?: number;
  12. viewportWidth?: number;
  13. viewportHeight?: number;
  14. };
  15. export const Popover = ({
  16. children,
  17. left,
  18. top,
  19. onCloseRequest,
  20. fitInViewport = false,
  21. offsetLeft = 0,
  22. offsetTop = 0,
  23. viewportWidth = window.innerWidth,
  24. viewportHeight = window.innerHeight,
  25. }: Props) => {
  26. const popoverRef = useRef<HTMLDivElement>(null);
  27. // ensure the popover doesn't overflow the viewport
  28. useLayoutEffect(() => {
  29. if (fitInViewport && popoverRef.current) {
  30. const element = popoverRef.current;
  31. const { x, y, width, height } = element.getBoundingClientRect();
  32. if (x + width - offsetLeft > viewportWidth) {
  33. element.style.left = `${viewportWidth - width}px`;
  34. }
  35. if (y + height - offsetTop > viewportHeight) {
  36. element.style.top = `${viewportHeight - height}px`;
  37. }
  38. }
  39. }, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
  40. useEffect(() => {
  41. if (onCloseRequest) {
  42. const handler = (event: PointerEvent) => {
  43. if (!popoverRef.current?.contains(event.target as Node)) {
  44. unstable_batchedUpdates(() => onCloseRequest(event));
  45. }
  46. };
  47. document.addEventListener("pointerdown", handler, false);
  48. return () => document.removeEventListener("pointerdown", handler, false);
  49. }
  50. }, [onCloseRequest]);
  51. return (
  52. <div className="popover" style={{ top, left }} ref={popoverRef}>
  53. {children}
  54. </div>
  55. );
  56. };