From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Sun, 15 Mar 2026 16:19:35 -0400
Subject: refactor: packages/

---
 packages/excalidraw/components/ContextMenu.tsx | 128 +++++++++++++++++++++++++
 1 file changed, 128 insertions(+)
 create mode 100644 packages/excalidraw/components/ContextMenu.tsx

(limited to 'packages/excalidraw/components/ContextMenu.tsx')

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>
+    );
+  },
+);
-- 
cgit v1.2.3

