Modal.tsx 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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. }) => {
  17. const { theme = THEME.LIGHT } = props;
  18. const modalRoot = useBodyRoot(theme);
  19. if (!modalRoot) {
  20. return null;
  21. }
  22. const handleKeydown = (event: React.KeyboardEvent) => {
  23. if (event.key === KEYS.ESCAPE) {
  24. event.nativeEvent.stopImmediatePropagation();
  25. event.stopPropagation();
  26. props.onCloseRequest();
  27. }
  28. };
  29. return createPortal(
  30. <div
  31. className={clsx("Modal", props.className)}
  32. role="dialog"
  33. aria-modal="true"
  34. onKeyDown={handleKeydown}
  35. aria-labelledby={props.labelledBy}
  36. >
  37. <div className="Modal__background" onClick={props.onCloseRequest}></div>
  38. <div
  39. className="Modal__content"
  40. style={{ "--max-width": `${props.maxWidth}px` }}
  41. tabIndex={0}
  42. >
  43. {props.children}
  44. </div>
  45. </div>,
  46. modalRoot,
  47. );
  48. };
  49. const useBodyRoot = (theme: AppState["theme"]) => {
  50. const [div, setDiv] = useState<HTMLDivElement | null>(null);
  51. const isMobile = useIsMobile();
  52. const isMobileRef = useRef(isMobile);
  53. isMobileRef.current = isMobile;
  54. const { container: excalidrawContainer } = useExcalidrawContainer();
  55. useLayoutEffect(() => {
  56. if (div) {
  57. div.classList.toggle("excalidraw--mobile", isMobile);
  58. }
  59. }, [div, isMobile]);
  60. useLayoutEffect(() => {
  61. const isDarkTheme =
  62. !!excalidrawContainer?.classList.contains("theme--dark") ||
  63. theme === "dark";
  64. const div = document.createElement("div");
  65. div.classList.add("excalidraw", "excalidraw-modal-container");
  66. div.classList.toggle("excalidraw--mobile", isMobileRef.current);
  67. if (isDarkTheme) {
  68. div.classList.add("theme--dark");
  69. div.classList.add("theme--dark-background-none");
  70. }
  71. document.body.appendChild(div);
  72. setDiv(div);
  73. return () => {
  74. document.body.removeChild(div);
  75. };
  76. }, [excalidrawContainer, theme]);
  77. return div;
  78. };