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);
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

View File

@ -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"

View File

@ -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);

View File

@ -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}

View File

@ -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) {

View File

@ -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>
)}

View File

@ -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)
) : (

View File

@ -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}

View File

@ -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;
}

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.
- `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

View File

@ -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");

View File

@ -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>

View File

@ -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();
});