diff --git a/src/components/App.tsx b/src/components/App.tsx index 0967d1dc9..6fc7f1e42 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1862,6 +1862,7 @@ class App extends React.Component { /** if true, returns the first selected element (with highest z-index) of all hit elements */ preferSelected?: boolean; + cycleElementsUnderCursor?: boolean; }, ): NonDeleted | null { const allHitElements = this.getElementsAtPosition(x, y); @@ -1872,6 +1873,13 @@ class App extends React.Component { return allHitElements[index]; } } + } else if (opts?.cycleElementsUnderCursor) { + const selectedIdx = allHitElements.findIndex( + (element) => this.state.selectedElementIds[element.id], + ); + return selectedIdx > 0 + ? allHitElements[selectedIdx - 1] + : allHitElements[allHitElements.length - 1]; } const elementWithHighestZIndex = allHitElements[allHitElements.length - 1]; @@ -2746,10 +2754,13 @@ class App extends React.Component { // hitElement may already be set above, so check first pointerDownState.hit.element = - pointerDownState.hit.element ?? + (!event[KEYS.CTRL_OR_CMD] ? pointerDownState.hit.element : null) ?? this.getElementAtPosition( pointerDownState.origin.x, pointerDownState.origin.y, + { + cycleElementsUnderCursor: event[KEYS.CTRL_OR_CMD], + }, ); // For overlapped elements one position may hit diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index ec640e68b..b82667418 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -144,6 +144,52 @@ describe("inner box-selection", () => { }); }); +describe("selecting with cmd/ctrl modifier", () => { + beforeEach(async () => { + await render(); + }); + it("cycling through elements under cursor", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 200, + height: 200, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 200, + height: 200, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect3 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 200, + height: 200, + backgroundColor: "red", + fillStyle: "solid", + }); + h.elements = [rect1, rect2, rect3]; + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.clickAt(100, 100); + assertSelectedElements(rect3); + mouse.clickAt(100, 100); + assertSelectedElements(rect2); + mouse.clickAt(100, 100); + assertSelectedElements(rect1); + mouse.clickAt(100, 100); + assertSelectedElements(rect3); + }); + }); +}); + describe("selection element", () => { it("create selection element on pointer down", async () => { const { getByToolName, container } = await render();