Parcourir la source

select single element on cmd-click (#2087)

David Luzar il y a 4 ans
Parent
commit
4c2d34ffd7

+ 26 - 15
src/components/App.tsx

@@ -157,6 +157,7 @@ import {
   isElementInGroup,
   getSelectedGroupIdForElement,
   getElementsInGroup,
+  editGroupForSelectedElement,
 } from "../groups";
 import { Library } from "../data/library";
 import Scene from "../scene/Scene";
@@ -939,10 +940,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
   private onTapEnd = (event: TouchEvent) => {
     event.preventDefault();
     if (event.touches.length > 0) {
-      const { previousSelectedElementIds } = this.state;
       this.setState({
         previousSelectedElementIds: {},
-        selectedElementIds: previousSelectedElementIds,
+        selectedElementIds: this.state.previousSelectedElementIds,
       });
     }
   };
@@ -1617,10 +1617,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
 
   private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
     event.preventDefault();
-    const { previousSelectedElementIds } = this.state;
     this.setState({
       previousSelectedElementIds: {},
-      selectedElementIds: previousSelectedElementIds,
+      selectedElementIds: this.state.previousSelectedElementIds,
     });
     gesture.initialScale = null;
   });
@@ -1904,11 +1903,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
 
     resetCursor();
 
-    this.startTextEditing({
-      sceneX,
-      sceneY,
-      insertAtParentCenter: !event.altKey,
-    });
+    if (!event[KEYS.CTRL_OR_CMD]) {
+      this.startTextEditing({
+        sceneX,
+        sceneY,
+        insertAtParentCenter: !event.altKey,
+      });
+    }
   };
 
   private handleCanvasPointerMove = (
@@ -2485,7 +2486,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     }
   };
 
-  // Returns whether the pointer event has been completely handled
+  /**
+   * @returns whether the pointer event has been completely handled
+   */
   private handleSelectionOnPointerDown = (
     event: React.PointerEvent<HTMLCanvasElement>,
     pointerDownState: PointerDownState,
@@ -2587,6 +2590,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
 
         // If we click on something
         if (hitElement != null) {
+          // on CMD/CTRL, drill down to hit element regardless of groups etc.
+          if (event[KEYS.CTRL_OR_CMD]) {
+            this.setState((prevState) => ({
+              ...editGroupForSelectedElement(prevState, hitElement),
+              previousSelectedElementIds: this.state.selectedElementIds,
+            }));
+            // mark as not completely handled so as to allow dragging etc.
+            return false;
+          }
+
           // deselect if item is selected
           // if shift is not clicked, this will always return true
           // otherwise, it will trigger selection based on current
@@ -2619,7 +2632,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
                     ...prevState,
                     selectedElementIds: {
                       ...prevState.selectedElementIds,
-                      [hitElement!.id]: true,
+                      [hitElement.id]: true,
                     },
                   },
                   this.scene.getElements(),
@@ -2630,9 +2643,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           }
         }
 
-        const { selectedElementIds } = this.state;
         this.setState({
-          previousSelectedElementIds: selectedElementIds,
+          previousSelectedElementIds: this.state.selectedElementIds,
         });
       }
     }
@@ -3530,10 +3542,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           ? prevState.editingGroupId
           : null,
     }));
-    const { selectedElementIds } = this.state;
     this.setState({
       selectedElementIds: {},
-      previousSelectedElementIds: selectedElementIds,
+      previousSelectedElementIds: this.state.selectedElementIds,
     });
   }
 

+ 14 - 0
src/groups.ts

@@ -86,6 +86,20 @@ export function selectGroupsForSelectedElements(
   return nextAppState;
 }
 
+export const editGroupForSelectedElement = (
+  appState: AppState,
+  element: NonDeleted<ExcalidrawElement>,
+): AppState => {
+  return {
+    ...appState,
+    editingGroupId: element.groupIds.length ? element.groupIds[0] : null,
+    selectedGroupIds: {},
+    selectedElementIds: {
+      [element.id]: true,
+    },
+  };
+};
+
 export function isElementInGroup(element: ExcalidrawElement, groupId: string) {
   return element.groupIds.includes(groupId);
 }

+ 751 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -878,6 +878,757 @@ exports[`given element A and group of elements B and given both are selected whe
 
 exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `22`;
 
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = `
+Object {
+  "appearance": "light",
+  "collaborators": Map {},
+  "currentItemBackgroundColor": "transparent",
+  "currentItemFillStyle": "hachure",
+  "currentItemFontFamily": 1,
+  "currentItemFontSize": 20,
+  "currentItemLinearStrokeSharpness": "round",
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeSharpness": "sharp",
+  "currentItemStrokeStyle": "solid",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "up",
+  "cursorX": 0,
+  "cursorY": 0,
+  "draggingElement": null,
+  "editingElement": null,
+  "editingGroupId": "id10",
+  "editingLinearElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "gridSize": null,
+  "height": 768,
+  "isBindingEnabled": true,
+  "isCollaborating": false,
+  "isLibraryOpen": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "offsetLeft": 0,
+  "offsetTop": 0,
+  "openMenu": null,
+  "previousSelectedElementIds": Object {},
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id12": true,
+    "id7": true,
+  },
+  "selectedGroupIds": Object {},
+  "selectionElement": null,
+  "shouldAddWatermark": false,
+  "shouldCacheIgnoreZoom": false,
+  "showShortcutsDialog": false,
+  "startBoundElement": null,
+  "suggestedBindings": Array [],
+  "username": "",
+  "viewBackgroundColor": "#ffffff",
+  "width": 1024,
+  "zenModeEnabled": false,
+  "zoom": 1,
+}
+`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 0 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElementIds": null,
+  "fillStyle": "hachure",
+  "groupIds": Array [
+    "id4",
+    "id10",
+  ],
+  "height": 10,
+  "id": "id0",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 4,
+  "versionNonce": 493213705,
+  "width": 10,
+  "x": 0,
+  "y": 0,
+}
+`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 1 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElementIds": null,
+  "fillStyle": "hachure",
+  "groupIds": Array [
+    "id4",
+    "id10",
+  ],
+  "height": 10,
+  "id": "id1",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 449462985,
+  "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 4,
+  "versionNonce": 915032327,
+  "width": 10,
+  "x": 30,
+  "y": 30,
+}
+`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 2 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElementIds": null,
+  "fillStyle": "hachure",
+  "groupIds": Array [
+    "id10",
+  ],
+  "height": 10,
+  "id": "id7",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 400692809,
+  "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 3,
+  "versionNonce": 81784553,
+  "width": 10,
+  "x": 60,
+  "y": 60,
+}
+`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] history 1`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id1": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 453191,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+          "id1": true,
+          "id2": true,
+          "id3": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1150084233,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1116226695,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": "id4",
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+          "id5": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1150084233,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1116226695,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id7": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1150084233,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1116226695,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id7",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 400692809,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1604849351,
+          "width": 10,
+          "x": 60,
+          "y": 60,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+          "id1": true,
+          "id7": true,
+          "id8": true,
+          "id9": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 493213705,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 915032327,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id10",
+          ],
+          "height": 10,
+          "id": "id7",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 400692809,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 81784553,
+          "width": 10,
+          "x": 60,
+          "y": 60,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": "id4",
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+          "id11": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 493213705,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 915032327,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id10",
+          ],
+          "height": 10,
+          "id": "id7",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 400692809,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 81784553,
+          "width": 10,
+          "x": 60,
+          "y": 60,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": "id10",
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id12": true,
+          "id7": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 493213705,
+          "width": 10,
+          "x": 0,
+          "y": 0,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+            "id10",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 4,
+          "versionNonce": 915032327,
+          "width": 10,
+          "x": 30,
+          "y": 30,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "boundElementIds": null,
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id10",
+          ],
+          "height": 10,
+          "id": "id7",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 400692809,
+          "strokeColor": "#000000",
+          "strokeSharpness": "sharp",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 81784553,
+          "width": 10,
+          "x": 60,
+          "y": 60,
+        },
+      ],
+    },
+  ],
+}
+`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of elements 1`] = `3`;
+
+exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `41`;
+
 exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = `
 Object {
   "appearance": "light",

+ 49 - 2
src/tests/regressionTests.test.tsx

@@ -50,6 +50,20 @@ const group = (elements: ExcalidrawElement[]) => {
   });
 };
 
+const assertSelectedElements = (...elements: ExcalidrawElement[]) => {
+  expect(
+    getSelectedElements().map((element) => {
+      return element.id;
+    }),
+  ).toEqual(expect.arrayContaining(elements.map((element) => element.id)));
+};
+
+const clearSelection = () => {
+  // @ts-ignore
+  h.app.clearSelection(null);
+  expect(getSelectedElements().length).toBe(0);
+};
+
 let altKey = false;
 let shiftKey = false;
 let ctrlKey = false;
@@ -191,8 +205,7 @@ class Pointer {
     /** if multiple elements supplied, they're shift-selected */
     elements: ExcalidrawElement | ExcalidrawElement[],
   ) {
-    // @ts-ignore
-    h.app.clearSelection(null);
+    clearSelection();
     withModifierKeys({ shift: true }, () => {
       elements = Array.isArray(elements) ? elements : [elements];
       elements.forEach((element) => {
@@ -1649,6 +1662,40 @@ describe("regression tests", () => {
     expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev);
     expect(getSelectedElements()).toEqual(selectedElements_prev);
   });
+
+  it("Cmd/Ctrl-click exclusively select element under pointer", () => {
+    const rect1 = createElement("rectangle", { x: 0 });
+    const rect2 = createElement("rectangle", { x: 30 });
+
+    group([rect1, rect2]);
+    assertSelectedElements(rect1, rect2);
+
+    withModifierKeys({ ctrl: true }, () => {
+      mouse.clickOn(rect1);
+    });
+    assertSelectedElements(rect1);
+
+    clearSelection();
+    withModifierKeys({ ctrl: true }, () => {
+      mouse.clickOn(rect1);
+    });
+    assertSelectedElements(rect1);
+
+    const rect3 = createElement("rectangle", { x: 60 });
+    group([rect1, rect3]);
+    assertSelectedElements(rect1, rect2, rect3);
+
+    withModifierKeys({ ctrl: true }, () => {
+      mouse.clickOn(rect1);
+    });
+    assertSelectedElements(rect1);
+
+    clearSelection();
+    withModifierKeys({ ctrl: true }, () => {
+      mouse.clickOn(rect3);
+    });
+    assertSelectedElements(rect3);
+  });
 });
 
 it(