瀏覽代碼

fix: use excal id so every element has unique id (#3696)

* fix: use excal id so every element has unique id

* fix

* fix

* fix

* add docs

* fix
Aakansha Doshi 3 年之前
父節點
當前提交
9325109836

+ 19 - 6
src/components/App.tsx

@@ -197,9 +197,10 @@ import { actionToggleViewMode } from "../actions/actionToggleViewMode";
 
 const IsMobileContext = React.createContext(false);
 export const useIsMobile = () => useContext(IsMobileContext);
-const ExcalidrawContainerContext = React.createContext<HTMLDivElement | null>(
-  null,
-);
+const ExcalidrawContainerContext = React.createContext<{
+  container: HTMLDivElement | null;
+  id: string | null;
+}>({ container: null, id: null });
 export const useExcalidrawContainer = () =>
   useContext(ExcalidrawContainerContext);
 
@@ -244,6 +245,10 @@ class App extends React.Component<AppProps, AppState> {
   public libraryItemsFromStorage: LibraryItems | undefined;
   private id: string;
   private history: History;
+  private excalidrawContainerValue: {
+    container: HTMLDivElement | null;
+    id: string;
+  };
 
   constructor(props: AppProps) {
     super(props);
@@ -300,6 +305,12 @@ class App extends React.Component<AppProps, AppState> {
       }
       readyPromise.resolve(api);
     }
+
+    this.excalidrawContainerValue = {
+      container: this.excalidrawContainerRef.current,
+      id: this.id,
+    };
+
     this.scene = new Scene();
     this.library = new Library(this);
     this.history = new History();
@@ -327,7 +338,7 @@ class App extends React.Component<AppProps, AppState> {
     if (viewModeEnabled) {
       return (
         <canvas
-          id="canvas"
+          className="excalidraw__canvas"
           style={{
             width: canvasDOMWidth,
             height: canvasDOMHeight,
@@ -349,7 +360,7 @@ class App extends React.Component<AppProps, AppState> {
     }
     return (
       <canvas
-        id="canvas"
+        className="excalidraw__canvas"
         style={{
           width: canvasDOMWidth,
           height: canvasDOMHeight,
@@ -394,7 +405,7 @@ class App extends React.Component<AppProps, AppState> {
         }
       >
         <ExcalidrawContainerContext.Provider
-          value={this.excalidrawContainerRef.current}
+          value={this.excalidrawContainerValue}
         >
           <IsMobileContext.Provider value={this.isMobile}>
             <LayerUI
@@ -725,6 +736,8 @@ class App extends React.Component<AppProps, AppState> {
   };
 
   public async componentDidMount() {
+    this.excalidrawContainerValue.container = this.excalidrawContainerRef.current;
+
     if (
       process.env.NODE_ENV === ENV.TEST ||
       process.env.NODE_ENV === ENV.DEVELOPMENT

+ 3 - 2
src/components/Dialog.tsx

@@ -2,7 +2,7 @@ import clsx from "clsx";
 import React, { useEffect, useState } from "react";
 import { useCallbackRefState } from "../hooks/useCallbackRefState";
 import { t } from "../i18n";
-import { useIsMobile } from "../components/App";
+import { useExcalidrawContainer, useIsMobile } from "../components/App";
 import { KEYS } from "../keys";
 import "./Dialog.scss";
 import { back, close } from "./icons";
@@ -21,6 +21,7 @@ export const Dialog = (props: {
 }) => {
   const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
   const [lastActiveElement] = useState(document.activeElement);
+  const { id } = useExcalidrawContainer();
 
   useEffect(() => {
     if (!islandNode) {
@@ -82,7 +83,7 @@ export const Dialog = (props: {
       theme={props.theme}
     >
       <Island ref={setIslandNode}>
-        <h2 id="dialog-title" className="Dialog__title">
+        <h2 id={`${id}-dialog-title`} className="Dialog__title">
           <span className="Dialog__titleContent">{props.title}</span>
           <button
             className="Modal__close"

+ 1 - 1
src/components/ErrorDialog.tsx

@@ -12,7 +12,7 @@ export const ErrorDialog = ({
   onClose?: () => void;
 }) => {
   const [modalIsShown, setModalIsShown] = useState(!!message);
-  const excalidrawContainer = useExcalidrawContainer();
+  const { container: excalidrawContainer } = useExcalidrawContainer();
 
   const handleClose = React.useCallback(() => {
     setModalIsShown(false);

+ 0 - 2
src/components/LockIcon.tsx

@@ -8,7 +8,6 @@ type LockIconSize = "s" | "m";
 type LockIconProps = {
   title?: string;
   name?: string;
-  id?: string;
   checked: boolean;
   onChange?(): void;
   size?: LockIconSize;
@@ -57,7 +56,6 @@ export const LockIcon = (props: LockIconProps) => {
         className="ToolIcon_type_checkbox"
         type="checkbox"
         name={props.name}
-        id={props.id}
         onChange={props.onChange}
         checked={props.checked}
         aria-label={props.title}

+ 1 - 1
src/components/Modal.tsx

@@ -58,7 +58,7 @@ const useBodyRoot = (theme: AppState["theme"]) => {
   const isMobileRef = useRef(isMobile);
   isMobileRef.current = isMobile;
 
-  const excalidrawContainer = useExcalidrawContainer();
+  const { container: excalidrawContainer } = useExcalidrawContainer();
 
   useLayoutEffect(() => {
     if (div) {

+ 4 - 2
src/components/ProjectName.tsx

@@ -4,6 +4,7 @@ import React, { useState } from "react";
 import { focusNearestParent } from "../utils";
 
 import "./ProjectName.scss";
+import { useExcalidrawContainer } from "./App";
 
 type Props = {
   value: string;
@@ -13,6 +14,7 @@ type Props = {
 };
 
 export const ProjectName = (props: Props) => {
+  const { id } = useExcalidrawContainer();
   const [fileName, setFileName] = useState<string>(props.value);
 
   const handleBlur = (event: any) => {
@@ -43,12 +45,12 @@ export const ProjectName = (props: Props) => {
           className="TextInput"
           onBlur={handleBlur}
           onKeyDown={handleKeyDown}
-          id="filename"
+          id={`${id}-filename`}
           value={fileName}
           onChange={(event) => setFileName(event.target.value)}
         />
       ) : (
-        <span className="TextInput TextInput--readonly" id="filename">
+        <span className="TextInput TextInput--readonly" id={`${id}-filename`}>
           {props.value}
         </span>
       )}

+ 4 - 2
src/components/Section.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import { t } from "../i18n";
+import { useExcalidrawContainer } from "./App";
 
 interface SectionProps extends React.HTMLProps<HTMLElement> {
   heading: string;
@@ -7,13 +8,14 @@ interface SectionProps extends React.HTMLProps<HTMLElement> {
 }
 
 export const Section = ({ heading, children, ...props }: SectionProps) => {
+  const { id } = useExcalidrawContainer();
   const header = (
-    <h2 className="visually-hidden" id={`${heading}-title`}>
+    <h2 className="visually-hidden" id={`${id}-${heading}-title`}>
       {t(`headings.${heading}`)}
     </h2>
   );
   return (
-    <section {...props} aria-labelledby={`${heading}-title`}>
+    <section {...props} aria-labelledby={`${id}-${heading}-title`}>
       {typeof children === "function" ? (
         children(header)
       ) : (

+ 3 - 1
src/components/ToolButton.tsx

@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
 
 import React from "react";
 import clsx from "clsx";
+import { useExcalidrawContainer } from "./App";
 
 type ToolIconSize = "s" | "m";
 
@@ -43,6 +44,7 @@ type ToolButtonProps =
 const DEFAULT_SIZE: ToolIconSize = "m";
 
 export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
+  const { id: excalId } = useExcalidrawContainer();
   const innerRef = React.useRef(null);
   React.useImperativeHandle(ref, () => innerRef.current);
   const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
@@ -98,7 +100,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
         aria-label={props["aria-label"]}
         aria-keyshortcuts={props["aria-keyshortcuts"]}
         data-testid={props["data-testid"]}
-        id={props.id}
+        id={`${excalId}-${props.id}`}
         onChange={props.onChange}
         checked={props.checked}
         ref={innerRef}

+ 3 - 2
src/css/styles.scss

@@ -51,11 +51,12 @@
     image-rendering: -moz-crisp-edges; // FF
 
     z-index: var(--zIndex-canvas);
-  }
 
-  #canvas {
     // Remove the main canvas from document flow to avoid resizeObserver
     // feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
+  }
+
+  &__canvas {
     position: absolute;
   }
 

+ 4 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -35,6 +35,10 @@ Please add the latest change on the top under the correct section.
 - `UIOptions.canvasActions.saveAsScene` is now renamed to `UiOptions.canvasActions.export.saveFileToDisk`. Defaults to `true` hence the **save file to disk** button is rendered inside the export dialog.
 - `exportToBackend` is now renamed to `UIOptions.canvasActions.export.exportToBackend`. If this prop is not passed, the **shareable-link** button will not be rendered, same as before.
 
+### Fixes
+
+- Use excalidraw Id in elements so every element has unique id [#3696](https://github.com/excalidraw/excalidraw/pull/3696).
+
 ### Refactor
 
 - #### BREAKING CHANGE

+ 5 - 0
src/setupTests.ts

@@ -1,6 +1,11 @@
 import "@testing-library/jest-dom";
 import "jest-canvas-mock";
 
+jest.mock("nanoid", () => {
+  return {
+    nanoid: jest.fn(() => "test-id"),
+  };
+});
 // ReactDOM is located inside index.tsx file
 // as a result, we need a place for it to render into
 const element = document.createElement("div");

+ 4 - 4
src/tests/packages/__snapshots__/excalidraw.test.tsx.snap

@@ -2,12 +2,12 @@
 
 exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide any UI element when canvasActions is "undefined" 1`] = `
 <section
-  aria-labelledby="canvasActions-title"
+  aria-labelledby="test-id-canvasActions-title"
   class="zen-mode-transition"
 >
   <h2
     class="visually-hidden"
-    id="canvasActions-title"
+    id="test-id-canvasActions-title"
   >
     Canvas actions
   </h2>
@@ -201,12 +201,12 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
 
 exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when the UIOptions prop is "undefined" 1`] = `
 <section
-  aria-labelledby="canvasActions-title"
+  aria-labelledby="test-id-canvasActions-title"
   class="zen-mode-transition"
 >
   <h2
     class="visually-hidden"
-    id="canvasActions-title"
+    id="test-id-canvasActions-title"
   >
     Canvas actions
   </h2>

+ 2 - 4
src/tests/packages/excalidraw.test.tsx

@@ -136,7 +136,7 @@ describe("<Excalidraw/>", () => {
       await render(<Excalidraw />);
 
       const canvasActions = document.querySelector(
-        'section[aria-labelledby="canvasActions-title"]',
+        'section[aria-labelledby="test-id-canvasActions-title"]',
       );
 
       expect(canvasActions).toMatchSnapshot();
@@ -145,11 +145,9 @@ describe("<Excalidraw/>", () => {
     describe("Test canvasActions", () => {
       it('should not hide any UI element when canvasActions is "undefined"', async () => {
         await render(<Excalidraw UIOptions={{}} />);
-
         const canvasActions = document.querySelector(
-          'section[aria-labelledby="canvasActions-title"]',
+          'section[aria-labelledby="test-id-canvasActions-title"]',
         );
-
         expect(canvasActions).toMatchSnapshot();
       });