aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/ContextMenu.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/components/ContextMenu.tsx')
-rw-r--r--packages/excalidraw/components/ContextMenu.tsx128
1 files changed, 128 insertions, 0 deletions
diff --git a/packages/excalidraw/components/ContextMenu.tsx b/packages/excalidraw/components/ContextMenu.tsx
new file mode 100644
index 0000000..517e5fa
--- /dev/null
+++ b/packages/excalidraw/components/ContextMenu.tsx
@@ -0,0 +1,128 @@
+import clsx from "clsx";
+import { Popover } from "./Popover";
+import type { TranslationKeys } from "../i18n";
+import { t } from "../i18n";
+
+import "./ContextMenu.scss";
+import type { ShortcutName } from "../actions/shortcuts";
+import { getShortcutFromShortcutName } from "../actions/shortcuts";
+import type { Action } from "../actions/types";
+import type { ActionManager } from "../actions/manager";
+import { useExcalidrawAppState, useExcalidrawElements } from "./App";
+import React from "react";
+
+export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
+
+export type ContextMenuItems = (ContextMenuItem | false | null | undefined)[];
+
+type ContextMenuProps = {
+ actionManager: ActionManager;
+ items: ContextMenuItems;
+ top: number;
+ left: number;
+ onClose: (callback?: () => void) => void;
+};
+
+export const CONTEXT_MENU_SEPARATOR = "separator";
+
+export const ContextMenu = React.memo(
+ ({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
+ const appState = useExcalidrawAppState();
+ const elements = useExcalidrawElements();
+
+ const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
+ if (
+ item &&
+ (item === CONTEXT_MENU_SEPARATOR ||
+ !item.predicate ||
+ item.predicate(
+ elements,
+ appState,
+ actionManager.app.props,
+ actionManager.app,
+ ))
+ ) {
+ acc.push(item);
+ }
+ return acc;
+ }, []);
+
+ return (
+ <Popover
+ onCloseRequest={() => {
+ onClose();
+ }}
+ top={top}
+ left={left}
+ fitInViewport={true}
+ offsetLeft={appState.offsetLeft}
+ offsetTop={appState.offsetTop}
+ viewportWidth={appState.width}
+ viewportHeight={appState.height}
+ >
+ <ul
+ className="context-menu"
+ onContextMenu={(event) => event.preventDefault()}
+ >
+ {filteredItems.map((item, idx) => {
+ if (item === CONTEXT_MENU_SEPARATOR) {
+ if (
+ !filteredItems[idx - 1] ||
+ filteredItems[idx - 1] === CONTEXT_MENU_SEPARATOR
+ ) {
+ return null;
+ }
+ return <hr key={idx} className="context-menu-item-separator" />;
+ }
+
+ const actionName = item.name;
+ let label = "";
+ if (item.label) {
+ if (typeof item.label === "function") {
+ label = t(
+ item.label(
+ elements,
+ appState,
+ actionManager.app,
+ ) as unknown as TranslationKeys,
+ );
+ } else {
+ label = t(item.label as unknown as TranslationKeys);
+ }
+ }
+
+ return (
+ <li
+ key={idx}
+ data-testid={actionName}
+ onClick={() => {
+ // 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.
+ onClose(() => {
+ actionManager.executeAction(item, "contextMenu");
+ });
+ }}
+ >
+ <button
+ type="button"
+ className={clsx("context-menu-item", {
+ dangerous: actionName === "deleteSelectedElements",
+ checkmark: item.checked?.(appState),
+ })}
+ >
+ <div className="context-menu-item__label">{label}</div>
+ <kbd className="context-menu-item__shortcut">
+ {actionName
+ ? getShortcutFromShortcutName(actionName as ShortcutName)
+ : ""}
+ </kbd>
+ </button>
+ </li>
+ );
+ })}
+ </ul>
+ </Popover>
+ );
+ },
+);