Переглянути джерело

fix: editing linear element (#9839)

Marcel Mraz 1 місяць тому
батько
коміт
9036812b6d

+ 39 - 81
packages/element/src/delta.ts

@@ -2,7 +2,6 @@ import {
   arrayToMap,
   arrayToObject,
   assertNever,
-  invariant,
   isDevEnv,
   isShallowEqual,
   isTestEnv,
@@ -561,70 +560,50 @@ export class AppStateDelta implements DeltaContainer<AppState> {
   ): [AppState, boolean] {
     try {
       const {
-        selectedElementIds: removedSelectedElementIds = {},
-        selectedGroupIds: removedSelectedGroupIds = {},
+        selectedElementIds: deletedSelectedElementIds = {},
+        selectedGroupIds: deletedSelectedGroupIds = {},
       } = this.delta.deleted;
 
       const {
-        selectedElementIds: addedSelectedElementIds = {},
-        selectedGroupIds: addedSelectedGroupIds = {},
-        selectedLinearElementId,
-        selectedLinearElementIsEditing,
+        selectedElementIds: insertedSelectedElementIds = {},
+        selectedGroupIds: insertedSelectedGroupIds = {},
+        selectedLinearElement: insertedSelectedLinearElement,
         ...directlyApplicablePartial
       } = this.delta.inserted;
 
       const mergedSelectedElementIds = Delta.mergeObjects(
         appState.selectedElementIds,
-        addedSelectedElementIds,
-        removedSelectedElementIds,
+        insertedSelectedElementIds,
+        deletedSelectedElementIds,
       );
 
       const mergedSelectedGroupIds = Delta.mergeObjects(
         appState.selectedGroupIds,
-        addedSelectedGroupIds,
-        removedSelectedGroupIds,
+        insertedSelectedGroupIds,
+        deletedSelectedGroupIds,
       );
 
-      let selectedLinearElement = appState.selectedLinearElement;
-
-      if (selectedLinearElementId === null) {
-        // Unselect linear element (visible change)
-        selectedLinearElement = null;
-      } else if (
-        selectedLinearElementId &&
-        nextElements.has(selectedLinearElementId)
-      ) {
-        selectedLinearElement = new LinearElementEditor(
-          nextElements.get(
-            selectedLinearElementId,
-          ) as NonDeleted<ExcalidrawLinearElement>,
-          nextElements,
-          selectedLinearElementIsEditing === true, // Can be unknown which is defaulted to false
-        );
-      }
-
-      if (
-        // Value being 'null' is equivaluent to unknown in this case because it only gets set
-        // to null when 'selectedLinearElementId' is set to null
-        selectedLinearElementIsEditing != null
-      ) {
-        invariant(
-          selectedLinearElement,
-          `selectedLinearElement is null when selectedLinearElementIsEditing is set to ${selectedLinearElementIsEditing}`,
-        );
-
-        selectedLinearElement = {
-          ...selectedLinearElement,
-          isEditing: selectedLinearElementIsEditing,
-        };
-      }
+      const selectedLinearElement =
+        insertedSelectedLinearElement &&
+        nextElements.has(insertedSelectedLinearElement.elementId)
+          ? new LinearElementEditor(
+              nextElements.get(
+                insertedSelectedLinearElement.elementId,
+              ) as NonDeleted<ExcalidrawLinearElement>,
+              nextElements,
+              insertedSelectedLinearElement.isEditing,
+            )
+          : null;
 
       const nextAppState = {
         ...appState,
         ...directlyApplicablePartial,
         selectedElementIds: mergedSelectedElementIds,
         selectedGroupIds: mergedSelectedGroupIds,
-        selectedLinearElement,
+        selectedLinearElement:
+          typeof insertedSelectedLinearElement !== "undefined"
+            ? selectedLinearElement
+            : appState.selectedLinearElement,
       };
 
       const constainsVisibleChanges = this.filterInvisibleChanges(
@@ -753,64 +732,53 @@ export class AppStateDelta implements DeltaContainer<AppState> {
             }
 
             break;
-          case "selectedLinearElementId": {
-            const appStateKey = AppStateDelta.convertToAppStateKey(key);
-            const linearElement = nextAppState[appStateKey];
+          case "selectedLinearElement":
+            const nextLinearElement = nextAppState[key];
 
-            if (!linearElement) {
+            if (!nextLinearElement) {
               // previously there was a linear element (assuming visible), now there is none
               visibleDifferenceFlag.value = true;
             } else {
-              const element = nextElements.get(linearElement.elementId);
+              const element = nextElements.get(nextLinearElement.elementId);
 
               if (element && !element.isDeleted) {
                 // previously there wasn't a linear element, now there is one which is visible
                 visibleDifferenceFlag.value = true;
               } else {
                 // there was assigned a linear element now, but it's deleted
-                nextAppState[appStateKey] = null;
+                nextAppState[key] = null;
               }
             }
 
             break;
-          }
-          case "selectedLinearElementIsEditing": {
-            // Changes in editing state are always visible
-            const prevIsEditing =
-              prevAppState.selectedLinearElement?.isEditing ?? false;
-            const nextIsEditing =
-              nextAppState.selectedLinearElement?.isEditing ?? false;
-
-            if (prevIsEditing !== nextIsEditing) {
-              visibleDifferenceFlag.value = true;
-            }
-            break;
-          }
-          case "lockedMultiSelections": {
+          case "lockedMultiSelections":
             const prevLockedUnits = prevAppState[key] || {};
             const nextLockedUnits = nextAppState[key] || {};
 
+            // TODO: this seems wrong, we are already doing this comparison generically above,
+            // hence instead we should check whether elements are actually visible,
+            // so that once these changes are applied they actually result in a visible change to the user
             if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
               visibleDifferenceFlag.value = true;
             }
             break;
-          }
-          case "activeLockedId": {
+          case "activeLockedId":
             const prevHitLockedId = prevAppState[key] || null;
             const nextHitLockedId = nextAppState[key] || null;
 
+            // TODO: this seems wrong, we are already doing this comparison generically above,
+            // hence instead we should check whether elements are actually visible,
+            // so that once these changes are applied they actually result in a visible change to the user
             if (prevHitLockedId !== nextHitLockedId) {
               visibleDifferenceFlag.value = true;
             }
             break;
-          }
-          default: {
+          default:
             assertNever(
               key,
               `Unknown ObservedElementsAppState's key "${key}"`,
               true,
             );
-          }
         }
       }
     }
@@ -818,15 +786,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
     return visibleDifferenceFlag.value;
   }
 
-  private static convertToAppStateKey(
-    key: keyof Pick<ObservedElementsAppState, "selectedLinearElementId">,
-  ): keyof Pick<AppState, "selectedLinearElement"> {
-    switch (key) {
-      case "selectedLinearElementId":
-        return "selectedLinearElement";
-    }
-  }
-
   private static filterSelectedElements(
     selectedElementIds: AppState["selectedElementIds"],
     elements: SceneElementsMap,
@@ -891,8 +850,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
       editingGroupId,
       selectedGroupIds,
       selectedElementIds,
-      selectedLinearElementId,
-      selectedLinearElementIsEditing,
+      selectedLinearElement,
       croppingElementId,
       lockedMultiSelections,
       activeLockedId,

+ 7 - 10
packages/element/src/store.ts

@@ -978,8 +978,7 @@ const getDefaultObservedAppState = (): ObservedAppState => {
     viewBackgroundColor: COLOR_PALETTE.white,
     selectedElementIds: {},
     selectedGroupIds: {},
-    selectedLinearElementId: null,
-    selectedLinearElementIsEditing: null,
+    selectedLinearElement: null,
     croppingElementId: null,
     activeLockedId: null,
     lockedMultiSelections: {},
@@ -998,14 +997,12 @@ export const getObservedAppState = (
     croppingElementId: appState.croppingElementId,
     activeLockedId: appState.activeLockedId,
     lockedMultiSelections: appState.lockedMultiSelections,
-    selectedLinearElementId:
-      (appState as AppState).selectedLinearElement?.elementId ??
-      (appState as ObservedAppState).selectedLinearElementId ??
-      null,
-    selectedLinearElementIsEditing:
-      (appState as AppState).selectedLinearElement?.isEditing ??
-      (appState as ObservedAppState).selectedLinearElementIsEditing ??
-      null,
+    selectedLinearElement: appState.selectedLinearElement
+      ? {
+          elementId: appState.selectedLinearElement.elementId,
+          isEditing: !!appState.selectedLinearElement.isEditing,
+        }
+      : null,
   };
 
   Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {

+ 10 - 11
packages/element/tests/delta.test.tsx

@@ -7,7 +7,10 @@ describe("AppStateDelta", () => {
   describe("ensure stable delta properties order", () => {
     it("should maintain stable order for root properties", () => {
       const name = "untitled scene";
-      const selectedLinearElementId = "id1" as LinearElementEditor["elementId"];
+      const selectedLinearElement = {
+        elementId: "id1" as LinearElementEditor["elementId"],
+        isEditing: false,
+      };
 
       const commonAppState = {
         viewBackgroundColor: "#ffffff",
@@ -24,23 +27,23 @@ describe("AppStateDelta", () => {
       const prevAppState1: ObservedAppState = {
         ...commonAppState,
         name: "",
-        selectedLinearElementId: null,
+        selectedLinearElement: null,
       };
 
       const nextAppState1: ObservedAppState = {
         ...commonAppState,
         name,
-        selectedLinearElementId,
+        selectedLinearElement,
       };
 
       const prevAppState2: ObservedAppState = {
-        selectedLinearElementId: null,
+        selectedLinearElement: null,
         name: "",
         ...commonAppState,
       };
 
       const nextAppState2: ObservedAppState = {
-        selectedLinearElementId,
+        selectedLinearElement,
         name,
         ...commonAppState,
       };
@@ -58,9 +61,7 @@ describe("AppStateDelta", () => {
         selectedGroupIds: {},
         editingGroupId: null,
         croppingElementId: null,
-        selectedLinearElementId: null,
-        selectedLinearElementIsEditing: null,
-        editingLinearElementId: null,
+        selectedLinearElement: null,
         activeLockedId: null,
         lockedMultiSelections: {},
       };
@@ -106,9 +107,7 @@ describe("AppStateDelta", () => {
         selectedElementIds: {},
         editingGroupId: null,
         croppingElementId: null,
-        selectedLinearElementId: null,
-        selectedLinearElementIsEditing: null,
-        editingLinearElementId: null,
+        selectedLinearElement: null,
         activeLockedId: null,
         lockedMultiSelections: {},
       };

+ 2 - 2
packages/excalidraw/data/reconcile.ts

@@ -20,7 +20,7 @@ export type ReconciledExcalidrawElement = OrderedExcalidrawElement &
 export type RemoteExcalidrawElement = OrderedExcalidrawElement &
   MakeBrand<"RemoteExcalidrawElement">;
 
-const shouldDiscardRemoteElement = (
+export const shouldDiscardRemoteElement = (
   localAppState: AppState,
   local: OrderedExcalidrawElement | undefined,
   remote: RemoteExcalidrawElement,
@@ -30,7 +30,7 @@ const shouldDiscardRemoteElement = (
     // local element is being edited
     (local.id === localAppState.editingTextElement?.id ||
       local.id === localAppState.resizingElement?.id ||
-      local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
+      local.id === localAppState.newElement?.id ||
       // local element is newer
       local.version > remote.version ||
       // resolve conflicting edits deterministically by taking the one with

+ 1 - 0
packages/excalidraw/data/restore.ts

@@ -291,6 +291,7 @@ const restoreElement = (
       // if empty text, mark as deleted. We keep in array
       // for data integrity purposes (collab etc.)
       if (!text && !element.isDeleted) {
+        // TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
         element = { ...element, originalText: text, isDeleted: true };
         element = bumpVersion(element);
       }

+ 108 - 125
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap

@@ -543,13 +543,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
           "selectedElementIds": {
             "id4": true,
           },
-          "selectedLinearElementId": "id4",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id4",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -1026,13 +1027,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
           "selectedElementIds": {
             "id4": true,
           },
-          "selectedLinearElementId": "id4",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id4",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -2414,13 +2416,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
           "selectedElementIds": {
             "id4": true,
           },
-          "selectedLinearElementId": "id4",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id4",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -7028,9 +7031,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
   "scrollX": 0,
   "scrollY": 0,
   "searchMatches": null,
-  "selectedElementIds": {
-    "id0": true,
-  },
+  "selectedElementIds": {},
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": {},
   "selectionElement": null,
@@ -7131,74 +7132,22 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
     },
     "elements": {
       "added": {},
-      "removed": {
-        "id0": {
-          "deleted": {
-            "angle": 0,
-            "backgroundColor": "transparent",
-            "boundElements": null,
-            "customData": undefined,
-            "elbowed": false,
-            "endArrowhead": "arrow",
-            "endBinding": null,
-            "fillStyle": "solid",
-            "frameId": null,
-            "groupIds": [],
-            "height": 10,
-            "index": "a0",
-            "isDeleted": false,
-            "lastCommittedPoint": [
-              10,
-              10,
-            ],
-            "link": null,
-            "locked": false,
-            "opacity": 100,
-            "points": [
-              [
-                0,
-                0,
-              ],
-              [
-                10,
-                10,
-              ],
-            ],
-            "roughness": 1,
-            "roundness": {
-              "type": 2,
-            },
-            "startArrowhead": null,
-            "startBinding": null,
-            "strokeColor": "#1e1e1e",
-            "strokeStyle": "solid",
-            "strokeWidth": 2,
-            "type": "arrow",
-            "version": 6,
-            "width": 10,
-            "x": 0,
-            "y": 0,
-          },
-          "inserted": {
-            "isDeleted": true,
-            "version": 5,
-          },
-        },
-      },
+      "removed": {},
       "updated": {},
     },
-    "id": "id2",
+    "id": "id13",
   },
   {
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -7207,16 +7156,22 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
       "removed": {},
       "updated": {},
     },
-    "id": "id4",
+    "id": "id14",
   },
   {
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementIsEditing": true,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": true,
+          },
         },
         "inserted": {
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -7225,16 +7180,22 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
       "removed": {},
       "updated": {},
     },
-    "id": "id6",
+    "id": "id15",
   },
   {
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementIsEditing": true,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": true,
+          },
         },
       },
     },
@@ -7243,7 +7204,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
       "removed": {},
       "updated": {},
     },
-    "id": "id10",
+    "id": "id16",
   },
 ]
 `;
@@ -10210,12 +10171,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -15770,13 +15732,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -16064,15 +16027,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -16688,15 +16652,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -17320,15 +17285,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -18017,15 +17983,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -18132,15 +18099,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -18744,15 +18712,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -18859,15 +18828,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
           "selectedElementIds": {
             "id13": true,
           },
-          "selectedLinearElementId": "id13",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id13",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -20656,12 +20626,13 @@ exports[`history > singleplayer undo/redo > should support linear element creati
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -20676,10 +20647,16 @@ exports[`history > singleplayer undo/redo > should support linear element creati
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementIsEditing": true,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": true,
+          },
         },
         "inserted": {
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -20747,10 +20724,16 @@ exports[`history > singleplayer undo/redo > should support linear element creati
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementIsEditing": true,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": true,
+          },
         },
       },
     },

+ 63 - 46
packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -6394,15 +6394,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
           "selectedElementIds": {
             "id9": true,
           },
-          "selectedLinearElementId": "id9",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id9",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id6": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -6470,13 +6471,19 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
           "selectedElementIds": {
             "id12": true,
           },
-          "selectedLinearElementId": "id12",
+          "selectedLinearElement": {
+            "elementId": "id12",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {
             "id9": true,
           },
-          "selectedLinearElementId": "id9",
+          "selectedLinearElement": {
+            "elementId": "id9",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -6542,15 +6549,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
           "selectedElementIds": {
             "id15": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
           "selectedElementIds": {
             "id12": true,
           },
-          "selectedLinearElementId": "id12",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id12",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -6677,12 +6685,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": "id15",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id15",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -6700,15 +6709,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
           "selectedElementIds": {
             "id22": true,
           },
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
           "selectedElementIds": {
             "id15": true,
           },
-          "selectedLinearElementId": "id15",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id15",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -6833,12 +6843,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": "id22",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id22",
+            "isEditing": false,
+          },
         },
         "inserted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -6873,12 +6884,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
-          "selectedLinearElementId": "id22",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id22",
+            "isEditing": false,
+          },
         },
       },
     },
@@ -8719,13 +8731,14 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -8946,13 +8959,14 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -9365,13 +9379,14 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -9770,13 +9785,14 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
           "selectedElementIds": {
             "id0": true,
           },
-          "selectedLinearElementId": "id0",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id0",
+            "isEditing": false,
+          },
         },
         "inserted": {
           "selectedElementIds": {},
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
       },
     },
@@ -14494,12 +14510,13 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
     "appState": AppStateDelta {
       "delta": Delta {
         "deleted": {
-          "selectedLinearElementId": null,
-          "selectedLinearElementIsEditing": null,
+          "selectedLinearElement": null,
         },
         "inserted": {
-          "selectedLinearElementId": "id6",
-          "selectedLinearElementIsEditing": false,
+          "selectedLinearElement": {
+            "elementId": "id6",
+            "isEditing": false,
+          },
         },
       },
     },

+ 4 - 5
packages/excalidraw/tests/history.test.tsx

@@ -3043,15 +3043,14 @@ describe("history", () => {
       });
 
       Keyboard.undo();
-      expect(API.getUndoStack().length).toBe(3);
-      expect(API.getRedoStack().length).toBe(1);
-      expect(h.state.selectedLinearElement).not.toBeNull();
-      expect(h.state.selectedLinearElement?.isEditing).toBe(true);
+      expect(API.getUndoStack().length).toBe(0);
+      expect(API.getRedoStack().length).toBe(4);
+      expect(h.state.selectedLinearElement).toBeNull();
 
       Keyboard.redo();
       expect(API.getUndoStack().length).toBe(4);
       expect(API.getRedoStack().length).toBe(0);
-      expect(h.state.selectedLinearElement).not.toBeNull();
+      expect(h.state.selectedLinearElement).toBeNull();
       expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
     });
 

+ 4 - 2
packages/excalidraw/types.ts

@@ -248,8 +248,10 @@ export type ObservedElementsAppState = {
   editingGroupId: AppState["editingGroupId"];
   selectedElementIds: AppState["selectedElementIds"];
   selectedGroupIds: AppState["selectedGroupIds"];
-  selectedLinearElementId: LinearElementEditor["elementId"] | null;
-  selectedLinearElementIsEditing: boolean | null;
+  selectedLinearElement: {
+    elementId: LinearElementEditor["elementId"];
+    isEditing: boolean;
+  } | null;
   croppingElementId: AppState["croppingElementId"];
   lockedMultiSelections: AppState["lockedMultiSelections"];
   activeLockedId: AppState["activeLockedId"];