From f1aa8f86a9745c6c1623ce8aabbc305522053616 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:25:00 +0200 Subject: [PATCH] feat(editor): disable arrowhead toggle via dblclick (#11822) --- .../tests/linearElementEditor.test.tsx | 65 ++++++----------- packages/excalidraw/components/App.tsx | 71 ------------------- 2 files changed, 21 insertions(+), 115 deletions(-) diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 254f1e9bd8..5abc3b5a5d 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -335,36 +335,14 @@ describe("Test Linear Elements", () => { await getTextEditor(); }); - describe("Arrowhead toggle on endpoint dblclick", () => { - // the toggle replaces the element (immutable update), so always re-read - // the arrow from the scene instead of holding on to a stale reference - const getArrow = () => h.elements[0] as ExcalidrawArrowElement; - - it("should toggle end arrowhead between default and none", () => { - createTwoPointerLinearElement("arrow"); - expect(getArrow().endArrowhead).toBe(null); - - mouse.doubleClickAt(p2[0], p2[1]); - expect(getArrow().endArrowhead).toBe("arrow"); - expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); - - mouse.doubleClickAt(p2[0], p2[1]); - expect(getArrow().endArrowhead).toBe(null); - }); - - it("should toggle start arrowhead between default and none", () => { - createTwoPointerLinearElement("arrow"); - expect(getArrow().startArrowhead).toBe(null); - - mouse.doubleClickAt(p1[0], p1[1]); - expect(getArrow().startArrowhead).toBe("arrow"); - expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); - - mouse.doubleClickAt(p1[0], p1[1]); - expect(getArrow().startArrowhead).toBe(null); - }); - - it("should restore the arrowhead removed by the previous toggle", () => { + it.each([ + ["start", null], + ["start", "triangle"], + ["end", null], + ["end", "triangle"], + ] as const)( + "should not toggle the %s arrowhead from %s on endpoint dblclick", + async (side, arrowhead) => { const arrow = API.createElement({ type: "arrow", x: p1[0], @@ -372,26 +350,25 @@ describe("Test Linear Elements", () => { width: p2[0] - p1[0], height: 0, points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])], - endArrowhead: "triangle", + startArrowhead: side === "start" ? arrowhead : null, + endArrowhead: side === "end" ? arrowhead : null, }); API.setElements([arrow]); mouse.clickAt(midpoint[0], midpoint[1]); - mouse.doubleClickAt(p2[0], p2[1]); - expect(getArrow().endArrowhead).toBe(null); + const endpoint = side === "start" ? p1 : p2; + mouse.doubleClickAt(endpoint[0], endpoint[1]); - mouse.doubleClickAt(p2[0], p2[1]); - expect(getArrow().endArrowhead).toBe("triangle"); - }); + const updatedArrow = h.elements[0] as ExcalidrawArrowElement; + expect( + side === "start" + ? updatedArrow.startArrowhead + : updatedArrow.endArrowhead, + ).toBe(arrowhead); - it("should not toggle arrowheads on dblclick outside endpoints", () => { - createTwoPointerLinearElement("arrow"); - - mouse.doubleClickAt(midpoint[0], midpoint[1]); - expect(getArrow().startArrowhead).toBe(null); - expect(getArrow().endArrowhead).toBe(null); - }); - }); + await getTextEditor(); + }, + ); it("shouldn't create text element on double click in line editor (arrow)", async () => { createTwoPointerLinearElement("arrow"); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 4a7a32139a..cb8320118b 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -282,7 +282,6 @@ import type { ExcalidrawEmbeddableElement, Ordered, MagicGenerationData, - Arrowhead, ExcalidrawArrowElement, ExcalidrawElbowArrowElement, SceneElementsMap, @@ -708,9 +707,6 @@ class App extends React.Component { /** current frame pointer cords */ lastPointerMoveCoords: { x: number; y: number } | null = null; private lastCompletedCanvasClicks: { x: number; y: number }[] = []; - /** arrowheads removed via endpoint double-click toggle, so a subsequent - * toggle can restore the original arrowhead (keyed by `elementId:side`) */ - private removedArrowheads = new Map(); /** previous frame pointer coords */ previousPointerMoveCoords: { x: number; y: number } | null = null; @@ -6940,49 +6936,6 @@ class App extends React.Component { ); }; - /** - * Toggles the arrowhead at the given endpoint between no arrowhead and the - * arrowhead it had before the last toggle (falling back to the current - * default arrowhead). - */ - private toggleArrowheadAtEndpoint = ( - element: ExcalidrawArrowElement, - side: "start" | "end", - ) => { - const currentArrowhead = - side === "start" ? element.startArrowhead : element.endArrowhead; - - this.store.scheduleCapture(); - - let arrowheadUpdate: - | { startArrowhead: Arrowhead | null } - | { endArrowhead: Arrowhead | null }; - - if (currentArrowhead) { - this.removedArrowheads.set(`${element.id}:${side}`, currentArrowhead); - arrowheadUpdate = - side === "start" ? { startArrowhead: null } : { endArrowhead: null }; - } else { - const arrowhead = - this.removedArrowheads.get(`${element.id}:${side}`) ?? - (side === "start" - ? this.state.currentItemStartArrowhead - : this.state.currentItemEndArrowhead) ?? - "arrow"; - arrowheadUpdate = - side === "start" - ? { startArrowhead: arrowhead } - : { endArrowhead: arrowhead }; - } - - this.scene.mapElements((_element) => { - if (_element.id === element.id && isArrowElement(_element)) { - return newElementWith(_element, arrowheadUpdate); - } - return _element; - }); - }; - private handleCanvasDoubleClick = ( event: Pick< React.MouseEvent, @@ -7029,30 +6982,6 @@ class App extends React.Component { const selectedLinearElement: ExcalidrawLinearElement = selectedElements[0]; - if ( - !event[KEYS.CTRL_OR_CMD] && - isArrowElement(selectedLinearElement) && - this.state.selectedLinearElement?.elementId === selectedLinearElement.id - ) { - const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( - selectedLinearElement, - this.scene.getNonDeletedElementsMap(), - this.state.zoom, - sceneX, - sceneY, - ); - if ( - clickedPointIndex === 0 || - clickedPointIndex === selectedLinearElement.points.length - 1 - ) { - this.toggleArrowheadAtEndpoint( - selectedLinearElement, - clickedPointIndex === 0 ? "start" : "end", - ); - return; - } - } - if ( ((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) || isLineElement(selectedLinearElement)) &&