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]>
Цей коміт міститься в:
ntbang0901 authored and GitHub committed 2026-11-04 01:31:10 +08:00
1 джерело a35e434376
коміт 555226a9bf
15 files changed
+180 -9

No files matched your search

+1
Переглянути файл
@@ -30,6 +30,7 @@ export function WindowGroupTabs() {
drag.current = { label: tab.label, x: e.clientX, moved: false };
}}
onPointerMove={(e) => {
if (drag.current && !(e.buttons & 1)) drag.current = null; // reordered macOS tap, not a drag
if (drag.current && Math.abs(e.clientX - drag.current.x) > 5) {
drag.current.moved = true;
e.currentTarget.setPointerCapture(e.pointerId);
+6
Переглянути файл
@@ -219,6 +219,7 @@ function StripeHandle({
document.body.style.cursor = "col-resize";
let next = { names, width, px: start };
const move = (ev: globalThis.PointerEvent) => {
if (!(ev.buttons & 1)) return end();
const r = resolve(start + sign * (ev.clientX - origin));
if (r.names !== next.names) onPreview(r.names);
next = r;
@@ -314,6 +315,8 @@ function startStripeDrag(e: PointerEvent<HTMLButtonElement>, id: string) {
};
const onMove = (ev: globalThis.PointerEvent) => {
if (!ghost) {
// Reordered macOS tap: no button held, so not a drag (see pointerDrag.ts).
if (!(ev.buttons & 1)) return finish(false);
if (Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < DRAG_THRESHOLD) return;
const r = button.getBoundingClientRect();
ghost = button.cloneNode(true) as HTMLElement;
@@ -328,6 +331,7 @@ function startStripeDrag(e: PointerEvent<HTMLButtonElement>, id: string) {
document.body.appendChild(ghost);
useStripeDrag.setState({ id, drop: null });
}
if (!(ev.buttons & 1)) return;
place(ev.clientX, ev.clientY);
const drop = stripeDropAt(ev.clientX, ev.clientY);
const cur = useStripeDrag.getState().drop;
@@ -812,6 +816,7 @@ function SplitHandle({ anchor, hidden }: { anchor: ToolWindowAnchor; hidden: boo
document.body.style.cursor = stacked ? "row-resize" : "col-resize";
let ratio: number | null = null;
const move = (ev: globalThis.PointerEvent) => {
if (!(ev.buttons & 1)) return end();
ratio = ratioOf(m.clamp(m.first + (stacked ? ev.clientY : ev.clientX) - origin), m.total);
apply(m.areas, ratio);
};
@@ -963,6 +968,7 @@ function GapHandle({
document.body.style.cursor = vertical ? "col-resize" : "row-resize";
let next = m.size;
const move = (ev: globalThis.PointerEvent) => {
if (!(ev.buttons & 1)) return end();
const raw = m.size + sign * ((vertical ? ev.clientX : ev.clientY) - origin);
// Dragged past half the minimum: collapse, as IDEA does.
next = raw < MIN[anchor] / 2 ? 0 : clamp(raw, m.max);