Files
openidea/src/workbench/WindowGroupTabs.tsx
T
ntbang0901 555226a9bf 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 <121003585+ntbang0901@users.noreply.github.com>
2026-11-04 01:31:10 +08:00

124 lines
5.1 KiB
TypeScript

import { useRef, useState } from "react";
import { executeAction } from "@/platform/actions";
import { useTx } from "@/platform/i18n";
import { reorderedTabs, useWindowGroup } from "@/platform/windowGroups";
import { Icon, IconButton, showContextMenu } from "@/ui";
import { notifications } from "./contributions";
import styles from "./WindowGroupTabs.module.css";
function run(work: Promise<unknown>) {
void work.catch((e) => notifications().notify({ level: "error", title: "Window group", body: String(e) }));
}
export function WindowGroupTabs() {
const tabs = useWindowGroup((state) => state.tabs);
const drag = useRef<{ label: string; x: number; moved: boolean } | null>(null);
const [dragging, setDragging] = useState<string | null>(null);
const tx = useTx();
if (tabs.length < 2) return null;
return (
<div className={styles.bar} data-window-group>
<div className={styles.tabs} role="tablist" aria-label={tx("Project windows")}>
{tabs.map((tab) => (
<div
className={`${styles.item} ${tab.current ? styles.selected : ""} ${dragging === tab.label ? styles.dragging : ""}`}
key={tab.label}
data-project-tab={tab.label}
onPointerDown={(e) => {
drag.current = null;
if (e.button === 0 && (e.target as Element).closest('[role="tab"]'))
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);
setDragging(tab.label);
}
}}
onPointerUp={(e) => {
const source = drag.current;
if (source?.moved) {
const target = document
.elementFromPoint(e.clientX, e.clientY)
?.closest<HTMLElement>("[data-project-tab]")?.dataset.projectTab;
if (target) run(executeAction("window.reorderTabs", reorderedTabs(tabs, source.label, target)));
}
setDragging(null);
// Keep the moved flag through the click following pointerup.
if (!source?.moved) drag.current = null;
}}
onPointerCancel={() => {
drag.current = null;
setDragging(null);
}}
onClickCapture={(e) => {
if (drag.current?.moved) {
e.preventDefault();
e.stopPropagation();
drag.current = null;
}
}}
onContextMenu={(e) => {
e.preventDefault();
const index = tabs.indexOf(tab);
showContextMenu(e, [
{
label: "Move Tab to New Window",
disabled: !tab.current,
run: () => executeAction("window.detachTab"),
},
{ kind: "separator" },
{ label: "Close Tab", run: () => executeAction("window.closeTab", tab.label) },
{ label: "Close Other Tabs", run: () => executeAction("window.closeOtherTabs", tab.label) },
{
label: "Close Tabs to the Left",
disabled: index <= 0,
run: () => executeAction("window.closeTabsToLeft", tab.label),
},
{
label: "Close Tabs to the Right",
disabled: index >= tabs.length - 1,
run: () => executeAction("window.closeTabsToRight", tab.label),
},
]);
}}
>
<button
className={styles.tab}
role="tab"
aria-selected={tab.current}
tabIndex={tab.current ? 0 : -1}
title={tab.root ?? tab.name}
onClick={() => run(executeAction("window.selectTab", tab.label))}
onKeyDown={(e) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
e.preventDefault();
const index = tabs.indexOf(tab);
const next =
e.key === "Home"
? 0
: e.key === "End"
? tabs.length - 1
: (index + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
run(executeAction("window.selectTab", tabs[next]!.label));
}}
>
<Icon name="folder" size={14} />
<span>{tab.name}</span>
</button>
<IconButton
icon="close"
size="small"
title={tx`Close ${tab.name}`}
onClick={() => run(executeAction("window.closeTab", tab.label))}
/>
</div>
))}
</div>
<IconButton icon="external-link" title="Move Tab to New Window" action="window.detachTab" />
</div>
);
}