From 7e135c4e22e373a7004586e6c7ea200e4aa2088b Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 21 Dec 2022 12:47:09 +0100 Subject: [PATCH] feat: move contextMenu into the component tree and control via appState (#6021) --- src/actions/actionClipboard.tsx | 30 + src/actions/actionToggleGridMode.tsx | 3 + src/actions/actionToggleLock.ts | 12 +- src/actions/actionToggleViewMode.tsx | 3 + src/actions/actionToggleZenMode.tsx | 3 + src/actions/types.ts | 2 + src/appState.ts | 2 + src/components/App.tsx | 379 ++--- src/components/ContextMenu.scss | 22 +- src/components/ContextMenu.tsx | 224 ++- .../__snapshots__/contextmenu.test.tsx.snap | 1406 ++++++++++++++++- .../regressionTests.test.tsx.snap | 53 + src/tests/elementLocking.test.tsx | 2 +- .../packages/__snapshots__/utils.test.ts.snap | 1 + src/types.ts | 8 + 15 files changed, 1752 insertions(+), 398 deletions(-) diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 62a89a38..5be391d3 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -3,6 +3,7 @@ import { register } from "./register"; import { copyTextToSystemClipboard, copyToClipboard, + probablySupportsClipboardBlob, probablySupportsClipboardWriteText, } from "../clipboard"; import { actionDeleteSelected } from "./actionDeleteSelected"; @@ -23,11 +24,31 @@ export const actionCopy = register({ commitToHistory: false, }; }, + contextItemPredicate: (elements, appState, appProps, app) => { + return app.device.isMobile && !!navigator.clipboard; + }, contextItemLabel: "labels.copy", // don't supply a shortcut since we handle this conditionally via onCopy event keyTest: undefined, }); +export const actionPaste = register({ + name: "paste", + trackEvent: { category: "element" }, + perform: (elements: any, appStates: any, data, app) => { + app.pasteFromClipboard(null); + return { + commitToHistory: false, + }; + }, + contextItemPredicate: (elements, appState, appProps, app) => { + return app.device.isMobile && !!navigator.clipboard; + }, + contextItemLabel: "labels.paste", + // don't supply a shortcut since we handle this conditionally via onCopy event + keyTest: undefined, +}); + export const actionCut = register({ name: "cut", trackEvent: { category: "element" }, @@ -35,6 +56,9 @@ export const actionCut = register({ actionCopy.perform(elements, appState, data, app); return actionDeleteSelected.perform(elements, appState); }, + contextItemPredicate: (elements, appState, appProps, app) => { + return app.device.isMobile && !!navigator.clipboard; + }, contextItemLabel: "labels.cut", keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, }); @@ -77,6 +101,9 @@ export const actionCopyAsSvg = register({ }; } }, + contextItemPredicate: (elements) => { + return probablySupportsClipboardWriteText && elements.length > 0; + }, contextItemLabel: "labels.copyAsSvg", }); @@ -131,6 +158,9 @@ export const actionCopyAsPng = register({ }; } }, + contextItemPredicate: (elements) => { + return probablySupportsClipboardBlob && elements.length > 0; + }, contextItemLabel: "labels.copyAsPng", keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey, }); diff --git a/src/actions/actionToggleGridMode.tsx b/src/actions/actionToggleGridMode.tsx index c3617398..f8336a4b 100644 --- a/src/actions/actionToggleGridMode.tsx +++ b/src/actions/actionToggleGridMode.tsx @@ -20,6 +20,9 @@ export const actionToggleGridMode = register({ }; }, checked: (appState: AppState) => appState.gridSize !== null, + contextItemPredicate: (element, appState, props) => { + return typeof props.gridModeEnabled === "undefined"; + }, contextItemLabel: "labels.showGrid", keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE, }); diff --git a/src/actions/actionToggleLock.ts b/src/actions/actionToggleLock.ts index c944c37c..c44bd570 100644 --- a/src/actions/actionToggleLock.ts +++ b/src/actions/actionToggleLock.ts @@ -41,15 +41,9 @@ export const actionToggleLock = register({ : "labels.elementLock.lock"; } - if (selected.length > 1) { - return getOperation(selected) === "lock" - ? "labels.elementLock.lockAll" - : "labels.elementLock.unlockAll"; - } - - throw new Error( - "Unexpected zero elements to lock/unlock. This should never happen.", - ); + return getOperation(selected) === "lock" + ? "labels.elementLock.lockAll" + : "labels.elementLock.unlockAll"; }, keyTest: (event, appState, elements) => { return ( diff --git a/src/actions/actionToggleViewMode.tsx b/src/actions/actionToggleViewMode.tsx index 4b1adf47..b2f529c1 100644 --- a/src/actions/actionToggleViewMode.tsx +++ b/src/actions/actionToggleViewMode.tsx @@ -18,6 +18,9 @@ export const actionToggleViewMode = register({ }; }, checked: (appState) => appState.viewModeEnabled, + contextItemPredicate: (elements, appState, appProps) => { + return typeof appProps.viewModeEnabled === "undefined"; + }, contextItemLabel: "labels.viewMode", keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R, diff --git a/src/actions/actionToggleZenMode.tsx b/src/actions/actionToggleZenMode.tsx index 5ed191eb..7578c02e 100644 --- a/src/actions/actionToggleZenMode.tsx +++ b/src/actions/actionToggleZenMode.tsx @@ -18,6 +18,9 @@ export const actionToggleZenMode = register({ }; }, checked: (appState) => appState.zenModeEnabled, + contextItemPredicate: (elements, appState, appProps) => { + return typeof appProps.zenModeEnabled === "undefined"; + }, contextItemLabel: "buttons.zenMode", keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z, diff --git a/src/actions/types.ts b/src/actions/types.ts index 0ec27ec5..93e29cfc 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -143,6 +143,8 @@ export interface Action { contextItemPredicate?: ( elements: readonly ExcalidrawElement[], appState: AppState, + appProps: ExcalidrawProps, + app: AppClassProperties, ) => boolean; checked?: (appState: Readonly) => boolean; trackEvent: diff --git a/src/appState.ts b/src/appState.ts index 49792213..d1cbe92f 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -64,6 +64,7 @@ export const getDefaultAppState = (): Omit< lastPointerDownWith: "mouse", multiElement: null, name: `${t("labels.untitled")}-${getDateTime()}`, + contextMenu: null, openMenu: null, openPopup: null, openSidebar: null, @@ -157,6 +158,7 @@ const APP_STATE_STORAGE_CONF = (< name: { browser: true, export: false, server: false }, offsetLeft: { browser: false, export: false, server: false }, offsetTop: { browser: false, export: false, server: false }, + contextMenu: { browser: false, export: false, server: false }, openMenu: { browser: true, export: false, server: false }, openPopup: { browser: false, export: false, server: false }, openSidebar: { browser: true, export: false, server: false }, diff --git a/src/components/App.tsx b/src/components/App.tsx index da9fba57..daa33d0e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -42,11 +42,7 @@ import { actions } from "../actions/register"; import { ActionResult } from "../actions/types"; import { trackEvent } from "../analytics"; import { getDefaultAppState, isEraserActive } from "../appState"; -import { - parseClipboard, - probablySupportsClipboardBlob, - probablySupportsClipboardWriteText, -} from "../clipboard"; +import { parseClipboard } from "../clipboard"; import { APP_NAME, CURSOR_TYPE, @@ -227,7 +223,11 @@ import { updateActiveTool, getShortcutKey, } from "../utils"; -import ContextMenu, { ContextMenuOption } from "./ContextMenu"; +import { + ContextMenu, + ContextMenuItems, + CONTEXT_MENU_SEPARATOR, +} from "./ContextMenu"; import LayerUI from "./LayerUI"; import { Toast } from "./Toast"; import { actionToggleViewMode } from "../actions/actionToggleViewMode"; @@ -274,6 +274,7 @@ import { import { shouldShowBoundingBox } from "../element/transformHandles"; import { atom } from "jotai"; import { Fonts } from "../scene/Fonts"; +import { actionPaste } from "../actions/actionClipboard"; export const isMenuOpenAtom = atom(false); export const isDropdownOpenAtom = atom(false); @@ -383,7 +384,6 @@ class App extends React.Component { hitLinkElement?: NonDeletedExcalidrawElement; lastPointerDown: React.PointerEvent | null = null; lastPointerUp: React.PointerEvent | PointerEvent | null = null; - contextMenuOpen: boolean = false; lastScenePointer: { x: number; y: number } | null = null; constructor(props: AppProps) { @@ -602,6 +602,7 @@ class App extends React.Component {
{selectedElement.length === 1 && + !this.state.contextMenu && this.state.showHyperlinkPopup && ( { closable={this.state.toast.closable} /> )} + {this.state.contextMenu && ( + + )}
{this.renderCanvas()}
{" "} @@ -644,8 +653,6 @@ class App extends React.Component { private syncActionResult = withBatchedUpdates( (actionResult: ActionResult) => { - // Since context menu closes when action triggered so setting to false - this.contextMenuOpen = false; if (this.unmounted || actionResult === false) { return; } @@ -674,7 +681,7 @@ class App extends React.Component { this.addNewImagesToImageCache(); } - if (actionResult.appState || editingElement) { + if (actionResult.appState || editingElement || this.state.contextMenu) { if (actionResult.commitToHistory) { this.history.resumeRecording(); } @@ -700,12 +707,17 @@ class App extends React.Component { if (typeof this.props.name !== "undefined") { name = this.props.name; } + this.setState( (state) => { // using Object.assign instead of spread to fool TS 4.2.2+ into // regarding the resulting type as not containing undefined // (which the following expression will never contain) return Object.assign(actionResult.appState || {}, { + // NOTE this will prevent opening context menu using an action + // or programmatically from the host, so it will need to be + // rewritten later + contextMenu: null, editingElement: editingElement || actionResult.appState?.editingElement || null, viewModeEnabled, @@ -1462,7 +1474,7 @@ class App extends React.Component { } }; - private pasteFromClipboard = withBatchedUpdates( + public pasteFromClipboard = withBatchedUpdates( async (event: ClipboardEvent | null) => { const isPlainPaste = !!(IS_PLAIN_PASTE && event); @@ -1470,7 +1482,7 @@ class App extends React.Component { const target = document.activeElement; const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(target); - if (!isExcalidrawActive) { + if (event && !isExcalidrawActive) { return; } @@ -1744,10 +1756,11 @@ class App extends React.Component { this.history.resumeRecording(); } - // Collaboration - - setAppState: React.Component["setState"] = (state) => { - this.setState(state); + setAppState: React.Component["setState"] = ( + state, + callback, + ) => { + this.setState(state, callback); }; removePointer = (event: React.PointerEvent | PointerEvent) => { @@ -3101,7 +3114,7 @@ class App extends React.Component { hitElement && hitElement.link && this.state.selectedElementIds[hitElement.id] && - !this.contextMenuOpen && + !this.state.contextMenu && !this.state.showHyperlinkPopup ) { this.setState({ showHyperlinkPopup: "info" }); @@ -3323,6 +3336,14 @@ class App extends React.Component { private handleCanvasPointerDown = ( event: React.PointerEvent, ) => { + // since contextMenu options are potentially evaluated on each render, + // and an contextMenu action may depend on selection state, we must + // close the contextMenu before we update the selection on pointerDown + // (e.g. resetting selection) + if (this.state.contextMenu) { + this.setState({ contextMenu: null }); + } + // remove any active selection when we start to interact with canvas // (mainly, we care about removing selection outside the component which // would prevent our copy handling otherwise) @@ -3389,8 +3410,6 @@ class App extends React.Component { return; } - // Since context menu closes on pointer down so setting to false - this.contextMenuOpen = false; this.clearSelectionIfNotUsingSelection(); this.updateBindingEnabledOnPointerMove(event); @@ -5949,7 +5968,17 @@ class App extends React.Component { includeLockedElements: true, }); - const type = element ? "element" : "canvas"; + const selectedElements = getSelectedElements( + this.scene.getNonDeletedElements(), + this.state, + ); + const isHittignCommonBoundBox = + this.isHittingCommonBoundingBoxOfSelectedElements( + { x, y }, + selectedElements, + ); + + const type = element || isHittignCommonBoundBox ? "element" : "canvas"; const container = this.excalidrawContainerRef.current!; const { top: offsetTop, left: offsetLeft } = @@ -5957,25 +5986,30 @@ class App extends React.Component { const left = event.clientX - offsetLeft; const top = event.clientY - offsetTop; - if (element && !this.state.selectedElementIds[element.id]) { - this.setState( - selectGroupsForSelectedElements( - { - ...this.state, - selectedElementIds: { [element.id]: true }, - selectedLinearElement: isLinearElement(element) - ? new LinearElementEditor(element, this.scene) - : null, - }, - this.scene.getNonDeletedElements(), - ), - () => { - this._openContextMenu({ top, left }, type); - }, - ); - } else { - this._openContextMenu({ top, left }, type); - } + trackEvent("contextMenu", "openContextMenu", type); + + this.setState( + { + ...(element && !this.state.selectedElementIds[element.id] + ? selectGroupsForSelectedElements( + { + ...this.state, + selectedElementIds: { [element.id]: true }, + selectedLinearElement: isLinearElement(element) + ? new LinearElementEditor(element, this.scene) + : null, + }, + this.scene.getNonDeletedElements(), + ) + : this.state), + showHyperlinkPopup: false, + }, + () => { + this.setState({ + contextMenu: { top, left, items: this.getContextMenuItems(type) }, + }); + }, + ); }; private maybeDragNewGenericElement = ( @@ -6083,215 +6117,84 @@ class App extends React.Component { return false; }; - /** @private use this.handleCanvasContextMenu */ - private _openContextMenu = ( - { - left, - top, - }: { - left: number; - top: number; - }, + private getContextMenuItems = ( type: "canvas" | "element", - ) => { - trackEvent("contextMenu", "openContextMenu", type); - if (this.state.showHyperlinkPopup) { - this.setState({ showHyperlinkPopup: false }); - } - this.contextMenuOpen = true; - const maybeGroupAction = actionGroup.contextItemPredicate!( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); + ): ContextMenuItems => { + const options: ContextMenuItems = []; - const maybeUngroupAction = actionUngroup.contextItemPredicate!( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); + options.push(actionCopyAsPng, actionCopyAsSvg); - const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); + // canvas contextMenu + // ------------------------------------------------------------------------- - const maybeFlipVertical = actionFlipVertical.contextItemPredicate!( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); - - const mayBeAllowUnbinding = actionUnbindText.contextItemPredicate( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); - - const mayBeAllowBinding = actionBindText.contextItemPredicate( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); - - const mayBeAllowToggleLineEditing = - actionToggleLinearEditor.contextItemPredicate( - this.actionManager.getElementsIncludingDeleted(), - this.actionManager.getAppState(), - ); - - const separator = "separator"; - - const elements = this.scene.getNonDeletedElements(); - - const selectedElements = getSelectedElements( - this.scene.getNonDeletedElements(), - this.state, - ); - - const options: ContextMenuOption[] = []; - if (probablySupportsClipboardBlob && elements.length > 0) { - options.push(actionCopyAsPng); - } - - if (probablySupportsClipboardWriteText && elements.length > 0) { - options.push(actionCopyAsSvg); - } - - if ( - type === "element" && - copyText.contextItemPredicate(elements, this.state) && - probablySupportsClipboardWriteText - ) { - options.push(copyText); - } if (type === "canvas") { - const viewModeOptions = [ - ...options, - typeof this.props.gridModeEnabled === "undefined" && + if (this.state.viewModeEnabled) { + return [ + ...options, actionToggleGridMode, - typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode, - typeof this.props.viewModeEnabled === "undefined" && + actionToggleZenMode, actionToggleViewMode, + actionToggleStats, + ]; + } + + return [ + actionPaste, + CONTEXT_MENU_SEPARATOR, + actionCopyAsPng, + actionCopyAsSvg, + copyText, + CONTEXT_MENU_SEPARATOR, + actionSelectAll, + CONTEXT_MENU_SEPARATOR, + actionToggleGridMode, + actionToggleZenMode, + actionToggleViewMode, actionToggleStats, ]; - - if (this.state.viewModeEnabled) { - ContextMenu.push({ - options: viewModeOptions, - top, - left, - actionManager: this.actionManager, - appState: this.state, - container: this.excalidrawContainerRef.current!, - elements, - }); - } else { - ContextMenu.push({ - options: [ - this.device.isMobile && - navigator.clipboard && { - trackEvent: false, - name: "paste", - perform: (elements, appStates) => { - this.pasteFromClipboard(null); - return { - commitToHistory: false, - }; - }, - contextItemLabel: "labels.paste", - }, - this.device.isMobile && navigator.clipboard && separator, - probablySupportsClipboardBlob && - elements.length > 0 && - actionCopyAsPng, - probablySupportsClipboardWriteText && - elements.length > 0 && - actionCopyAsSvg, - probablySupportsClipboardWriteText && - selectedElements.length > 0 && - copyText, - ((probablySupportsClipboardBlob && elements.length > 0) || - (probablySupportsClipboardWriteText && elements.length > 0)) && - separator, - actionSelectAll, - separator, - typeof this.props.gridModeEnabled === "undefined" && - actionToggleGridMode, - typeof this.props.zenModeEnabled === "undefined" && - actionToggleZenMode, - typeof this.props.viewModeEnabled === "undefined" && - actionToggleViewMode, - actionToggleStats, - ], - top, - left, - actionManager: this.actionManager, - appState: this.state, - container: this.excalidrawContainerRef.current!, - elements, - }); - } - } else if (type === "element") { - if (this.state.viewModeEnabled) { - ContextMenu.push({ - options: [navigator.clipboard && actionCopy, ...options], - top, - left, - actionManager: this.actionManager, - appState: this.state, - container: this.excalidrawContainerRef.current!, - elements, - }); - } else { - ContextMenu.push({ - options: [ - this.device.isMobile && actionCut, - this.device.isMobile && navigator.clipboard && actionCopy, - this.device.isMobile && - navigator.clipboard && { - name: "paste", - trackEvent: false, - perform: (elements, appStates) => { - this.pasteFromClipboard(null); - return { - commitToHistory: false, - }; - }, - contextItemLabel: "labels.paste", - }, - this.device.isMobile && separator, - ...options, - separator, - actionCopyStyles, - actionPasteStyles, - separator, - maybeGroupAction && actionGroup, - mayBeAllowUnbinding && actionUnbindText, - mayBeAllowBinding && actionBindText, - maybeUngroupAction && actionUngroup, - (maybeGroupAction || maybeUngroupAction) && separator, - actionAddToLibrary, - separator, - actionSendBackward, - actionBringForward, - actionSendToBack, - actionBringToFront, - separator, - maybeFlipHorizontal && actionFlipHorizontal, - maybeFlipVertical && actionFlipVertical, - (maybeFlipHorizontal || maybeFlipVertical) && separator, - mayBeAllowToggleLineEditing && actionToggleLinearEditor, - actionLink.contextItemPredicate(elements, this.state) && actionLink, - actionDuplicateSelection, - actionToggleLock, - separator, - actionDeleteSelected, - ], - top, - left, - actionManager: this.actionManager, - appState: this.state, - container: this.excalidrawContainerRef.current!, - elements, - }); - } } + + // element contextMenu + // ------------------------------------------------------------------------- + + options.push(copyText); + + if (this.state.viewModeEnabled) { + return [actionCopy, ...options]; + } + + return [ + actionCut, + actionCopy, + actionPaste, + CONTEXT_MENU_SEPARATOR, + ...options, + CONTEXT_MENU_SEPARATOR, + actionCopyStyles, + actionPasteStyles, + CONTEXT_MENU_SEPARATOR, + actionGroup, + actionUnbindText, + actionBindText, + actionUngroup, + CONTEXT_MENU_SEPARATOR, + actionAddToLibrary, + CONTEXT_MENU_SEPARATOR, + actionSendBackward, + actionBringForward, + actionSendToBack, + actionBringToFront, + CONTEXT_MENU_SEPARATOR, + actionFlipHorizontal, + actionFlipVertical, + CONTEXT_MENU_SEPARATOR, + actionToggleLinearEditor, + actionLink, + actionDuplicateSelection, + actionToggleLock, + CONTEXT_MENU_SEPARATOR, + actionDeleteSelected, + ]; }; private handleWheel = withBatchedUpdates((event: WheelEvent) => { diff --git a/src/components/ContextMenu.scss b/src/components/ContextMenu.scss index df8db7fb..57976311 100644 --- a/src/components/ContextMenu.scss +++ b/src/components/ContextMenu.scss @@ -19,7 +19,7 @@ color: var(--popup-text-color); } - .context-menu-option { + .context-menu-item { position: relative; width: 100%; min-width: 9.5rem; @@ -43,16 +43,16 @@ } &.dangerous { - .context-menu-option__label { + .context-menu-item__label { color: $oc-red-7; } } - .context-menu-option__label { + .context-menu-item__label { justify-self: start; margin-inline-end: 20px; } - .context-menu-option__shortcut { + .context-menu-item__shortcut { justify-self: end; opacity: 0.6; font-family: inherit; @@ -60,37 +60,37 @@ } } - .context-menu-option:hover { + .context-menu-item:hover { color: var(--popup-bg-color); background-color: var(--select-highlight-color); &.dangerous { - .context-menu-option__label { + .context-menu-item__label { color: var(--popup-bg-color); } background-color: $oc-red-6; } } - .context-menu-option:focus { + .context-menu-item:focus { z-index: 1; } @include isMobile { - .context-menu-option { + .context-menu-item { display: block; - .context-menu-option__label { + .context-menu-item__label { margin-inline-end: 0; } - .context-menu-option__shortcut { + .context-menu-item__shortcut { display: none; } } } - .context-menu-option-separator { + .context-menu-item-separator { border: none; border-top: 1px solid $oc-gray-5; } diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 4b4a5f2f..2ec72e5e 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1,4 +1,3 @@ -import { createRoot, Root } from "react-dom/client"; import clsx from "clsx"; import { Popover } from "./Popover"; import { t } from "../i18n"; @@ -10,135 +9,116 @@ import { } from "../actions/shortcuts"; import { Action } from "../actions/types"; import { ActionManager } from "../actions/manager"; -import { AppState } from "../types"; -import { NonDeletedExcalidrawElement } from "../element/types"; +import { + useExcalidrawAppState, + useExcalidrawElements, + useExcalidrawSetAppState, +} from "./App"; +import React from "react"; -export type ContextMenuOption = "separator" | Action; +export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action; + +export type ContextMenuItems = (ContextMenuItem | false | null | undefined)[]; type ContextMenuProps = { - options: ContextMenuOption[]; - onCloseRequest?(): void; + actionManager: ActionManager; + items: ContextMenuItems; top: number; left: number; - actionManager: ActionManager; - appState: Readonly; - elements: readonly NonDeletedExcalidrawElement[]; }; -const ContextMenu = ({ - options, - onCloseRequest, - top, - left, - actionManager, - appState, - elements, -}: ContextMenuProps) => { - return ( - -
    event.preventDefault()} - > - {options.map((option, idx) => { - if (option === "separator") { - return
    ; - } +export const CONTEXT_MENU_SEPARATOR = "separator"; - const actionName = option.name; - let label = ""; - if (option.contextItemLabel) { - if (typeof option.contextItemLabel === "function") { - label = t(option.contextItemLabel(elements, appState)); - } else { - label = t(option.contextItemLabel); - } - } - return ( -
  • - -
  • - ); - })} -
-
- ); -}; +export const ContextMenu = React.memo( + ({ actionManager, items, top, left }: ContextMenuProps) => { + const appState = useExcalidrawAppState(); + const setAppState = useExcalidrawSetAppState(); + const elements = useExcalidrawElements(); -const contextMenuRoots = new WeakMap(); - -const getContextMenuRoot = (container: HTMLElement): Root => { - let contextMenuRoot = contextMenuRoots.get(container); - if (contextMenuRoot) { - return contextMenuRoot; - } - contextMenuRoot = createRoot( - container.querySelector(".excalidraw-contextMenuContainer")!, - ); - contextMenuRoots.set(container, contextMenuRoot); - return contextMenuRoot; -}; - -const handleClose = (container: HTMLElement) => { - const contextMenuRoot = contextMenuRoots.get(container); - if (contextMenuRoot) { - contextMenuRoot.unmount(); - contextMenuRoots.delete(container); - } -}; - -export default { - push(params: { - options: (ContextMenuOption | false | null | undefined)[]; - top: ContextMenuProps["top"]; - left: ContextMenuProps["left"]; - actionManager: ContextMenuProps["actionManager"]; - appState: Readonly; - container: HTMLElement; - elements: readonly NonDeletedExcalidrawElement[]; - }) { - const options = Array.of(); - params.options.forEach((option) => { - if (option) { - options.push(option); + const filteredItems = items.reduce((acc: ContextMenuItem[], item) => { + if ( + item && + (item === CONTEXT_MENU_SEPARATOR || + !item.contextItemPredicate || + item.contextItemPredicate( + elements, + appState, + actionManager.app.props, + actionManager.app, + )) + ) { + acc.push(item); } - }); - if (options.length) { - getContextMenuRoot(params.container).render( - handleClose(params.container)} - actionManager={params.actionManager} - appState={params.appState} - elements={params.elements} - />, - ); - } + return acc; + }, []); + + return ( + setAppState({ contextMenu: null })} + top={top} + left={left} + fitInViewport={true} + offsetLeft={appState.offsetLeft} + offsetTop={appState.offsetTop} + viewportWidth={appState.width} + viewportHeight={appState.height} + > +
    event.preventDefault()} + > + {filteredItems.map((item, idx) => { + if (item === CONTEXT_MENU_SEPARATOR) { + if ( + !filteredItems[idx - 1] || + filteredItems[idx - 1] === CONTEXT_MENU_SEPARATOR + ) { + return null; + } + return
    ; + } + + const actionName = item.name; + let label = ""; + if (item.contextItemLabel) { + if (typeof item.contextItemLabel === "function") { + label = t(item.contextItemLabel(elements, appState)); + } else { + label = t(item.contextItemLabel); + } + } + + return ( +
  • { + // we need update state before executing the action in case + // the action uses the appState it's being passed (that still + // contains a defined contextMenu) to return the next state. + setAppState({ contextMenu: null }, () => { + actionManager.executeAction(item, "contextMenu"); + }); + }} + > + +
  • + ); + })} +
+
+ ); }, -}; +); diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 1c3cccca..ed03cd99 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -9,6 +9,257 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.cut", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "cut", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copy", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "copy", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyStyles", + "keyTest": [Function], + "name": "copyStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.pasteStyles", + "keyTest": [Function], + "name": "pasteStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.group", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "group", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.unbindText", + "contextItemPredicate": [Function], + "name": "unbindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.bindText", + "contextItemPredicate": [Function], + "name": "bindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.ungroup", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "ungroup", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendBackward", + "keyPriority": 40, + "keyTest": [Function], + "name": "sendBackward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringForward", + "keyPriority": 40, + "keyTest": [Function], + "name": "bringForward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendToBack", + "keyTest": [Function], + "name": "sendToBack", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringToFront", + "keyTest": [Function], + "name": "bringToFront", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.flipHorizontal", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.flipVertical", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipVertical", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "hyperlink", + "perform": [Function], + "trackEvent": Object { + "action": "click", + "category": "hyperlink", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.duplicateSelection", + "keyTest": [Function], + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": [Function], + "keyTest": [Function], + "name": "toggleLock", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.delete", + "keyTest": [Function], + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": Object { + "action": "delete", + "category": "element", + }, + }, + ], + "left": 30, + "top": 40, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -177,7 +428,7 @@ Object { exports[`contextMenu element right-clicking on a group should select whole group: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element right-clicking on a group should select whole group: [end of test] number of renders 1`] = `6`; +exports[`contextMenu element right-clicking on a group should select whole group: [end of test] number of renders 1`] = `7`; exports[`contextMenu element selecting 'Add to library' in context menu adds element to library: [end of test] appState 1`] = ` Object { @@ -188,6 +439,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -362,7 +614,7 @@ Object { exports[`contextMenu element selecting 'Add to library' in context menu adds element to library: [end of test] number of elements 1`] = `1`; -exports[`contextMenu element selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `11`; +exports[`contextMenu element selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `14`; exports[`contextMenu element selecting 'Bring forward' in context menu brings element forward: [end of test] appState 1`] = ` Object { @@ -373,6 +625,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -716,7 +969,7 @@ Object { exports[`contextMenu element selecting 'Bring forward' in context menu brings element forward: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `16`; +exports[`contextMenu element selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `18`; exports[`contextMenu element selecting 'Bring to front' in context menu brings element to front: [end of test] appState 1`] = ` Object { @@ -727,6 +980,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1070,7 +1324,7 @@ Object { exports[`contextMenu element selecting 'Bring to front' in context menu brings element to front: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `16`; +exports[`contextMenu element selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `18`; exports[`contextMenu element selecting 'Copy styles' in context menu copies styles: [end of test] appState 1`] = ` Object { @@ -1081,6 +1335,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1255,7 +1510,7 @@ Object { exports[`contextMenu element selecting 'Copy styles' in context menu copies styles: [end of test] number of elements 1`] = `1`; -exports[`contextMenu element selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `11`; +exports[`contextMenu element selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `14`; exports[`contextMenu element selecting 'Delete' in context menu deletes element: [end of test] appState 1`] = ` Object { @@ -1266,6 +1521,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1476,7 +1732,7 @@ Object { exports[`contextMenu element selecting 'Delete' in context menu deletes element: [end of test] number of elements 1`] = `1`; -exports[`contextMenu element selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `11`; +exports[`contextMenu element selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `14`; exports[`contextMenu element selecting 'Duplicate' in context menu duplicates element: [end of test] appState 1`] = ` Object { @@ -1487,6 +1743,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1760,7 +2017,7 @@ Object { exports[`contextMenu element selecting 'Duplicate' in context menu duplicates element: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `11`; +exports[`contextMenu element selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `14`; exports[`contextMenu element selecting 'Group selection' in context menu groups selected elements: [end of test] appState 1`] = ` Object { @@ -1771,6 +2028,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -2132,7 +2390,7 @@ Object { exports[`contextMenu element selecting 'Group selection' in context menu groups selected elements: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `17`; +exports[`contextMenu element selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `20`; exports[`contextMenu element selecting 'Paste styles' in context menu pastes styles: [end of test] appState 1`] = ` Object { @@ -2143,6 +2401,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "#e64980", "currentItemEndArrowhead": "arrow", @@ -2978,7 +3237,7 @@ Object { exports[`contextMenu element selecting 'Paste styles' in context menu pastes styles: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `28`; +exports[`contextMenu element selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `33`; exports[`contextMenu element selecting 'Send backward' in context menu sends element backward: [end of test] appState 1`] = ` Object { @@ -2989,6 +3248,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -3332,7 +3592,7 @@ Object { exports[`contextMenu element selecting 'Send backward' in context menu sends element backward: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `15`; +exports[`contextMenu element selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `18`; exports[`contextMenu element selecting 'Send to back' in context menu sends element to back: [end of test] appState 1`] = ` Object { @@ -3343,6 +3603,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -3686,7 +3947,7 @@ Object { exports[`contextMenu element selecting 'Send to back' in context menu sends element to back: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `15`; +exports[`contextMenu element selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `18`; exports[`contextMenu element selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] appState 1`] = ` Object { @@ -3697,6 +3958,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4124,7 +4386,7 @@ Object { exports[`contextMenu element selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `18`; +exports[`contextMenu element selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `21`; exports[`contextMenu element shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = ` Object { @@ -4135,6 +4397,257 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.cut", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "cut", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copy", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "copy", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyStyles", + "keyTest": [Function], + "name": "copyStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.pasteStyles", + "keyTest": [Function], + "name": "pasteStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.group", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "group", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.unbindText", + "contextItemPredicate": [Function], + "name": "unbindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.bindText", + "contextItemPredicate": [Function], + "name": "bindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.ungroup", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "ungroup", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendBackward", + "keyPriority": 40, + "keyTest": [Function], + "name": "sendBackward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringForward", + "keyPriority": 40, + "keyTest": [Function], + "name": "bringForward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendToBack", + "keyTest": [Function], + "name": "sendToBack", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringToFront", + "keyTest": [Function], + "name": "bringToFront", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.flipHorizontal", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.flipVertical", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipVertical", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "hyperlink", + "perform": [Function], + "trackEvent": Object { + "action": "click", + "category": "hyperlink", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.duplicateSelection", + "keyTest": [Function], + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": [Function], + "keyTest": [Function], + "name": "toggleLock", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.delete", + "keyTest": [Function], + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": Object { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -19, + "top": -9, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4414,7 +4927,7 @@ Object { exports[`contextMenu element shows 'Group selection' in context menu for multiple selected elements: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `18`; +exports[`contextMenu element shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `20`; exports[`contextMenu element shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = ` Object { @@ -4425,6 +4938,257 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.cut", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "cut", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copy", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "copy", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyStyles", + "keyTest": [Function], + "name": "copyStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.pasteStyles", + "keyTest": [Function], + "name": "pasteStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.group", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "group", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.unbindText", + "contextItemPredicate": [Function], + "name": "unbindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.bindText", + "contextItemPredicate": [Function], + "name": "bindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.ungroup", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "ungroup", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendBackward", + "keyPriority": 40, + "keyTest": [Function], + "name": "sendBackward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringForward", + "keyPriority": 40, + "keyTest": [Function], + "name": "bringForward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendToBack", + "keyTest": [Function], + "name": "sendToBack", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringToFront", + "keyTest": [Function], + "name": "bringToFront", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.flipHorizontal", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.flipVertical", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipVertical", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "hyperlink", + "perform": [Function], + "trackEvent": Object { + "action": "click", + "category": "hyperlink", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.duplicateSelection", + "keyTest": [Function], + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": [Function], + "keyTest": [Function], + "name": "toggleLock", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.delete", + "keyTest": [Function], + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": Object { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -19, + "top": -9, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4789,7 +5553,7 @@ Object { exports[`contextMenu element shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of elements 1`] = `2`; -exports[`contextMenu element shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `19`; +exports[`contextMenu element shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `21`; exports[`contextMenu element shows context menu for canvas: [end of test] appState 1`] = ` Object { @@ -4800,6 +5564,112 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.selectAll", + "keyTest": [Function], + "name": "selectAll", + "perform": [Function], + "trackEvent": Object { + "category": "canvas", + }, + }, + "separator", + Object { + "checked": [Function], + "contextItemLabel": "labels.showGrid", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "gridMode", + "perform": [Function], + "trackEvent": Object { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + Object { + "checked": [Function], + "contextItemLabel": "buttons.zenMode", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "zenMode", + "perform": [Function], + "trackEvent": Object { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + Object { + "checked": [Function], + "contextItemLabel": "labels.viewMode", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "viewMode", + "perform": [Function], + "trackEvent": Object { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + Object { + "checked": [Function], + "contextItemLabel": "stats.title", + "keyTest": [Function], + "name": "stats", + "perform": [Function], + "trackEvent": Object { + "category": "menu", + }, + "viewMode": true, + }, + ], + "left": -19, + "top": -9, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4897,7 +5767,7 @@ Object { exports[`contextMenu element shows context menu for canvas: [end of test] number of elements 1`] = `0`; -exports[`contextMenu element shows context menu for canvas: [end of test] number of renders 1`] = `4`; +exports[`contextMenu element shows context menu for canvas: [end of test] number of renders 1`] = `6`; exports[`contextMenu element shows context menu for element: [end of test] appState 1`] = ` Object { @@ -4908,6 +5778,257 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.cut", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "cut", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copy", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "copy", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyStyles", + "keyTest": [Function], + "name": "copyStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.pasteStyles", + "keyTest": [Function], + "name": "pasteStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.group", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "group", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.unbindText", + "contextItemPredicate": [Function], + "name": "unbindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.bindText", + "contextItemPredicate": [Function], + "name": "bindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.ungroup", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "ungroup", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendBackward", + "keyPriority": 40, + "keyTest": [Function], + "name": "sendBackward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringForward", + "keyPriority": 40, + "keyTest": [Function], + "name": "bringForward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendToBack", + "keyTest": [Function], + "name": "sendToBack", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringToFront", + "keyTest": [Function], + "name": "bringToFront", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.flipHorizontal", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.flipVertical", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipVertical", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "hyperlink", + "perform": [Function], + "trackEvent": Object { + "action": "click", + "category": "hyperlink", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.duplicateSelection", + "keyTest": [Function], + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": [Function], + "keyTest": [Function], + "name": "toggleLock", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.delete", + "keyTest": [Function], + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": Object { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -19, + "top": -9, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4994,6 +6115,257 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": Object { + "items": Array [ + Object { + "contextItemLabel": "labels.cut", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "cut", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copy", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "copy", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.paste", + "contextItemPredicate": [Function], + "keyTest": undefined, + "name": "paste", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyAsPng", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "copyAsPng", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyAsSvg", + "contextItemPredicate": [Function], + "name": "copyAsSvg", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.copyText", + "contextItemPredicate": [Function], + "name": "copyText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.copyStyles", + "keyTest": [Function], + "name": "copyStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.pasteStyles", + "keyTest": [Function], + "name": "pasteStyles", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.group", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "group", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.unbindText", + "contextItemPredicate": [Function], + "name": "unbindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.bindText", + "contextItemPredicate": [Function], + "name": "bindText", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.ungroup", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "ungroup", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendBackward", + "keyPriority": 40, + "keyTest": [Function], + "name": "sendBackward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringForward", + "keyPriority": 40, + "keyTest": [Function], + "name": "bringForward", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.sendToBack", + "keyTest": [Function], + "name": "sendToBack", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.bringToFront", + "keyTest": [Function], + "name": "bringToFront", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": "labels.flipHorizontal", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": "labels.flipVertical", + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "flipVertical", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": [Function], + "contextItemPredicate": [Function], + "keyTest": [Function], + "name": "hyperlink", + "perform": [Function], + "trackEvent": Object { + "action": "click", + "category": "hyperlink", + }, + }, + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.duplicateSelection", + "keyTest": [Function], + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + Object { + "contextItemLabel": [Function], + "keyTest": [Function], + "name": "toggleLock", + "perform": [Function], + "trackEvent": Object { + "category": "element", + }, + }, + "separator", + Object { + "PanelComponent": [Function], + "contextItemLabel": "labels.delete", + "keyTest": [Function], + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": Object { + "action": "delete", + "category": "element", + }, + }, + ], + "left": 80, + "top": 90, + }, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -5250,6 +6622,6 @@ exports[`contextMenu element shows context menu for element: [end of test] numbe exports[`contextMenu element shows context menu for element: [end of test] number of elements 2`] = `2`; -exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `10`; +exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `12`; -exports[`contextMenu element shows context menu for element: [end of test] number of renders 2`] = `7`; +exports[`contextMenu element shows context menu for element: [end of test] number of renders 2`] = `11`; diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index ca181ddb..f842f48c 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -9,6 +9,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -544,6 +545,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1085,6 +1087,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -1991,6 +1994,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -2220,6 +2224,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -2752,6 +2757,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -3040,6 +3046,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -3223,6 +3230,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -3738,6 +3746,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "#fa5252", "currentItemEndArrowhead": "arrow", @@ -4005,6 +4014,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4234,6 +4244,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4509,6 +4520,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -4796,6 +4808,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -5213,6 +5226,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -5553,6 +5567,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -5866,6 +5881,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -6103,6 +6119,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -6288,6 +6305,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -6815,6 +6833,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -7179,6 +7198,7 @@ Object { "type": "freedraw", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -9530,6 +9550,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -9948,6 +9969,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "#fa5252", "currentItemEndArrowhead": "arrow", @@ -10236,6 +10258,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -10483,6 +10506,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -10803,6 +10827,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -10986,6 +11011,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -11169,6 +11195,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -11352,6 +11379,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -11588,6 +11616,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -11824,6 +11853,7 @@ Object { "type": "freedraw", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -12051,6 +12081,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -12287,6 +12318,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -12470,6 +12502,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -12706,6 +12739,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -12889,6 +12923,7 @@ Object { "type": "freedraw", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -13116,6 +13151,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -13299,6 +13335,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -14137,6 +14174,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -14425,6 +14463,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -14535,6 +14574,7 @@ Object { "type": "rectangle", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -14643,6 +14683,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -14829,6 +14870,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -15196,6 +15238,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -15826,6 +15869,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "#fa5252", "currentItemEndArrowhead": "arrow", @@ -16051,6 +16095,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -17013,6 +17058,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -17121,6 +17167,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -17979,6 +18026,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -18450,6 +18498,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -18790,6 +18839,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -18900,6 +18950,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -19470,6 +19521,7 @@ Object { "type": "text", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", @@ -19578,6 +19630,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", diff --git a/src/tests/elementLocking.test.tsx b/src/tests/elementLocking.test.tsx index 6b98aa1e..9cf0968d 100644 --- a/src/tests/elementLocking.test.tsx +++ b/src/tests/elementLocking.test.tsx @@ -152,7 +152,7 @@ describe("element locking", () => { expect(contextMenu).not.toBeNull(); expect( contextMenu?.querySelector( - `li[data-testid="toggleLock"] .context-menu-option__label`, + `li[data-testid="toggleLock"] .context-menu-item__label`, ), ).toHaveTextContent(t("labels.elementLock.unlock")); }); diff --git a/src/tests/packages/__snapshots__/utils.test.ts.snap b/src/tests/packages/__snapshots__/utils.test.ts.snap index da3d8439..a74da5dc 100644 --- a/src/tests/packages/__snapshots__/utils.test.ts.snap +++ b/src/tests/packages/__snapshots__/utils.test.ts.snap @@ -9,6 +9,7 @@ Object { "type": "selection", }, "collaborators": Map {}, + "contextMenu": null, "currentChartType": "bar", "currentItemBackgroundColor": "transparent", "currentItemEndArrowhead": "arrow", diff --git a/src/types.ts b/src/types.ts index e83a4226..218ca4de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,7 @@ import { MaybeTransformHandleType } from "./element/transformHandles"; import Library from "./data/library"; import type { FileSystemHandle } from "./data/filesystem"; import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; +import { ContextMenuItems } from "./components/ContextMenu"; export type Point = Readonly; @@ -92,6 +93,11 @@ export type LastActiveToolBeforeEraser = | null; export type AppState = { + contextMenu: { + items: ContextMenuItems; + top: number; + left: number; + } | null; showWelcomeScreen: boolean; isLoading: boolean; errorMessage: string | null; @@ -147,6 +153,7 @@ export type AppState = { isResizing: boolean; isRotating: boolean; zoom: Zoom; + // mobile-only openMenu: "canvas" | "shape" | null; openPopup: | "canvasColorPicker" @@ -407,6 +414,7 @@ export type AppClassProperties = { files: BinaryFiles; device: App["device"]; scene: App["scene"]; + pasteFromClipboard: App["pasteFromClipboard"]; }; export type PointerDownState = Readonly<{