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:
parent
9cfe7b45e5
commit
94fe1ff6e6
@ -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
src/actions/shortcuts.ts
Normal file
63
src/actions/shortcuts.ts
Normal 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] : "";
|
||||
};
|
@ -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,
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user