浏览代码

fix: remove legacy React.render() from the editor (#5893)

David Luzar 2 年之前
父节点
当前提交
8ed0fc2c87
共有 3 个文件被更改,包括 33 次插入48 次删除
  1. 1 0
      src/components/App.tsx
  2. 25 30
      src/components/ContextMenu.tsx
  3. 7 18
      src/components/ImageExportDialog.tsx

+ 1 - 0
src/components/App.tsx

@@ -5917,6 +5917,7 @@ class App extends React.Component<AppProps, AppState> {
     },
     type: "canvas" | "element",
   ) => {
+    trackEvent("contextMenu", "openContextMenu", type);
     if (this.state.showHyperlinkPopup) {
       this.setState({ showHyperlinkPopup: false });
     }

+ 25 - 30
src/components/ContextMenu.tsx

@@ -1,4 +1,4 @@
-import { render, unmountComponentAtNode } from "react-dom";
+import { createRoot, Root } from "react-dom/client";
 import clsx from "clsx";
 import { Popover } from "./Popover";
 import { t } from "../i18n";
@@ -89,42 +89,38 @@ const ContextMenu = ({
   );
 };
 
-const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
+const contextMenuRoots = new WeakMap<HTMLElement, Root>();
 
-const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
-  let contextMenuNode = contextMenuNodeByContainer.get(container);
-  if (contextMenuNode) {
-    return contextMenuNode;
+const getContextMenuRoot = (container: HTMLElement): Root => {
+  let contextMenuRoot = contextMenuRoots.get(container);
+  if (contextMenuRoot) {
+    return contextMenuRoot;
   }
-  contextMenuNode = document.createElement("div");
-  container
-    .querySelector(".excalidraw-contextMenuContainer")!
-    .appendChild(contextMenuNode);
-  contextMenuNodeByContainer.set(container, contextMenuNode);
-  return contextMenuNode;
-};
-
-type ContextMenuParams = {
-  options: (ContextMenuOption | false | null | undefined)[];
-  top: ContextMenuProps["top"];
-  left: ContextMenuProps["left"];
-  actionManager: ContextMenuProps["actionManager"];
-  appState: Readonly<AppState>;
-  container: HTMLElement;
-  elements: readonly NonDeletedExcalidrawElement[];
+  contextMenuRoot = createRoot(
+    container.querySelector(".excalidraw-contextMenuContainer")!,
+  );
+  contextMenuRoots.set(container, contextMenuRoot);
+  return contextMenuRoot;
 };
 
 const handleClose = (container: HTMLElement) => {
-  const contextMenuNode = contextMenuNodeByContainer.get(container);
-  if (contextMenuNode) {
-    unmountComponentAtNode(contextMenuNode);
-    contextMenuNode.remove();
-    contextMenuNodeByContainer.delete(container);
+  const contextMenuRoot = contextMenuRoots.get(container);
+  if (contextMenuRoot) {
+    contextMenuRoot.unmount();
+    contextMenuRoots.delete(container);
   }
 };
 
 export default {
-  push(params: ContextMenuParams) {
+  push(params: {
+    options: (ContextMenuOption | false | null | undefined)[];
+    top: ContextMenuProps["top"];
+    left: ContextMenuProps["left"];
+    actionManager: ContextMenuProps["actionManager"];
+    appState: Readonly<AppState>;
+    container: HTMLElement;
+    elements: readonly NonDeletedExcalidrawElement[];
+  }) {
     const options = Array.of<ContextMenuOption>();
     params.options.forEach((option) => {
       if (option) {
@@ -132,7 +128,7 @@ export default {
       }
     });
     if (options.length) {
-      render(
+      getContextMenuRoot(params.container).render(
         <ContextMenu
           top={params.top}
           left={params.left}
@@ -142,7 +138,6 @@ export default {
           appState={params.appState}
           elements={params.elements}
         />,
-        getContextMenuNode(params.container),
       );
     }
   },

+ 7 - 18
src/components/ImageExportDialog.tsx

@@ -1,9 +1,7 @@
 import React, { useEffect, useRef, useState } from "react";
-import { render, unmountComponentAtNode } from "react-dom";
 import { probablySupportsClipboardBlob } from "../clipboard";
 import { canvasToBlob } from "../data/blob";
 import { NonDeletedExcalidrawElement } from "../element/types";
-import { CanvasError } from "../errors";
 import { t } from "../i18n";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
 import { exportToCanvas } from "../scene/export";
@@ -33,19 +31,6 @@ export const ErrorCanvasPreview = () => {
   );
 };
 
-const renderPreview = (
-  content: HTMLCanvasElement | Error,
-  previewNode: HTMLDivElement,
-) => {
-  unmountComponentAtNode(previewNode);
-  previewNode.innerHTML = "";
-  if (content instanceof HTMLCanvasElement) {
-    previewNode.appendChild(content);
-  } else {
-    render(<ErrorCanvasPreview />, previewNode);
-  }
-};
-
 export type ExportCB = (
   elements: readonly NonDeletedExcalidrawElement[],
   scale?: number,
@@ -99,6 +84,7 @@ const ImageExportModal = ({
   const [exportSelected, setExportSelected] = useState(someElementIsSelected);
   const previewRef = useRef<HTMLDivElement>(null);
   const { exportBackground, viewBackgroundColor } = appState;
+  const [renderError, setRenderError] = useState<Error | null>(null);
 
   const exportedElements = exportSelected
     ? getSelectedElements(elements, appState, true)
@@ -119,15 +105,16 @@ const ImageExportModal = ({
       exportPadding,
     })
       .then((canvas) => {
+        setRenderError(null);
         // if converting to blob fails, there's some problem that will
         // likely prevent preview and export (e.g. canvas too big)
         return canvasToBlob(canvas).then(() => {
-          renderPreview(canvas, previewNode);
+          previewNode.replaceChildren(canvas);
         });
       })
       .catch((error) => {
         console.error(error);
-        renderPreview(new CanvasError(), previewNode);
+        setRenderError(error);
       });
   }, [
     appState,
@@ -140,7 +127,9 @@ const ImageExportModal = ({
 
   return (
     <div className="ExportDialog">
-      <div className="ExportDialog__preview" ref={previewRef} />
+      <div className="ExportDialog__preview" ref={previewRef}>
+        {renderError && <ErrorCanvasPreview />}
+      </div>
       {supportsContextFilters &&
         actionManager.renderAction("exportWithDarkMode")}
       <div style={{ display: "grid", gridTemplateColumns: "1fr" }}>