diff --git a/src/components/App.tsx b/src/components/App.tsx index c5c0d1fc..0967d1dc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1858,9 +1858,21 @@ class App extends React.Component { private getElementAtPosition( x: number, y: number, + opts?: { + /** if true, returns the first selected element (with highest z-index) + of all hit elements */ + preferSelected?: boolean; + }, ): NonDeleted | null { const allHitElements = this.getElementsAtPosition(x, y); 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 = allHitElements[allHitElements.length - 1]; // If we're hitting element with highest z-index only on its bounding box @@ -3935,7 +3947,7 @@ class App extends React.Component { event.preventDefault(); 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"; diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 8cbbc0e4..e2f294f7 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/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`] = ` Object { "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`] = ` Object { "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 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 2`] = `6`; diff --git a/src/tests/contextmenu.test.tsx b/src/tests/contextmenu.test.tsx index 198d20a6..c2c0ef67 100644 --- a/src/tests/contextmenu.test.tsx +++ b/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", () => { UI.clickTool("rectangle"); mouse.down(10, 10); diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index ee2ac252..36849039 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -20,6 +20,15 @@ const readFile = util.promisify(fs.readFile); const { h } = window; export class API { + static setSelectedElements = (elements: ExcalidrawElement[]) => { + h.setState({ + selectedElementIds: elements.reduce((acc, element) => { + acc[element.id] = true; + return acc; + }, {} as Record), + }); + }; + static getSelectedElements = (): ExcalidrawElement[] => { return h.elements.filter( (element) => h.state.selectedElementIds[element.id],