feat(editor): disable arrowhead toggle via dblclick (#11822)

This commit is contained in:
David Luzar
2026-08-04 16:25:00 +02:00
committed by GitHub
parent 39103bd33a
commit f1aa8f86a9
2 changed files with 21 additions and 115 deletions
@@ -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");
-71
View File
@@ -282,7 +282,6 @@ import type {
ExcalidrawEmbeddableElement,
Ordered,
MagicGenerationData,
Arrowhead,
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
SceneElementsMap,
@@ -708,9 +707,6 @@ class App extends React.Component<AppProps, AppState> {
/** 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<string, Arrowhead>();
/** previous frame pointer coords */
previousPointerMoveCoords: { x: number; y: number } | null = null;
@@ -6940,49 +6936,6 @@ class App extends React.Component<AppProps, AppState> {
);
};
/**
* 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<HTMLCanvasElement>,
@@ -7029,30 +6982,6 @@ class App extends React.Component<AppProps, AppState> {
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)) &&