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
This commit is contained in:
Aakansha Doshi 2021-06-10 02:46:56 +05:30 committed by GitHub
parent 69b6fbb3f4
commit 9325109836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 53 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. - `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. - `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 ### Refactor
- #### BREAKING CHANGE - #### BREAKING CHANGE

View File

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

View File

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

View File

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