feat: Arrow binding is a preference (#10839)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács
2026-03-03 22:55:40 +01:00
committed by GitHub
parent 60b275880d
commit 437595fa65
24 changed files with 1164 additions and 146 deletions
+36 -19
View File
@@ -119,7 +119,6 @@ import {
fixBindingsAfterDeletion,
getHoveredElementForBinding,
isBindingEnabled,
shouldEnableBindingForPointerEvent,
updateBoundElements,
LinearElementEditor,
newElementWith,
@@ -319,6 +318,8 @@ import {
actionToggleElementLock,
actionToggleLinearEditor,
actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleCropEditor,
} from "../actions";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
@@ -2734,7 +2735,9 @@ class App extends React.Component<AppProps, AppState> {
private onBlur = withBatchedUpdates(() => {
isHoldingSpace = false;
this.setState({ isBindingEnabled: true });
this.setState({
isBindingEnabled: this.state.bindingPreference === "enabled",
});
});
private onUnload = () => {
@@ -4937,13 +4940,15 @@ class App extends React.Component<AppProps, AppState> {
return;
}
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
if (event[KEYS.CTRL_OR_CMD] && !event.repeat) {
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode();
}
flushSync(() => {
this.setState({ isBindingEnabled: false });
this.setState({
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
});
maybeHandleArrowPointlikeDrag({ app: this, event });
@@ -5217,10 +5222,13 @@ class App extends React.Component<AppProps, AppState> {
}
}
}
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
flushSync(() => {
this.setState({ isBindingEnabled: true });
});
if (!event[KEYS.CTRL_OR_CMD]) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
flushSync(() => {
this.setState({ isBindingEnabled: preferenceEnabled });
});
}
maybeHandleArrowPointlikeDrag({ app: this, event });
}
@@ -7138,6 +7146,14 @@ class App extends React.Component<AppProps, AppState> {
private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLElement>,
) => {
// If Ctrl is not held, ensure isBindingEnabled reflects the user preference.
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer;
this.lastPointerMoveCoords = {
@@ -7358,7 +7374,6 @@ class App extends React.Component<AppProps, AppState> {
}
this.clearSelectionIfNotUsingSelection();
this.updateBindingEnabledOnPointerMove(event);
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
return;
@@ -7581,6 +7596,13 @@ class App extends React.Component<AppProps, AppState> {
this.removePointer(event);
this.lastPointerUpEvent = event;
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state,
@@ -8636,7 +8658,9 @@ class App extends React.Component<AppProps, AppState> {
): void => {
if (event.ctrlKey) {
flushSync(() => {
this.setState({ isBindingEnabled: false });
this.setState({
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
});
}
@@ -11330,15 +11354,6 @@ class App extends React.Component<AppProps, AppState> {
this.addNewImagesToImageCache();
}, IMAGE_RENDER_TIMEOUT);
private updateBindingEnabledOnPointerMove = (
event: React.PointerEvent<HTMLElement>,
) => {
const shouldEnableBinding = shouldEnableBindingForPointerEvent(event);
if (this.state.isBindingEnabled !== shouldEnableBinding) {
this.setState({ isBindingEnabled: shouldEnableBinding });
}
};
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -12100,6 +12115,8 @@ class App extends React.Component<AppProps, AppState> {
CONTEXT_MENU_SEPARATOR,
actionToggleGridMode,
actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleZenMode,
actionToggleViewMode,
actionToggleStats,