дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(ui): start pointer drags only while the primary button is held (#1071)
On macOS, with an editable element focused and an input method active, WebKit can deliver a tap as pointerup then pointerdown, so drag trackers saw every later pointermove as a held button: one tap plus a pointer move started a phantom drag (file drag label, tab detach, tool window move) and the next tap dropped it. A drag now starts only from a move with the primary button down, a move without it cancels the press, and late button-less moves after a drag started are ignored so the drop is kept. Resizers, dialog drag/resize, pan and the review gutter end instead; the Project view also listens for pointercancel. Refs #760 Co-authored-by: ntbang0901 <[email protected]>
Цей коміт міститься в:
1 джерело
a35e434376
коміт
555226a9bf
15 files changed
+180
-9
No files matched your search
@@ -282,8 +282,10 @@ export function Grid(props: GridProps) {
|
||||
e.stopPropagation();
|
||||
const startX = e.clientX;
|
||||
const start = widths[col] ?? 100;
|
||||
const move = (ev: globalThis.MouseEvent) =>
|
||||
const move = (ev: globalThis.MouseEvent) => {
|
||||
if (!(ev.buttons & 1)) return up();
|
||||
setWidths((w) => w.map((x, i) => (i === col ? Math.max(40, start + ev.clientX - startX) : x)));
|
||||
};
|
||||
const up = () => {
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseup", up);
|
||||
|
||||
@@ -242,6 +242,35 @@ describe("ProjectView", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("starts a file drag only while the primary button is held", async () => {
|
||||
const { row, rows } = setup();
|
||||
await waitFor(() => expect(rows()).toContain("README.md"));
|
||||
document.elementFromPoint = () => null; // jsdom lacks it; the drop-target lookup needs it
|
||||
// The drag label repeats the file name outside the tree row.
|
||||
const ghost = () => (screen.queryAllByText("README.md").length > 1 ? true : null);
|
||||
const move = (buttons: number, x: number) =>
|
||||
act(() => {
|
||||
window.dispatchEvent(new MouseEvent("pointermove", { bubbles: true, buttons, clientX: x, clientY: 0 }));
|
||||
});
|
||||
const release = () => act(() => void window.dispatchEvent(new MouseEvent("pointerup", { bubbles: true })));
|
||||
// jsdom has no PointerEvent, so React would see `button` undefined on a plain pointerDown.
|
||||
const press = () =>
|
||||
fireEvent(
|
||||
row("README.md").querySelector("[data-project-path]")!,
|
||||
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
||||
);
|
||||
// A macOS tap WebKit reorders: the press arrives late and no button is held.
|
||||
press();
|
||||
move(0, 40);
|
||||
expect(ghost()).toBeNull();
|
||||
release();
|
||||
press();
|
||||
move(1, 40);
|
||||
expect(ghost()).not.toBeNull();
|
||||
release();
|
||||
expect(ghost()).toBeNull();
|
||||
});
|
||||
|
||||
it("cancels a new file when its name field loses focus", async () => {
|
||||
const { rows } = setup();
|
||||
await waitFor(() => expect(rows()).toContain("README.md"));
|
||||
|
||||
@@ -581,6 +581,11 @@ export function ProjectView() {
|
||||
// Files (not folders) can also be dropped on an editor to open them there.
|
||||
let files: DragSession | null = null;
|
||||
const move = (ev: PointerEvent) => {
|
||||
// A press with no button held is a reordered macOS tap, not a drag (see pointerDrag.ts).
|
||||
if (!(ev.buttons & 1)) {
|
||||
if (!active) cleanup();
|
||||
return;
|
||||
}
|
||||
if (!active) {
|
||||
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) < DRAG_THRESHOLD) return;
|
||||
active = true;
|
||||
@@ -626,6 +631,7 @@ export function ProjectView() {
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
window.removeEventListener("pointercancel", cleanup);
|
||||
window.removeEventListener("keydown", key, true);
|
||||
files?.cancel();
|
||||
setDropTarget(null);
|
||||
@@ -633,6 +639,7 @@ export function ProjectView() {
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
window.addEventListener("pointercancel", cleanup);
|
||||
window.addEventListener("keydown", key, true);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -113,7 +113,16 @@ describe("service reordering integration", () => {
|
||||
const target = within(tree()).getByText("Gateway").closest('[role="treeitem"]')!;
|
||||
Object.defineProperty(document, "elementFromPoint", { configurable: true, value: () => target });
|
||||
const pointer = (el: Element | Window, type: string, y: number) =>
|
||||
fireEvent(el, new MouseEvent(type, { bubbles: true, button: 0, clientX: 70, clientY: y }));
|
||||
fireEvent(
|
||||
el,
|
||||
new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
button: 0,
|
||||
buttons: type === "pointerup" ? 0 : 1,
|
||||
clientX: 70,
|
||||
clientY: y,
|
||||
}),
|
||||
);
|
||||
pointer(source, "pointerdown", 60);
|
||||
pointer(window, "pointermove", 26);
|
||||
expect(document.querySelector('[aria-hidden="true"][role="treeitem"]')).not.toBeNull();
|
||||
|
||||
@@ -252,6 +252,7 @@ export function reviewGutter(handlers: ReviewGutterHandlers): Extension {
|
||||
const startDrag = (view: EditorView, drag: Drag) => {
|
||||
view.dispatch({ effects: setDrag.of(drag) });
|
||||
const move = (e: MouseEvent) => {
|
||||
if (!(e.buttons & 1)) return up();
|
||||
const line = lineAtY(view, e.clientY);
|
||||
const cur = view.state.field(field).drag;
|
||||
if (line === null || !cur) return;
|
||||
|
||||
@@ -47,6 +47,7 @@ export function attachPanZoom(viewport: HTMLElement): void {
|
||||
});
|
||||
viewport.addEventListener("pointermove", (e) => {
|
||||
if (!drag || drag.id !== e.pointerId) return;
|
||||
if (!(e.buttons & 1)) return end(e);
|
||||
apply({ ...view, x: view.x + e.clientX - drag.x, y: view.y + e.clientY - drag.y });
|
||||
drag = { ...drag, x: e.clientX, y: e.clientY };
|
||||
});
|
||||
|
||||
Reference in new issue
Заблокувати користувача