fix: prevent canvas text-selection highlight on shift+drag in Tauri (#12995)

* fix: prevent canvas text-selection highlight on shift+drag in Tauri WebView

* ensure macos fuctionality is identical to chromium

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
olayinkaadelakun
2026-05-13 14:34:25 -07:00
committed by GitHub
parent ea29bcc6b9
commit 191aaba05d
3 changed files with 145 additions and 0 deletions

View File

@ -0,0 +1,109 @@
import { act, renderHook } from "@testing-library/react";
import { createRef } from "react";
import { useShiftDragSelectFix } from "../useShiftDragSelectFix";
// Note: jsdom does not implement vendor-prefixed CSS properties such as
// -webkit-user-select, so only the unprefixed user-select property is
// asserted here. The -webkit- prefix is exercised in real WKWebView browsers.
function makeRef(el: HTMLElement | null) {
const ref = createRef<HTMLElement>();
Object.defineProperty(ref, "current", { value: el, writable: true });
return ref;
}
function fireMouseDown(target: HTMLElement, shiftKey: boolean) {
target.dispatchEvent(
new MouseEvent("mousedown", { bubbles: true, shiftKey }),
);
}
function fireMouseUp() {
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
}
function getUserSelect() {
return document.documentElement.style.getPropertyValue("user-select");
}
describe("useShiftDragSelectFix", () => {
let el: HTMLDivElement;
beforeEach(() => {
el = document.createElement("div");
document.body.appendChild(el);
document.documentElement.style.removeProperty("user-select");
});
afterEach(() => {
document.body.removeChild(el);
document.documentElement.style.removeProperty("user-select");
});
it("does nothing when ref.current is null", () => {
renderHook(() => useShiftDragSelectFix(makeRef(null)));
expect(getUserSelect()).toBe("");
});
it("does not suppress selection on mousedown without shiftKey", () => {
renderHook(() => useShiftDragSelectFix(makeRef(el)));
act(() => fireMouseDown(el, false));
expect(getUserSelect()).toBe("");
});
it("sets user-select:none on shift+mousedown", () => {
renderHook(() => useShiftDragSelectFix(makeRef(el)));
act(() => fireMouseDown(el, true));
expect(getUserSelect()).toBe("none");
});
it("restores user-select on mouseup after shift+drag", () => {
renderHook(() => useShiftDragSelectFix(makeRef(el)));
act(() => fireMouseDown(el, true));
act(() => fireMouseUp());
expect(getUserSelect()).toBe("");
});
it("only restores once — a second mouseup does not clear a subsequently set value", () => {
renderHook(() => useShiftDragSelectFix(makeRef(el)));
act(() => {
fireMouseDown(el, true);
fireMouseUp();
document.documentElement.style.setProperty("user-select", "text");
fireMouseUp();
});
// The restore listener was already removed after the first mouseup,
// so the second mouseup cannot clear the value we just set.
expect(getUserSelect()).toBe("text");
});
it("removes the mousedown listener on unmount", () => {
const { unmount } = renderHook(() => useShiftDragSelectFix(makeRef(el)));
unmount();
act(() => fireMouseDown(el, true));
expect(getUserSelect()).toBe("");
});
it("handles multiple shift+drag cycles correctly", () => {
renderHook(() => useShiftDragSelectFix(makeRef(el)));
for (let i = 0; i < 3; i++) {
act(() => fireMouseDown(el, true));
expect(getUserSelect()).toBe("none");
act(() => fireMouseUp());
expect(getUserSelect()).toBe("");
}
});
});

View File

@ -0,0 +1,33 @@
import { useEffect } from "react";
/**
* Suppresses text selection during shift-drag in WKWebView.
*
* WKWebView does not suppress selection on pointer-drag the way Chromium does,
* so ace editors, labels, and inputs highlight as the pointer moves over them.
* We set user-select:none on the root element for the lifetime of the drag and
* restore it on mouseup — so normal text editing is completely unaffected.
*/
export function useShiftDragSelectFix(
ref: React.RefObject<HTMLElement | null>,
) {
useEffect(() => {
const el = ref.current;
if (!el) return;
const onMouseDown = (e: MouseEvent) => {
if (!e.shiftKey) return;
document.documentElement.style.setProperty("-webkit-user-select", "none");
document.documentElement.style.setProperty("user-select", "none");
const restore = () => {
document.documentElement.style.removeProperty("-webkit-user-select");
document.documentElement.style.removeProperty("user-select");
document.removeEventListener("mouseup", restore);
};
document.addEventListener("mouseup", restore);
};
el.addEventListener("mousedown", onMouseDown);
return () => el.removeEventListener("mousedown", onMouseDown);
}, [ref]);
}

View File

@ -82,6 +82,7 @@ import {
MemoizedCanvasControls,
MemoizedSidebarTrigger,
} from "./MemoizedComponents";
import { useShiftDragSelectFix } from "./hooks/useShiftDragSelectFix";
import getRandomName from "./utils/get-random-name";
import isWrappedWithClass from "./utils/is-wrapped-with-class";
@ -729,6 +730,8 @@ export default function Page({
setSelectionEnded(false);
}, []);
useShiftDragSelectFix(reactFlowWrapper);
// Workaround to show the menu only after the selection has ended.
useEffect(() => {
if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {