mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 17:33:09 +08:00
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:
@ -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("");
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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]);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user