Browse Source

fix: on contextMenu, use selected element regardless of z-index (#3668)

David Luzar 4 years ago
parent
commit
c819b653bf

+ 13 - 1
src/components/App.tsx

@@ -1858,9 +1858,21 @@ class App extends React.Component<AppProps, AppState> {
   private getElementAtPosition(
   private getElementAtPosition(
     x: number,
     x: number,
     y: number,
     y: number,
+    opts?: {
+      /** if true, returns the first selected element (with highest z-index)
+        of all hit elements */
+      preferSelected?: boolean;
+    },
   ): NonDeleted<ExcalidrawElement> | null {
   ): NonDeleted<ExcalidrawElement> | null {
     const allHitElements = this.getElementsAtPosition(x, y);
     const allHitElements = this.getElementsAtPosition(x, y);
     if (allHitElements.length > 1) {
     if (allHitElements.length > 1) {
+      if (opts?.preferSelected) {
+        for (let index = allHitElements.length - 1; index > -1; index--) {
+          if (this.state.selectedElementIds[allHitElements[index].id]) {
+            return allHitElements[index];
+          }
+        }
+      }
       const elementWithHighestZIndex =
       const elementWithHighestZIndex =
         allHitElements[allHitElements.length - 1];
         allHitElements[allHitElements.length - 1];
       // If we're hitting element with highest z-index only on its bounding box
       // If we're hitting element with highest z-index only on its bounding box
@@ -3935,7 +3947,7 @@ class App extends React.Component<AppProps, AppState> {
     event.preventDefault();
     event.preventDefault();
 
 
     const { x, y } = viewportCoordsToSceneCoords(event, this.state);
     const { x, y } = viewportCoordsToSceneCoords(event, this.state);
-    const element = this.getElementAtPosition(x, y);
+    const element = this.getElementAtPosition(x, y, { preferSelected: true });
 
 
     const type = element ? "element" : "canvas";
     const type = element ? "element" : "canvas";
 
 

+ 154 - 0
src/tests/__snapshots__/contextmenu.test.tsx.snap

@@ -4235,6 +4235,84 @@ Object {
 }
 }
 `;
 `;
 
 
+exports[`contextMenu element shows context menu for element: [end of test] appState 2`] = `
+Object {
+  "collaborators": Map {},
+  "currentChartType": "bar",
+  "currentItemBackgroundColor": "transparent",
+  "currentItemEndArrowhead": "arrow",
+  "currentItemFillStyle": "hachure",
+  "currentItemFontFamily": 1,
+  "currentItemFontSize": 20,
+  "currentItemLinearStrokeSharpness": "round",
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStartArrowhead": null,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeSharpness": "sharp",
+  "currentItemStrokeStyle": "solid",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "up",
+  "draggingElement": null,
+  "editingElement": null,
+  "editingGroupId": null,
+  "editingLinearElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "exportEmbedScene": false,
+  "exportWithDarkMode": false,
+  "fileHandle": null,
+  "gridSize": null,
+  "height": 100,
+  "isBindingEnabled": true,
+  "isLibraryOpen": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "offsetLeft": 20,
+  "offsetTop": 10,
+  "openMenu": null,
+  "pasteDialog": Object {
+    "data": null,
+    "shown": false,
+  },
+  "previousSelectedElementIds": Object {},
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id1": true,
+  },
+  "selectedGroupIds": Object {},
+  "selectionElement": null,
+  "shouldCacheIgnoreZoom": false,
+  "showHelpDialog": false,
+  "showStats": false,
+  "startBoundElement": null,
+  "suggestedBindings": Array [],
+  "theme": "light",
+  "toastMessage": null,
+  "viewBackgroundColor": "#ffffff",
+  "viewModeEnabled": false,
+  "width": 200,
+  "zenModeEnabled": false,
+  "zoom": Object {
+    "translation": Object {
+      "x": 0,
+      "y": 0,
+    },
+    "value": 1,
+  },
+}
+`;
+
 exports[`contextMenu element shows context menu for element: [end of test] element 0 1`] = `
 exports[`contextMenu element shows context menu for element: [end of test] element 0 1`] = `
 Object {
 Object {
   "angle": 0,
   "angle": 0,
@@ -4261,6 +4339,58 @@ Object {
 }
 }
 `;
 `;
 
 
+exports[`contextMenu element shows context menu for element: [end of test] element 0 2`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "red",
+  "boundElementIds": null,
+  "fillStyle": "hachure",
+  "groupIds": Array [],
+  "height": 200,
+  "id": "id0",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 1,
+  "versionNonce": 0,
+  "width": 200,
+  "x": 0,
+  "y": 0,
+}
+`;
+
+exports[`contextMenu element shows context menu for element: [end of test] element 1 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "red",
+  "boundElementIds": null,
+  "fillStyle": "hachure",
+  "groupIds": Array [],
+  "height": 200,
+  "id": "id1",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 1278240551,
+  "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 1,
+  "versionNonce": 0,
+  "width": 200,
+  "x": 0,
+  "y": 0,
+}
+`;
+
 exports[`contextMenu element shows context menu for element: [end of test] history 1`] = `
 exports[`contextMenu element shows context menu for element: [end of test] history 1`] = `
 Object {
 Object {
   "recording": false,
   "recording": false,
@@ -4318,6 +4448,30 @@ Object {
 }
 }
 `;
 `;
 
 
+exports[`contextMenu element shows context menu for element: [end of test] history 2`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {},
+        "selectedGroupIds": Object {},
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [],
+    },
+  ],
+}
+`;
+
 exports[`contextMenu element shows context menu for element: [end of test] number of elements 1`] = `1`;
 exports[`contextMenu element shows context menu for element: [end of test] number of elements 1`] = `1`;
 
 
+exports[`contextMenu element shows context menu for element: [end of test] number of elements 2`] = `2`;
+
 exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `9`;
 exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `9`;
+
+exports[`contextMenu element shows context menu for element: [end of test] number of renders 2`] = `6`;

+ 40 - 0
src/tests/contextmenu.test.tsx

@@ -147,6 +147,46 @@ describe("contextMenu element", () => {
     });
     });
   });
   });
 
 
+  it("shows context menu for element", () => {
+    const rect1 = API.createElement({
+      type: "rectangle",
+      x: 0,
+      y: 0,
+      height: 200,
+      width: 200,
+      backgroundColor: "red",
+    });
+    const rect2 = API.createElement({
+      type: "rectangle",
+      x: 0,
+      y: 0,
+      height: 200,
+      width: 200,
+      backgroundColor: "red",
+    });
+    h.elements = [rect1, rect2];
+    API.setSelectedElements([rect1]);
+
+    // lower z-index
+    fireEvent.contextMenu(GlobalTestState.canvas, {
+      button: 2,
+      clientX: 100,
+      clientY: 100,
+    });
+    expect(queryContextMenu()).not.toBeNull();
+    expect(API.getSelectedElement().id).toBe(rect1.id);
+
+    // higher z-index
+    API.setSelectedElements([rect2]);
+    fireEvent.contextMenu(GlobalTestState.canvas, {
+      button: 2,
+      clientX: 100,
+      clientY: 100,
+    });
+    expect(queryContextMenu()).not.toBeNull();
+    expect(API.getSelectedElement().id).toBe(rect2.id);
+  });
+
   it("shows 'Group selection' in context menu for multiple selected elements", () => {
   it("shows 'Group selection' in context menu for multiple selected elements", () => {
     UI.clickTool("rectangle");
     UI.clickTool("rectangle");
     mouse.down(10, 10);
     mouse.down(10, 10);

+ 9 - 0
src/tests/helpers/api.ts

@@ -20,6 +20,15 @@ const readFile = util.promisify(fs.readFile);
 const { h } = window;
 const { h } = window;
 
 
 export class API {
 export class API {
+  static setSelectedElements = (elements: ExcalidrawElement[]) => {
+    h.setState({
+      selectedElementIds: elements.reduce((acc, element) => {
+        acc[element.id] = true;
+        return acc;
+      }, {} as Record<ExcalidrawElement["id"], true>),
+    });
+  };
+
   static getSelectedElements = (): ExcalidrawElement[] => {
   static getSelectedElements = (): ExcalidrawElement[] => {
     return h.elements.filter(
     return h.elements.filter(
       (element) => h.state.selectedElementIds[element.id],
       (element) => h.state.selectedElementIds[element.id],