Modal.tsx 2.3 KB

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