fix: prevent double-click to edit/create text scenarios on line (#9597)

* fix : double click on line enables line editor

* fix : prevent double-click to edit/create text
when inside line editor

* refactor: use lineCheck instead of arrowCheck in
doubleClick handler to align with updated logic

* fix: replace negative arrowCheck with lineCheck in
dbl click handler and fix double-click bind text
test in linearElementEditor tests

* clean up test

* simplify check

* add tests

* prevent text editing on dblclick when inside arrow editor

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
cheapster 2025-06-07 20:38:35 +05:30 committed by GitHub
parent ca1a4f25e7
commit 469caadb87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 104 additions and 41 deletions

View File

@ -129,6 +129,15 @@ export const isElbowArrow = (
return isArrowElement(element) && element.elbowed; return isArrowElement(element) && element.elbowed;
}; };
/**
* sharp or curved arrow, but not elbow
*/
export const isSimpleArrow = (
element?: ExcalidrawElement,
): element is ExcalidrawArrowElement => {
return isArrowElement(element) && !element.elbowed;
};
export const isSharpArrow = ( export const isSharpArrow = (
element?: ExcalidrawElement, element?: ExcalidrawElement,
): element is ExcalidrawArrowElement => { ): element is ExcalidrawArrowElement => {

View File

@ -1,6 +1,5 @@
import { pointCenter, pointFrom } from "@excalidraw/math"; import { pointCenter, pointFrom } from "@excalidraw/math";
import { act, queryByTestId, queryByText } from "@testing-library/react"; import { act, queryByTestId, queryByText } from "@testing-library/react";
import React from "react";
import { vi } from "vitest"; import { vi } from "vitest";
import { import {
@ -33,6 +32,8 @@ import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src";
import { LinearElementEditor } from "../src"; import { LinearElementEditor } from "../src";
import { newArrowElement } from "../src"; import { newArrowElement } from "../src";
import { getTextEditor } from "../../excalidraw/tests/queries/dom";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@ -252,7 +253,17 @@ describe("Test Linear Elements", () => {
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor when using double clicked with ctrl key", () => { it("should enter line editor on ctrl+dblclick (simple arrow)", () => {
createTwoPointerLinearElement("arrow");
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.doubleClick();
});
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
});
it("should enter line editor on ctrl+dblclick (line)", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.editingLinearElement?.elementId).toBeUndefined();
@ -262,6 +273,39 @@ describe("Test Linear Elements", () => {
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor on dblclick (line)", () => {
createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
mouse.doubleClick();
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
});
it("should not enter line editor on dblclick (arrow)", async () => {
createTwoPointerLinearElement("arrow");
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
mouse.doubleClick();
expect(h.state.editingLinearElement).toEqual(null);
await getTextEditor(".excalidraw-textEditorContainer > textarea");
});
it("shouldn't create text element on double click in line editor (arrow)", async () => {
createTwoPointerLinearElement("arrow");
const arrow = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(arrow);
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id);
mouse.doubleClick();
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id);
expect(h.elements.length).toEqual(1);
expect(
document.querySelector(".excalidraw-textEditorContainer > textarea"),
).toBe(null);
});
describe("Inside editor", () => { describe("Inside editor", () => {
it("should not drag line and add midpoint when dragged irrespective of threshold", () => { it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
@ -1063,13 +1107,7 @@ describe("Test Linear Elements", () => {
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);
mouse.doubleClickAt(line.x, line.y); mouse.doubleClickAt(line.x, line.y);
expect(h.elements.length).toBe(1);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBeNull();
expect(line.boundElements).toBeNull();
}); });
// TODO fix #7029 and rewrite this test // TODO fix #7029 and rewrite this test

View File

@ -230,6 +230,8 @@ import {
CaptureUpdateAction, CaptureUpdateAction,
type ElementUpdate, type ElementUpdate,
hitElementBoundingBox, hitElementBoundingBox,
isLineElement,
isSimpleArrow,
} from "@excalidraw/element"; } from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math";
@ -5438,17 +5440,17 @@ class App extends React.Component<AppProps, AppState> {
); );
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
const selectedLinearElement: ExcalidrawLinearElement =
selectedElements[0];
if ( if (
event[KEYS.CTRL_OR_CMD] && ((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) ||
(!this.state.editingLinearElement || isLineElement(selectedLinearElement)) &&
this.state.editingLinearElement.elementId !== this.state.editingLinearElement?.elementId !== selectedLinearElement.id
selectedElements[0].id) &&
!isElbowArrow(selectedElements[0])
) { ) {
this.store.scheduleCapture(); this.store.scheduleCapture();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor( editingLinearElement: new LinearElementEditor(
selectedElements[0], selectedLinearElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
}); });
@ -5515,6 +5517,13 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
} else if (
this.state.editingLinearElement &&
this.state.editingLinearElement.elementId ===
selectedLinearElement.id &&
isLineElement(selectedLinearElement)
) {
return;
} }
} }
@ -5563,7 +5572,13 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const container = this.getTextBindableContainerAtPosition(sceneX, sceneY); // shouldn't edit/create text when inside line editor (often false positive)
if (!this.state.editingLinearElement) {
const container = this.getTextBindableContainerAtPosition(
sceneX,
sceneY,
);
if (container) { if (container) {
if ( if (
@ -5594,6 +5609,7 @@ class App extends React.Component<AppProps, AppState> {
container, container,
}); });
} }
}
}; };
private getElementLinkAtPosition = ( private getElementLinkAtPosition = (