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:
parent
69b6fbb3f4
commit
9325109836
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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,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>
|
||||
)}
|
||||
|
@ -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)
|
||||
) : (
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user