Show shortcut context menu (#2501)

Co-authored-by: rene_mbp <harryloveslearning@googlemail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Rene 2020-12-12 23:03:58 +01:00 committed by GitHub
parent 9cfe7b45e5
commit 94fe1ff6e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 58 deletions

View File

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

63
src/actions/shortcuts.ts Normal file
View File

@ -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] : "";
};

View File

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

View File

@ -29,6 +29,18 @@
background-color: transparent; background-color: transparent;
border: none; border: none;
white-space: nowrap; 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 { .context-menu-option:hover {

View File

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

View File

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