fix(tabs): stop touch input from arming tab drag reorder
This commit is contained in:
1 parent
1d7fa00fe3
commit
b6c83838f3
4 files changed
+53
-17
No files matched your search
@@ -544,10 +544,13 @@ function handleTabClick(tab: QueryTab) {
|
||||
activateTab(tab.id);
|
||||
}
|
||||
|
||||
function handleTabMouseDown(event: MouseEvent, tabId: string) {
|
||||
function handleTabMouseDown(event: PointerEvent, tabId: string) {
|
||||
if (event.button === 0) {
|
||||
dispatchBeforeTabSwitch(tabId);
|
||||
event.preventDefault();
|
||||
// Don't preventDefault touch pointerdowns: that would cancel the tab
|
||||
// strip's native horizontal scroll, which is how touch users browse an
|
||||
// overflowing tab bar.
|
||||
if (event.pointerType !== "touch") event.preventDefault();
|
||||
}
|
||||
tabDrag.startDrag(event, tabId);
|
||||
}
|
||||
@@ -663,7 +666,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick.stop="startRenameTab(tab)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@mousedown="handleTabMouseDown($event, tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
@@ -861,7 +864,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick.stop="startRenameTab(tab)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@mousedown="handleTabMouseDown($event, tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
|
||||
+28
-6
@@ -7,21 +7,21 @@ function createDragHarness(onDrop = vi.fn(() => true)) {
|
||||
const drag = useTabDrag(onDrop);
|
||||
const source = document.createElement("div");
|
||||
source.innerHTML = '<span class="truncate">Source</span>';
|
||||
source.addEventListener("mousedown", (event) => drag.startDrag(event, "source"));
|
||||
source.addEventListener("pointerdown", (event) => drag.startDrag(event as PointerEvent, "source"));
|
||||
document.body.appendChild(source);
|
||||
return { drag, source, onDrop };
|
||||
}
|
||||
|
||||
function beginDrag(source: HTMLElement, x = 100, y = 20) {
|
||||
source.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0, clientX: x, clientY: y }));
|
||||
function beginDrag(source: HTMLElement, x = 100, y = 20, pointerType = "mouse") {
|
||||
source.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0, clientX: x, clientY: y, pointerType }));
|
||||
}
|
||||
|
||||
function movePointer(x: number, y = 20) {
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, clientX: x, clientY: y }));
|
||||
function movePointer(x: number, y = 20, pointerType = "mouse") {
|
||||
document.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, clientX: x, clientY: y, pointerType }));
|
||||
}
|
||||
|
||||
function releasePointer() {
|
||||
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
||||
document.dispatchEvent(new PointerEvent("pointerup", { bubbles: true }));
|
||||
}
|
||||
|
||||
function enterDropTarget(drag: ReturnType<typeof useTabDrag>, tabId = "target", x = 10) {
|
||||
@@ -90,4 +90,26 @@ describe("useTabDrag", () => {
|
||||
beginDrag(source);
|
||||
expect(drag.state.suppressClick).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores 18px of jitter, matching a typical touchscreen tap reported as generic mouse input", () => {
|
||||
const { drag, source, onDrop } = createDragHarness();
|
||||
beginDrag(source);
|
||||
|
||||
movePointer(100 - 18, 24);
|
||||
|
||||
expect(drag.state.active).toBe(false);
|
||||
expect(drag.state.suppressClick).toBe(false);
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("never arms a drag for touch input, even past the horizontal threshold", () => {
|
||||
const { drag, source, onDrop } = createDragHarness();
|
||||
beginDrag(source, 100, 20, "touch");
|
||||
|
||||
movePointer(100 + TAB_DRAG_HORIZONTAL_THRESHOLD + 20, 20, "touch");
|
||||
|
||||
expect(drag.state.active).toBe(false);
|
||||
expect(drag.state.suppressClick).toBe(false);
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -41,8 +41,9 @@ const AUTO_SCROLL_EDGE_SIZE = 40;
|
||||
const AUTO_SCROLL_MAX_SPEED = 28;
|
||||
// Trackpad/mouse jitter during a plain click can move the pointer a few pixels between
|
||||
// mousedown and mouseup. Without a minimum drag distance, that jitter reads as an
|
||||
// intentional drag and turns a single click into a multi-cell/row selection. Matches
|
||||
// TAB_DRAG_HORIZONTAL_THRESHOLD in useTabDrag.ts, which guards the same class of gesture.
|
||||
// intentional drag and turns a single click into a multi-cell/row selection. Guards the
|
||||
// same class of gesture as TAB_DRAG_HORIZONTAL_THRESHOLD in useTabDrag.ts, though that one
|
||||
// is tuned higher to also absorb touchscreen tap jitter, which doesn't apply here.
|
||||
const GRID_SELECTION_DRAG_THRESHOLD_PX = 12;
|
||||
type RowSelectionOperation = "replace" | "add" | "remove";
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ interface TabDragState {
|
||||
startY: number;
|
||||
}
|
||||
|
||||
export const TAB_DRAG_HORIZONTAL_THRESHOLD = 12;
|
||||
// Many touchscreen digitizers report to the OS/webview as a plain HID mouse
|
||||
// (no distinguishable pointerType), with tap contact-point drift well beyond
|
||||
// what real mouse/trackpad click jitter produces. 24px absorbs that jitter
|
||||
// while still requiring a clearly deliberate drag to reorder a tab.
|
||||
export const TAB_DRAG_HORIZONTAL_THRESHOLD = 24;
|
||||
|
||||
const state = reactive<TabDragState>({
|
||||
active: false,
|
||||
@@ -74,7 +78,7 @@ function removeGhost() {
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!pending && !state.active) return;
|
||||
|
||||
if (pending && !state.active) {
|
||||
@@ -122,8 +126,8 @@ let listenersAttached = false;
|
||||
|
||||
function ensureListeners() {
|
||||
if (listenersAttached) return;
|
||||
document.addEventListener("mousemove", onMouseMove, true);
|
||||
document.addEventListener("mouseup", onMouseUp, true);
|
||||
document.addEventListener("pointermove", onPointerMove, true);
|
||||
document.addEventListener("pointerup", onMouseUp, true);
|
||||
listenersAttached = true;
|
||||
}
|
||||
|
||||
@@ -131,8 +135,14 @@ export function useTabDrag(onDrop: (draggedId: string, targetId: string, positio
|
||||
ensureListeners();
|
||||
onDropCallback = onDrop;
|
||||
|
||||
function startDrag(event: MouseEvent, tabId: string) {
|
||||
function startDrag(event: PointerEvent, tabId: string) {
|
||||
if (event.button !== 0) return;
|
||||
// Touch input has no reliable equivalent of mouse jitter: a real finger's
|
||||
// contact point commonly drifts well past TAB_DRAG_HORIZONTAL_THRESHOLD
|
||||
// during an ordinary tap, which would misread the tap as a drag. Skip
|
||||
// arming the drag for touch entirely and let the tab strip's native
|
||||
// horizontal scroll handle the gesture instead.
|
||||
if (event.pointerType === "touch") return;
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest("button, input, [data-tab-title-input]")) return;
|
||||
state.suppressClick = false;
|
||||
|
||||
Reference in new issue
Block a user