mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-08-03 18:27:04 -04:00
fix: improve line creation ux on touch screens (#9740)
* fix: awkward point adding and removing on touch device * feat: move finalize to next to last point * feat: on touch screen, click would create a default line/arrow * fix: make default adaptive to zoom * fix: increase padding to avoid cutoffs * refactor: simplify * fix: only use bigger padding when needed * center arrow horizontally on pointer * increase min drag distance before we start 2-point-arrow-drag-creating * do not render 0-width arrow while creating * dead code * fix tests * fix: remove redundant code * do not enter line editor on creation --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
8492b144b0
commit
e5e07260c6
@ -36,6 +36,7 @@ export const APP_NAME = "Excalidraw";
|
||||
// (happens a lot with fast clicks with the text tool)
|
||||
export const TEXT_AUTOWRAP_THRESHOLD = 36; // px
|
||||
export const DRAGGING_THRESHOLD = 10; // px
|
||||
export const MINIMUM_ARROW_SIZE = 20; // px
|
||||
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
||||
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
||||
export const ELEMENT_TRANSLATE_AMOUNT = 1;
|
||||
|
@ -106,6 +106,11 @@ const getCanvasPadding = (element: ExcalidrawElement) => {
|
||||
return element.strokeWidth * 12;
|
||||
case "text":
|
||||
return element.fontSize / 2;
|
||||
case "arrow":
|
||||
if (element.endArrowhead || element.endArrowhead) {
|
||||
return 40;
|
||||
}
|
||||
return 20;
|
||||
default:
|
||||
return 20;
|
||||
}
|
||||
|
@ -154,11 +154,7 @@ export const actionFinalize = register({
|
||||
|
||||
if (element) {
|
||||
// pen and mouse have hover
|
||||
if (
|
||||
appState.multiElement &&
|
||||
element.type !== "freedraw" &&
|
||||
appState.lastPointerDownWith !== "touch"
|
||||
) {
|
||||
if (appState.multiElement && element.type !== "freedraw") {
|
||||
const { points, lastCommittedPoint } = element;
|
||||
if (
|
||||
!lastCommittedPoint ||
|
||||
|
@ -505,15 +505,3 @@ export const ExitZenModeAction = ({
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
);
|
||||
|
||||
export const FinalizeAction = ({
|
||||
renderAction,
|
||||
className,
|
||||
}: {
|
||||
renderAction: ActionManager["renderAction"];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={`finalize-button ${className}`}>
|
||||
{renderAction("finalize", { size: "small" })}
|
||||
</div>
|
||||
);
|
||||
|
@ -100,6 +100,7 @@ import {
|
||||
randomInteger,
|
||||
CLASSES,
|
||||
Emitter,
|
||||
MINIMUM_ARROW_SIZE,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@ -8162,7 +8163,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointDistance(
|
||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||
) < DRAGGING_THRESHOLD
|
||||
) *
|
||||
this.state.zoom.value <
|
||||
MINIMUM_ARROW_SIZE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -9113,25 +9116,54 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
||||
const dragDistance =
|
||||
pointDistance(
|
||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||
) * this.state.zoom.value;
|
||||
|
||||
if (
|
||||
(!pointerDownState.drag.hasOccurred ||
|
||||
dragDistance < MINIMUM_ARROW_SIZE) &&
|
||||
newElement &&
|
||||
!multiElement
|
||||
) {
|
||||
if (this.device.isTouchScreen) {
|
||||
const FIXED_DELTA_X = Math.min(
|
||||
(this.state.width * 0.7) / this.state.zoom.value,
|
||||
100,
|
||||
);
|
||||
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: newElement.x - FIXED_DELTA_X / 2,
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(FIXED_DELTA_X, 0),
|
||||
],
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
} else {
|
||||
const dx = pointerCoords.x - newElement.x;
|
||||
const dy = pointerCoords.y - newElement.y;
|
||||
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...newElement.points, pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
this.setState({
|
||||
multiElement: newElement,
|
||||
newElement,
|
||||
});
|
||||
}
|
||||
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
||||
if (
|
||||
isBindingEnabled(this.state) &&
|
||||
|
@ -2,13 +2,7 @@ import clsx from "clsx";
|
||||
|
||||
import { actionShortcuts } from "../../actions";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
UndoRedoActions,
|
||||
ZoomActions,
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { ExitZenModeAction, UndoRedoActions, ZoomActions } from "../Actions";
|
||||
import { HelpButton } from "../HelpButton";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
@ -29,10 +23,6 @@ const Footer = ({
|
||||
}) => {
|
||||
const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
|
||||
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
@ -60,15 +50,6 @@ const Footer = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{showFinalize && (
|
||||
<FinalizeAction
|
||||
renderAction={actionManager.renderAction}
|
||||
className={clsx("zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { throttleRAF } from "@excalidraw/common";
|
||||
|
||||
import { renderElement } from "@excalidraw/element";
|
||||
import { isInvisiblySmallElement, renderElement } from "@excalidraw/element";
|
||||
|
||||
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
||||
|
||||
@ -34,6 +34,14 @@ const _renderNewElementScene = ({
|
||||
context.scale(appState.zoom.value, appState.zoom.value);
|
||||
|
||||
if (newElement && newElement.type !== "selection") {
|
||||
// e.g. when creating arrows and we're still below the arrow drag distance
|
||||
// threshold
|
||||
// (for now we skip render only with elements while we're creating to be
|
||||
// safe)
|
||||
if (isInvisiblySmallElement(newElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderElement(
|
||||
newElement,
|
||||
elementsMap,
|
||||
|
@ -8190,7 +8190,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -8203,7 +8203,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -8369,7 +8369,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -8382,7 +8382,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -8548,7 +8548,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -8561,7 +8561,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -8758,7 +8758,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
@ -8771,8 +8771,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -8786,7 +8786,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -8982,7 +8982,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
@ -8995,8 +8995,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"polygon": false,
|
||||
@ -9009,7 +9009,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -9167,12 +9167,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": [
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
@ -9183,12 +9183,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"pressures": [
|
||||
@ -9204,7 +9204,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
|
||||
"strokeWidth": 2,
|
||||
"type": "freedraw",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -9401,7 +9401,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
@ -9414,8 +9414,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -9429,7 +9429,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -9595,7 +9595,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -9608,7 +9608,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -9804,7 +9804,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
@ -9817,8 +9817,8 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"polygon": false,
|
||||
@ -9831,7 +9831,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -9997,7 +9997,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -10010,7 +10010,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -10168,12 +10168,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": [
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
@ -10184,12 +10184,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
|
||||
0,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
],
|
||||
],
|
||||
"pressures": [
|
||||
@ -10205,7 +10205,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
|
||||
"strokeWidth": 2,
|
||||
"type": "freedraw",
|
||||
"version": 4,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
@ -10371,7 +10371,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"height": 30,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@ -10384,7 +10384,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
|
@ -150,7 +150,7 @@ describe("regression tests", () => {
|
||||
expect(h.state.activeTool.type).toBe(shape);
|
||||
|
||||
mouse.down(10, 10);
|
||||
mouse.up(10, 10);
|
||||
mouse.up(30, 30);
|
||||
|
||||
if (shouldSelect) {
|
||||
expect(API.getSelectedElement().type).toBe(shape);
|
||||
|
Loading…
x
Reference in New Issue
Block a user