Przeglądaj źródła

Show shortcut context menu (#2501)

Co-authored-by: rene_mbp <harryloveslearning@googlemail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
Rene 4 lat temu
rodzic
commit
94fe1ff6e6

+ 3 - 0
src/actions/manager.tsx

@@ -10,6 +10,7 @@ import {
 import { ExcalidrawElement } from "../element/types";
 import { AppState } from "../types";
 import { t } from "../i18n";
+import { ShortcutName } from "./shortcuts";
 
 export class ActionManager implements ActionsManagerInterface {
   actions = {} as ActionsManagerInterface["actions"];
@@ -102,6 +103,8 @@ export class ActionManager implements ActionsManagerInterface {
           (b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
       )
       .map((action) => ({
+        // take last bit of the label  "labels.<shortcutName>"
+        shortcutName: action.contextItemLabel?.split(".").pop() as ShortcutName,
         label: action.contextItemLabel ? t(action.contextItemLabel) : "",
         action: () => {
           this.updater(

+ 63 - 0
src/actions/shortcuts.ts

@@ -0,0 +1,63 @@
+import { t } from "../i18n";
+import { isDarwin } from "../keys";
+import { getShortcutKey } from "../utils";
+
+export type ShortcutName =
+  | "cut"
+  | "copy"
+  | "paste"
+  | "copyStyles"
+  | "pasteStyles"
+  | "selectAll"
+  | "delete"
+  | "duplicateSelection"
+  | "sendBackward"
+  | "bringForward"
+  | "sendToBack"
+  | "bringToFront"
+  | "copyAsPng"
+  | "copyAsSvg"
+  | "group"
+  | "ungroup"
+  | "toggleGridMode"
+  | "toggleStats"
+  | "addToLibrary";
+
+const shortcutMap: Record<ShortcutName, string[]> = {
+  cut: [getShortcutKey("CtrlOrCmd+X")],
+  copy: [getShortcutKey("CtrlOrCmd+C")],
+  paste: [getShortcutKey("CtrlOrCmd+V")],
+  copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
+  pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
+  selectAll: [getShortcutKey("CtrlOrCmd+A")],
+  delete: [getShortcutKey("Del")],
+  duplicateSelection: [
+    getShortcutKey("CtrlOrCmd+D"),
+    getShortcutKey(`Alt+${t("shortcutsDialog.drag")}`),
+  ],
+  sendBackward: [getShortcutKey("CtrlOrCmd+[")],
+  bringForward: [getShortcutKey("CtrlOrCmd+]")],
+  sendToBack: [
+    isDarwin
+      ? getShortcutKey("CtrlOrCmd+Alt+[")
+      : getShortcutKey("CtrlOrCmd+Shift+["),
+  ],
+  bringToFront: [
+    isDarwin
+      ? getShortcutKey("CtrlOrCmd+Alt+]")
+      : getShortcutKey("CtrlOrCmd+Shift+]"),
+  ],
+  copyAsPng: [getShortcutKey("Shift+Alt+C")],
+  copyAsSvg: [],
+  group: [getShortcutKey("CtrlOrCmd+G")],
+  ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
+  toggleGridMode: [getShortcutKey("CtrlOrCmd+'")],
+  toggleStats: [],
+  addToLibrary: [],
+};
+
+export const getShortcutFromShortcutName = (name: ShortcutName) => {
+  const shortcuts = shortcutMap[name];
+  // if multiple shortcuts availiable, take the first one
+  return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
+};

+ 10 - 0
src/components/App.tsx

@@ -3592,16 +3592,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       ContextMenu.push({
         options: [
           navigator.clipboard && {
+            shortcutName: "paste",
             label: t("labels.paste"),
             action: () => this.pasteFromClipboard(null),
           },
           probablySupportsClipboardBlob &&
             elements.length > 0 && {
+              shortcutName: "copyAsPng",
               label: t("labels.copyAsPng"),
               action: this.copyToClipboardAsPng,
             },
           probablySupportsClipboardWriteText &&
             elements.length > 0 && {
+              shortcutName: "copyAsSvg",
               label: t("labels.copyAsSvg"),
               action: this.copyToClipboardAsSvg,
             },
@@ -3609,10 +3612,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
             CANVAS_ONLY_ACTIONS.includes(action.name),
           ),
           {
+            shortcutName: "toggleGridMode",
             label: t("labels.toggleGridMode"),
             action: this.toggleGridMode,
           },
           {
+            shortcutName: "toggleStats",
             label: t("labels.toggleStats"),
             action: this.toggleStats,
           },
@@ -3630,22 +3635,27 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     ContextMenu.push({
       options: [
         {
+          shortcutName: "cut",
           label: t("labels.cut"),
           action: this.cutAll,
         },
         navigator.clipboard && {
+          shortcutName: "copy",
           label: t("labels.copy"),
           action: this.copyAll,
         },
         navigator.clipboard && {
+          shortcutName: "paste",
           label: t("labels.paste"),
           action: () => this.pasteFromClipboard(null),
         },
         probablySupportsClipboardBlob && {
+          shortcutName: "copyAsPng",
           label: t("labels.copyAsPng"),
           action: this.copyToClipboardAsPng,
         },
         probablySupportsClipboardWriteText && {
+          shortcutName: "copyAsSvg",
           label: t("labels.copyAsSvg"),
           action: this.copyToClipboardAsSvg,
         },

+ 12 - 0
src/components/ContextMenu.scss

@@ -29,6 +29,18 @@
     background-color: transparent;
     border: none;
     white-space: nowrap;
+
+    display: grid;
+    grid-template-columns: 1fr 0.2fr;
+    div:nth-child(1) {
+      justify-self: start;
+      margin-inline-end: 20px;
+    }
+    div:nth-child(2) {
+      justify-self: end;
+      opacity: 0.6;
+      font-size: 0.7rem;
+    }
   }
 
   .context-menu-option:hover {

+ 13 - 3
src/components/ContextMenu.tsx

@@ -4,8 +4,13 @@ import clsx from "clsx";
 import { Popover } from "./Popover";
 
 import "./ContextMenu.scss";
+import {
+  getShortcutFromShortcutName,
+  ShortcutName,
+} from "../actions/shortcuts";
 
 type ContextMenuOption = {
+  shortcutName: ShortcutName;
   label: string;
   action(): void;
 };
@@ -38,10 +43,15 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
           className="context-menu"
           onContextMenu={(event) => event.preventDefault()}
         >
-          {options.map(({ action, label }, idx) => (
-            <li key={idx} onClick={onCloseRequest}>
+          {options.map(({ action, shortcutName, label }, idx) => (
+            <li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
               <button className="context-menu-option" onClick={action}>
-                {label}
+                <div>{label}</div>
+                <div>
+                  {shortcutName
+                    ? getShortcutFromShortcutName(shortcutName)
+                    : ""}
+                </div>
               </button>
             </li>
           ))}

+ 62 - 55
src/tests/regressionTests.test.tsx

@@ -2,8 +2,9 @@ import { queryByText } from "@testing-library/react";
 import React from "react";
 import ReactDOM from "react-dom";
 import { copiedStyles } from "../actions/actionStyles";
+import { ShortcutName } from "../actions/shortcuts";
 import { ExcalidrawElement } from "../element/types";
-import { setLanguage, t } from "../i18n";
+import { setLanguage } from "../i18n";
 import { CODES, KEYS } from "../keys";
 import Excalidraw from "../packages/excalidraw/index";
 import { reseed } from "../random";
@@ -632,16 +633,19 @@ describe("regression tests", () => {
       clientY: 1,
     });
     const contextMenu = document.querySelector(".context-menu");
-    const options = contextMenu?.querySelectorAll(".context-menu-option");
-    const expectedOptions = [
-      t("labels.selectAll"),
-      t("labels.toggleGridMode"),
-      t("labels.toggleStats"),
+    const expectedShortcutNames: ShortcutName[] = [
+      "selectAll",
+      "toggleGridMode",
+      "toggleStats",
     ];
 
     expect(contextMenu).not.toBeNull();
-    expect(options?.length).toBe(3);
-    expect(options?.item(0).textContent).toBe(expectedOptions[0]);
+    expect(contextMenu?.children.length).toBe(expectedShortcutNames.length);
+    expectedShortcutNames.forEach((shortcutName) => {
+      expect(
+        contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
+      ).not.toBeNull();
+    });
   });
 
   it("shows context menu for element", () => {
@@ -655,24 +659,25 @@ describe("regression tests", () => {
       clientY: 1,
     });
     const contextMenu = document.querySelector(".context-menu");
-    const options = contextMenu?.querySelectorAll(".context-menu-option");
-    const expectedOptions = [
-      "Cut",
-      "Copy styles",
-      "Paste styles",
-      "Delete",
-      "Add to library",
-      "Send backward",
-      "Bring forward",
-      "Send to back",
-      "Bring to front",
-      "Duplicate",
+    const expectedShortcutNames: ShortcutName[] = [
+      "cut",
+      "copyStyles",
+      "pasteStyles",
+      "delete",
+      "addToLibrary",
+      "sendBackward",
+      "bringForward",
+      "sendToBack",
+      "bringToFront",
+      "duplicateSelection",
     ];
 
     expect(contextMenu).not.toBeNull();
-    expect(contextMenu?.children.length).toBe(10);
-    options?.forEach((opt, i) => {
-      expect(opt.textContent).toBe(expectedOptions[i]);
+    expect(contextMenu?.children.length).toBe(expectedShortcutNames.length);
+    expectedShortcutNames.forEach((shortcutName) => {
+      expect(
+        contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
+      ).not.toBeNull();
     });
   });
 
@@ -698,25 +703,26 @@ describe("regression tests", () => {
     });
 
     const contextMenu = document.querySelector(".context-menu");
-    const options = contextMenu?.querySelectorAll(".context-menu-option");
-    const expectedOptions = [
-      "Cut",
-      "Copy styles",
-      "Paste styles",
-      "Delete",
-      "Group selection",
-      "Add to library",
-      "Send backward",
-      "Bring forward",
-      "Send to back",
-      "Bring to front",
-      "Duplicate",
+    const expectedShortcutNames: ShortcutName[] = [
+      "cut",
+      "copyStyles",
+      "pasteStyles",
+      "delete",
+      "group",
+      "addToLibrary",
+      "sendBackward",
+      "bringForward",
+      "sendToBack",
+      "bringToFront",
+      "duplicateSelection",
     ];
 
     expect(contextMenu).not.toBeNull();
-    expect(contextMenu?.children.length).toBe(11);
-    options?.forEach((opt, i) => {
-      expect(opt.textContent).toBe(expectedOptions[i]);
+    expect(contextMenu?.children.length).toBe(expectedShortcutNames.length);
+    expectedShortcutNames.forEach((shortcutName) => {
+      expect(
+        contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
+      ).not.toBeNull();
     });
   });
 
@@ -746,25 +752,26 @@ describe("regression tests", () => {
     });
 
     const contextMenu = document.querySelector(".context-menu");
-    const options = contextMenu?.querySelectorAll(".context-menu-option");
-    const expectedOptions = [
-      "Cut",
-      "Copy styles",
-      "Paste styles",
-      "Delete",
-      "Ungroup selection",
-      "Add to library",
-      "Send backward",
-      "Bring forward",
-      "Send to back",
-      "Bring to front",
-      "Duplicate",
+    const expectedShortcutNames: ShortcutName[] = [
+      "cut",
+      "copyStyles",
+      "pasteStyles",
+      "delete",
+      "ungroup",
+      "addToLibrary",
+      "sendBackward",
+      "bringForward",
+      "sendToBack",
+      "bringToFront",
+      "duplicateSelection",
     ];
 
     expect(contextMenu).not.toBeNull();
-    expect(contextMenu?.children.length).toBe(11);
-    options?.forEach((opt, i) => {
-      expect(opt.textContent).toBe(expectedOptions[i]);
+    expect(contextMenu?.children.length).toBe(expectedShortcutNames.length);
+    expectedShortcutNames.forEach((shortcutName) => {
+      expect(
+        contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
+      ).not.toBeNull();
     });
   });