feat: Support customising canvas actions 🎉 (#3364)
* feat: Support hiding save, save as, clear & export * Remove canvasActions from state & minor changes * Rename prop to UIOptions & pass default value * Make requested changes * better type checking so that optional check not needed at every point * remove optional checks * Add few tests * Add describe block for canvasActions & use snapshot tests * Add support for hiding canvas background picker * Take snapshot of canvasActions instead of the whole app * Add support for hiding dark mode toggle * Update README.md * Rename table heading * Update changelog * Make requested changes * Update test name * tweaks Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
parent
c54a099010
commit
233576628c
@ -33,6 +33,7 @@ export const actionChangeViewBackgroundColor = register({
|
||||
type="canvasBackground"
|
||||
color={appState.viewBackgroundColor}
|
||||
onChange={(color) => updateData(color)}
|
||||
data-testid="canvas-background-picker"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -72,6 +73,7 @@ export const actionClearCanvas = register({
|
||||
updateData(null);
|
||||
}
|
||||
}}
|
||||
data-testid="clear-canvas-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
@ -136,6 +136,7 @@ export const actionSaveScene = register({
|
||||
aria-label={t("buttons.save")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
@ -167,6 +168,7 @@ export const actionSaveAsScene = register({
|
||||
showAriaLabel={useIsMobile()}
|
||||
hidden={!supported}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-as-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
@ -204,6 +206,7 @@ export const actionLoadScene = register({
|
||||
aria-label={t("buttons.load")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={updateData}
|
||||
data-testid="load-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
ActionResult,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { AppProps, AppState } from "../types";
|
||||
import { MODES } from "../constants";
|
||||
|
||||
// This is the <App> component, but for now we don't care about anything but its
|
||||
// `canvas` state.
|
||||
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
||||
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
|
||||
|
||||
export class ActionManager implements ActionsManagerInterface {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@ -52,10 +52,14 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||
const data = Object.values(this.actions)
|
||||
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
||||
.filter(
|
||||
(action) =>
|
||||
(action.name in canvasActions
|
||||
? canvasActions[action.name as keyof typeof canvasActions]
|
||||
: true) &&
|
||||
action.keyTest &&
|
||||
action.keyTest(
|
||||
event,
|
||||
@ -102,7 +106,15 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
// like the user list. We can use this key to extract more
|
||||
// data from app state. This is an alternative to generic prop hell!
|
||||
renderAction = (name: ActionName, id?: string) => {
|
||||
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||
|
||||
if (
|
||||
this.actions[name] &&
|
||||
"PanelComponent" in this.actions[name] &&
|
||||
(name in canvasActions
|
||||
? canvasActions[name as keyof typeof canvasActions]
|
||||
: true)
|
||||
) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
const updateData = (formState?: any) => {
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
import {
|
||||
APP_NAME,
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_UI_OPTIONS,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
@ -160,13 +161,7 @@ import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
Gesture,
|
||||
GestureEvent,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
|
||||
import {
|
||||
debounce,
|
||||
distance,
|
||||
@ -286,16 +281,21 @@ export type ExcalidrawImperativeAPI = {
|
||||
ready: true;
|
||||
};
|
||||
|
||||
class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
class App extends React.Component<AppProps, AppState> {
|
||||
canvas: HTMLCanvasElement | null = null;
|
||||
rc: RoughCanvas | null = null;
|
||||
unmounted: boolean = false;
|
||||
actionManager: ActionManager;
|
||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
public static defaultProps: Partial<AppProps> = {
|
||||
// needed for tests to pass since we directly render App in many tests
|
||||
UIOptions: DEFAULT_UI_OPTIONS,
|
||||
};
|
||||
|
||||
private scene: Scene;
|
||||
private resizeObserver: ResizeObserver | undefined;
|
||||
constructor(props: ExcalidrawProps) {
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
const defaultAppState = getDefaultAppState();
|
||||
const {
|
||||
@ -466,8 +466,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
||||
}
|
||||
showThemeBtn={typeof this.props?.theme === "undefined"}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
this.props.UIOptions.canvasActions.theme
|
||||
}
|
||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||
UIOptions={this.props.UIOptions}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
{this.state.showStats && (
|
||||
@ -878,7 +882,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||
if (prevProps.langCode !== this.props.langCode) {
|
||||
this.updateLanguage();
|
||||
}
|
||||
|
@ -17,7 +17,13 @@ import { Language, t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
|
||||
import {
|
||||
AppProps,
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
LibraryItem,
|
||||
LibraryItems,
|
||||
} from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
@ -65,6 +71,7 @@ interface LayerUIProps {
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
viewModeEnabled: boolean;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}
|
||||
|
||||
const useOnClickOutside = (
|
||||
@ -339,6 +346,7 @@ const LayerUI = ({
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
libraryReturnUrl,
|
||||
UIOptions,
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -359,6 +367,10 @@ const LayerUI = ({
|
||||
);
|
||||
|
||||
const renderExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createExporter = (type: ExportType): ExportCB => async (
|
||||
exportedElements,
|
||||
scale,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FontFamily } from "./element/types";
|
||||
import cssVariables from "./css/variables.module.scss";
|
||||
import { AppProps } from "./types";
|
||||
|
||||
export const APP_NAME = "Excalidraw";
|
||||
|
||||
@ -124,3 +125,15 @@ export const URL_QUERY_KEYS = {
|
||||
export const URL_HASH_KEYS = {
|
||||
addLibrary: "addLibrary",
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
canvasActions: {
|
||||
changeViewBackgroundColor: true,
|
||||
clearCanvas: true,
|
||||
export: true,
|
||||
loadScene: true,
|
||||
saveAsScene: true,
|
||||
saveScene: true,
|
||||
theme: true,
|
||||
},
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `UIOptions` prop to customise `canvas actions` which includes customising `background color picker`, `clear canvas`, `export`, `load`, `save`, `save as` & `theme toggle` [#3364](https://github.com/excalidraw/excalidraw/pull/3364).
|
||||
- Calculate `width/height` of canvas based on excalidraw component (".excalidraw" selector) & also resize and update offsets whenever the dimensions of excalidraw component gets updated [#3379](https://github.com/excalidraw/excalidraw/pull/3379). You also don't need to add a resize handler anymore for excalidraw as its handled now in excalidraw itself.
|
||||
#### BREAKING CHANGE
|
||||
- `width/height` props have been removed. Instead now it takes `100%` of `width` and `height` of the container so you need to make sure the container in which you are rendering Excalidraw has non zero dimensions (It should have non zero width and height so Excalidraw can match the dimensions of containing block)
|
||||
|
@ -364,6 +364,7 @@ To view the full example visit :point_down:
|
||||
| [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component |
|
||||
| [`name`](#name) | string | | Name of the drawing |
|
||||
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) |
|
||||
|
||||
### Dimensions of Excalidraw
|
||||
|
||||
@ -528,6 +529,26 @@ This prop controls Excalidraw's theme. When supplied, the value takes precedence
|
||||
|
||||
This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
||||
|
||||
### `UIOptions`
|
||||
|
||||
This prop can be used to customise UI of Excalidraw. Currently we support customising only [`canvasActions`](#canvasActions). It accepts the below parameters
|
||||
|
||||
<pre>
|
||||
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
|
||||
</pre>
|
||||
|
||||
#### canvasActions
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `changeViewBackgroundColor` | boolean | true | Implies whether to show `Background color picker` |
|
||||
| `clearCanvas` | boolean | true | Implies whether to show `Clear canvas button` |
|
||||
| `export` | boolean | true | Implies whether to show `Export button` |
|
||||
| `loadScene` | boolean | true | Implies whether to show `Load button` |
|
||||
| `saveAsScene` | boolean | true | Implies whether to show `Save as button` |
|
||||
| `saveScene` | boolean | true | Implies whether to show `Save button` |
|
||||
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
|
||||
|
||||
### Does it support collaboration ?
|
||||
|
||||
No Excalidraw package doesn't come with collaboration, since this would have different implementations on the consumer so we expose the API's which you can use to communicate with Excalidraw as mentioned above. If you are interested in understanding how Excalidraw does it you can check it [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
|
||||
|
@ -10,6 +10,7 @@ import "../../css/styles.scss";
|
||||
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||
import { IsMobileProvider } from "../../is-mobile";
|
||||
import { defaultLang } from "../../i18n";
|
||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||
|
||||
const Excalidraw = (props: ExcalidrawProps) => {
|
||||
const {
|
||||
@ -31,6 +32,15 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
renderCustomStats,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
|
||||
const UIOptions = {
|
||||
canvasActions: {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Block pinch-zooming on iOS outside of the content area
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
@ -69,6 +79,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
theme={theme}
|
||||
name={name}
|
||||
renderCustomStats={renderCustomStats}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
</IsMobileProvider>
|
||||
</InitializeApp>
|
||||
@ -94,6 +105,7 @@ const areEqual = (
|
||||
|
||||
Excalidraw.defaultProps = {
|
||||
lanCode: defaultLang.code,
|
||||
UIOptions: DEFAULT_UI_OPTIONS,
|
||||
};
|
||||
|
||||
const forwardedRefComp = forwardRef<
|
||||
|
439
src/tests/__snapshots__/excalidrawPackage.test.tsx.snap
Normal file
439
src/tests/__snapshots__/excalidrawPackage.test.tsx.snap
Normal file
@ -0,0 +1,439 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide any UI element when canvasActions is "undefined" 1`] = `
|
||||
<section
|
||||
aria-labelledby="canvasActions-title"
|
||||
class="zen-mode-transition"
|
||||
>
|
||||
<h2
|
||||
class="visually-hidden"
|
||||
id="canvasActions-title"
|
||||
>
|
||||
Canvas actions
|
||||
</h2>
|
||||
<div
|
||||
class="Island"
|
||||
style="--padding: 2; z-index: 1;"
|
||||
>
|
||||
<div
|
||||
class="Stack Stack_vertical"
|
||||
style="--gap: 4;"
|
||||
>
|
||||
<div
|
||||
class="Stack Stack_horizontal"
|
||||
style="--gap: 1; justify-content: space-between;"
|
||||
>
|
||||
<button
|
||||
aria-label="Load"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="load-button"
|
||||
title="Load"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="save-button"
|
||||
title="Save"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save as"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
|
||||
data-testid="save-as-button"
|
||||
hidden=""
|
||||
title="Save as"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Export"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="export-button"
|
||||
title="Export"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex;"
|
||||
>
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="color-picker-control-container"
|
||||
>
|
||||
<button
|
||||
aria-label="Canvas background"
|
||||
class="color-picker-label-swatch"
|
||||
style="--swatch-color: #ffffff;"
|
||||
/>
|
||||
<label
|
||||
class="color-input-container"
|
||||
>
|
||||
<div
|
||||
class="color-picker-hash"
|
||||
>
|
||||
#
|
||||
</div>
|
||||
<input
|
||||
aria-label="Canvas background"
|
||||
class="color-picker-input"
|
||||
spellcheck="false"
|
||||
value="ffffff"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="margin-inline-start: 0.25rem;"
|
||||
>
|
||||
<label
|
||||
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
||||
data-testid="toggle-dark-mode"
|
||||
title="Dark mode"
|
||||
>
|
||||
<input
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
class="rtl-mirror"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
width="512"
|
||||
>
|
||||
<path
|
||||
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when the UIOptions prop is "undefined" 1`] = `
|
||||
<section
|
||||
aria-labelledby="canvasActions-title"
|
||||
class="zen-mode-transition"
|
||||
>
|
||||
<h2
|
||||
class="visually-hidden"
|
||||
id="canvasActions-title"
|
||||
>
|
||||
Canvas actions
|
||||
</h2>
|
||||
<div
|
||||
class="Island"
|
||||
style="--padding: 2; z-index: 1;"
|
||||
>
|
||||
<div
|
||||
class="Stack Stack_vertical"
|
||||
style="--gap: 4;"
|
||||
>
|
||||
<div
|
||||
class="Stack Stack_horizontal"
|
||||
style="--gap: 1; justify-content: space-between;"
|
||||
>
|
||||
<button
|
||||
aria-label="Load"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="load-button"
|
||||
title="Load"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="save-button"
|
||||
title="Save"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save as"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
|
||||
data-testid="save-as-button"
|
||||
hidden=""
|
||||
title="Save as"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Export"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="export-button"
|
||||
title="Export"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex;"
|
||||
>
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="color-picker-control-container"
|
||||
>
|
||||
<button
|
||||
aria-label="Canvas background"
|
||||
class="color-picker-label-swatch"
|
||||
style="--swatch-color: #ffffff;"
|
||||
/>
|
||||
<label
|
||||
class="color-input-container"
|
||||
>
|
||||
<div
|
||||
class="color-picker-hash"
|
||||
>
|
||||
#
|
||||
</div>
|
||||
<input
|
||||
aria-label="Canvas background"
|
||||
class="color-picker-input"
|
||||
spellcheck="false"
|
||||
value="ffffff"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="margin-inline-start: 0.25rem;"
|
||||
>
|
||||
<label
|
||||
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
||||
data-testid="toggle-dark-mode"
|
||||
title="Dark mode"
|
||||
>
|
||||
<input
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
class="rtl-mirror"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
width="512"
|
||||
>
|
||||
<path
|
||||
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
@ -130,4 +130,86 @@ describe("<Excalidraw/>", () => {
|
||||
expect(textInput?.nodeName).toBe("SPAN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test UIOptions prop", () => {
|
||||
it('should not hide any UI element when the UIOptions prop is "undefined"', async () => {
|
||||
await render(<Excalidraw />);
|
||||
|
||||
const canvasActions = document.querySelector(
|
||||
'section[aria-labelledby="canvasActions-title"]',
|
||||
);
|
||||
|
||||
expect(canvasActions).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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"]',
|
||||
);
|
||||
|
||||
expect(canvasActions).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hide clear canvas button when clearCanvas is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "clear-canvas-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide export button when export is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "export-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide load button when loadScene is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { loadScene: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "load-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide save as button when saveAsScene is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { saveAsScene: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "save-as-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide save button when saveScene is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { saveScene: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "save-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide the canvas background picker when changeViewBackgroundColor is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw
|
||||
UIOptions={{ canvasActions: { changeViewBackgroundColor: false } }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide the theme toggle when theme is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { theme: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
21
src/types.ts
21
src/types.ts
@ -189,6 +189,7 @@ export interface ExcalidrawProps {
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => JSX.Element;
|
||||
UIOptions?: UIOptions;
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
@ -203,3 +204,23 @@ export enum UserIdleState {
|
||||
AWAY = "away",
|
||||
IDLE = "idle",
|
||||
}
|
||||
|
||||
type CanvasActions = {
|
||||
changeViewBackgroundColor?: boolean;
|
||||
clearCanvas?: boolean;
|
||||
export?: boolean;
|
||||
loadScene?: boolean;
|
||||
saveAsScene?: boolean;
|
||||
saveScene?: boolean;
|
||||
theme?: boolean;
|
||||
};
|
||||
|
||||
export type UIOptions = {
|
||||
canvasActions?: CanvasActions;
|
||||
};
|
||||
|
||||
export type AppProps = ExcalidrawProps & {
|
||||
UIOptions: {
|
||||
canvasActions: Required<CanvasActions>;
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user