Modal.tsx 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import "./Modal.scss";
  2. import React, { useState, useLayoutEffect, useRef } from "react";
  3. import { createPortal } from "react-dom";
  4. import clsx from "clsx";
  5. import { KEYS } from "../keys";
  6. import { useExcalidrawContainer, useIsMobile } from "./App";
  7. import { AppState } from "../types";
  8. import { THEME } from "../constants";
  9. export const Modal = (props: {
  10. className?: string;
  11. children: React.ReactNode;
  12. maxWidth?: number;
  13. onCloseRequest(): void;
  14. labelledBy: string;
  15. theme?: AppState["theme"];
  16. closeOnClickOutside?: boolean;
  17. }) => {
  18. const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
  19. const modalRoot = useBodyRoot(theme);
  20. if (!modalRoot) {
  21. return null;
  22. }
  23. const handleKeydown = (event: React.KeyboardEvent) => {
  24. if (event.key === KEYS.ESCAPE) {
  25. event.nativeEvent.stopImmediatePropagation();
  26. event.stopPropagation();
  27. props.onCloseRequest();
  28. }
  29. };
  30. return createPortal(
  31. <div
  32. className={clsx("Modal", props.className)}
  33. role="dialog"
  34. aria-modal="true"
  35. onKeyDown={handleKeydown}
  36. aria-labelledby={props.labelledBy}
  37. >
  38. <div
  39. className="Modal__background"
  40. onClick={closeOnClickOutside ? props.onCloseRequest : undefined}
  41. ></div>
  42. <div
  43. className="Modal__content"
  44. style={{ "--max-width": `${props.maxWidth}px` }}
  45. tabIndex={0}
  46. >
  47. {props.children}
  48. </div>
  49. </div>,
  50. modalRoot,
  51. );
  52. };
  53. const useBodyRoot = (theme: AppState["theme"]) => {
  54. const [div, setDiv] = useState<HTMLDivElement | null>(null);
  55. const isMobile = useIsMobile();
  56. const isMobileRef = useRef(isMobile);
  57. isMobileRef.current = isMobile;
  58. const { container: excalidrawContainer } = useExcalidrawContainer();
  59. useLayoutEffect(() => {
  60. if (div) {
  61. div.classList.toggle("excalidraw--mobile", isMobile);
  62. }
  63. }, [div, isMobile]);
  64. useLayoutEffect(() => {
  65. const isDarkTheme =
  66. !!excalidrawContainer?.classList.contains("theme--dark") ||
  67. theme === "dark";
  68. const div = document.createElement("div");
  69. div.classList.add("excalidraw", "excalidraw-modal-container");
  70. div.classList.toggle("excalidraw--mobile", isMobileRef.current);
  71. if (isDarkTheme) {
  72. div.classList.add("theme--dark");
  73. div.classList.add("theme--dark-background-none");
  74. }
  75. document.body.appendChild(div);
  76. setDiv(div);
  77. return () => {
  78. document.body.removeChild(div);
  79. };
  80. }, [excalidrawContainer, theme]);
  81. return div;
  82. };