diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 0381a2e39..659f598ba 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4300,6 +4300,27 @@ class App extends React.Component { }, 100); } + // remove selections on escape press + if ( + event.key === KEYS.ESCAPE && + // unless we're dragging or finalizing an action + !( + this.state.newElement || + isWritableElement(event.target) || + this.state.editingTextElement || + this.state.editingLinearElement || + ["freedraw", "eraser"].includes(this.state.activeTool.type) + ) + ) { + this.setState({ + selectedElementIds: {}, + selectedGroupIds: {}, + editingGroupId: null, + }); + event.preventDefault(); + return; + } + // prevent browser zoom in input fields if (event[KEYS.CTRL_OR_CMD] && isWritableElement(event.target)) { if (event.code === CODES.MINUS || event.code === CODES.EQUAL) { diff --git a/packages/excalidraw/tests/selection.test.tsx b/packages/excalidraw/tests/selection.test.tsx index 10f4f7ad9..ff5a6c009 100644 --- a/packages/excalidraw/tests/selection.test.tsx +++ b/packages/excalidraw/tests/selection.test.tsx @@ -107,6 +107,104 @@ describe("box-selection", () => { }); }); +describe("deselect when pressing escape", () => { + beforeEach(async () => { + await render(); + }); + + it("deselects elements", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 50, + height: 50, + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 100, + y: 0, + width: 50, + height: 50, + }); + + API.setElements([rect1, rect2]); + + mouse.clickAt(0, 0); + + assertSelectedElements([rect1.id]); + + Keyboard.keyDown(KEYS.ESCAPE); + + assertSelectedElements([]); + + mouse.downAt(-10, -10); + mouse.moveTo(160, 60); + mouse.up(); + + assertSelectedElements([rect1.id, rect2.id]); + + Keyboard.keyDown(KEYS.ESCAPE); + + assertSelectedElements([]); + }); + + it("deselects groups", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 50, + height: 50, + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 100, + y: 0, + width: 50, + height: 50, + }); + const rect3 = API.createElement({ + type: "rectangle", + x: 200, + y: 0, + width: 50, + height: 50, + }); + + API.setElements([rect1, rect2, rect3]); + + mouse.downAt(-10, -10); + mouse.moveTo(160, 60); + mouse.up(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyDown(KEYS.G); + }); + + expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); + assertSelectedElements([rect1.id, rect2.id]); + + Keyboard.keyDown(KEYS.ESCAPE); + + expect(Object.keys(h.state.selectedGroupIds).length).toBe(0); + assertSelectedElements([]); + + mouse.clickAt(100, 0); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.clickAt(200, 0); + }); + + expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); + assertSelectedElements([rect1.id, rect2.id, rect3.id]); + + Keyboard.keyDown(KEYS.ESCAPE); + + expect(Object.keys(h.state.selectedGroupIds).length).toBe(0); + assertSelectedElements([]); + }); +}); + describe("inner box-selection", () => { beforeEach(async () => { await render(); @@ -317,7 +415,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(8); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -349,7 +447,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(8); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -381,7 +479,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(8); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -425,8 +523,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderInteractiveScene).toHaveBeenCalledTimes(8); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -469,8 +567,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderInteractiveScene).toHaveBeenCalledTimes(8); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();