mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(grid): ignore pointer jitter during drag selection
This commit is contained in:
@@ -4328,6 +4328,7 @@ const {
|
||||
selectionFocus,
|
||||
finishCellSelection,
|
||||
extendCellSelection,
|
||||
isCellSelectionDragConfirmed,
|
||||
restoreCellSelectionState,
|
||||
cellIsSelected,
|
||||
columnIsSelected,
|
||||
@@ -4505,7 +4506,7 @@ function onCellMouseenter(rowIndex: number, visibleColIdx: number, actualColIdx:
|
||||
quickDownloadMenuCell.value = retainBinaryCellDownloadMenuForHover(quickDownloadMenuCell.value, { rowIndex, col: actualColIdx });
|
||||
if (!isScrolling.value) hoveredDetailCell.value = { rowIndex, col: actualColIdx };
|
||||
}
|
||||
extendCellSelection(rowIndex, visibleColIdx);
|
||||
if (isCellSelectionDragConfirmed()) extendCellSelection(rowIndex, visibleColIdx);
|
||||
}
|
||||
|
||||
function onCellMouseleave(rowIndex: number, actualColIdx: number) {
|
||||
|
||||
@@ -39,6 +39,45 @@ function rowEvent(options: { meta?: boolean; shift?: boolean } = {}): MouseEvent
|
||||
} as MouseEvent;
|
||||
}
|
||||
|
||||
function installPointerDocument() {
|
||||
const originalDocument = globalThis.document;
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
|
||||
const listeners = new Map<string, Set<EventListenerOrEventListenerObject>>();
|
||||
const animationFrames: FrameRequestCallback[] = [];
|
||||
const fakeDocument = {
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject) {
|
||||
const handlers = listeners.get(type) ?? new Set();
|
||||
handlers.add(listener);
|
||||
listeners.set(type, handlers);
|
||||
},
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject) {
|
||||
listeners.get(type)?.delete(listener);
|
||||
},
|
||||
} as Document;
|
||||
Object.defineProperty(globalThis, "document", { configurable: true, value: fakeDocument });
|
||||
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
||||
animationFrames.push(callback);
|
||||
return animationFrames.length;
|
||||
}) as typeof requestAnimationFrame;
|
||||
globalThis.cancelAnimationFrame = (() => undefined) as typeof cancelAnimationFrame;
|
||||
|
||||
return {
|
||||
animationFrames,
|
||||
dispatch(type: string, event: MouseEvent) {
|
||||
listeners.get(type)?.forEach((listener) => {
|
||||
if (typeof listener === "function") listener(event);
|
||||
else listener.handleEvent(event);
|
||||
});
|
||||
},
|
||||
restore() {
|
||||
Object.defineProperty(globalThis, "document", { configurable: true, value: originalDocument });
|
||||
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
||||
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("useDataGridSelection", () => {
|
||||
it("keeps selected columns and the range anchor attached to their columns after reordering", () => {
|
||||
const selection = createSelection();
|
||||
@@ -237,6 +276,81 @@ describe("useDataGridSelection", () => {
|
||||
expect(selection.hasCellSelection.value).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores cell jitter until the drag threshold and exposes confirmation for hover extension", () => {
|
||||
const pointerDocument = installPointerDocument();
|
||||
const selection = createSelection({ cellFromClientPoint: () => ({ rowIndex: 2, colIndex: 2 }) });
|
||||
|
||||
try {
|
||||
selection.beginCellSelection(0, 0, { button: 0, clientX: 100, clientY: 100, preventDefault() {} } as MouseEvent);
|
||||
pointerDocument.dispatch("mousemove", { clientX: 111, clientY: 100 } as MouseEvent);
|
||||
if (selection.isCellSelectionDragConfirmed()) selection.extendCellSelection(2, 2);
|
||||
|
||||
expect(selection.isCellSelectionDragConfirmed()).toBe(false);
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 0, endRow: 0, startCol: 0, endCol: 0 });
|
||||
|
||||
pointerDocument.dispatch("mousemove", { clientX: 112, clientY: 100 } as MouseEvent);
|
||||
|
||||
expect(selection.isCellSelectionDragConfirmed()).toBe(true);
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 0, endRow: 2, startCol: 0, endCol: 2 });
|
||||
} finally {
|
||||
selection.finishCellSelection();
|
||||
pointerDocument.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not extend an unconfirmed cell drag on mouseup", () => {
|
||||
const pointerDocument = installPointerDocument();
|
||||
const selection = createSelection({ cellFromClientPoint: () => ({ rowIndex: 2, colIndex: 2 }) });
|
||||
|
||||
try {
|
||||
selection.beginCellSelection(0, 0, { button: 0, clientX: 100, clientY: 100, preventDefault() {} } as MouseEvent);
|
||||
pointerDocument.dispatch("mousemove", { clientX: 111, clientY: 100 } as MouseEvent);
|
||||
pointerDocument.dispatch("mouseup", { clientX: 111, clientY: 100 } as MouseEvent);
|
||||
|
||||
expect(selection.isSelectingCells.value).toBe(false);
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 0, endRow: 0, startCol: 0, endCol: 0 });
|
||||
} finally {
|
||||
selection.finishCellSelection();
|
||||
pointerDocument.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores row gutter jitter until the drag threshold", () => {
|
||||
const pointerDocument = installPointerDocument();
|
||||
const selection = createSelection({ rowFromClientPoint: () => 3 });
|
||||
|
||||
try {
|
||||
selection.beginRowSelection(1, 2, { button: 0, clientX: 100, clientY: 100, preventDefault() {} } as MouseEvent);
|
||||
pointerDocument.dispatch("mousemove", { clientX: 111, clientY: 100 } as MouseEvent);
|
||||
|
||||
expect(selection.selectedRowIds.value).toEqual(new Set([2]));
|
||||
|
||||
pointerDocument.dispatch("mousemove", { clientX: 112, clientY: 100 } as MouseEvent);
|
||||
pointerDocument.animationFrames.shift()?.(0);
|
||||
|
||||
expect(selection.selectedRowIds.value).toEqual(new Set([2, 3, 4]));
|
||||
} finally {
|
||||
selection.finishRowSelection();
|
||||
pointerDocument.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not extend an unconfirmed row gutter drag on mouseup", () => {
|
||||
const pointerDocument = installPointerDocument();
|
||||
const selection = createSelection({ rowFromClientPoint: () => 3 });
|
||||
|
||||
try {
|
||||
selection.beginRowSelection(1, 2, { button: 0, clientX: 100, clientY: 100, preventDefault() {} } as MouseEvent);
|
||||
pointerDocument.dispatch("mouseup", { clientX: 111, clientY: 100 } as MouseEvent);
|
||||
|
||||
expect(selection.isSelectingRows.value).toBe(false);
|
||||
expect(selection.selectedRowIds.value).toEqual(new Set([2]));
|
||||
} finally {
|
||||
selection.finishRowSelection();
|
||||
pointerDocument.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("selects a continuous row range while dragging the row-number gutter", () => {
|
||||
const originalDocument = globalThis.document;
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
||||
|
||||
@@ -39,6 +39,11 @@ interface RestoredCellSelectionState {
|
||||
|
||||
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.
|
||||
const GRID_SELECTION_DRAG_THRESHOLD_PX = 12;
|
||||
type RowSelectionOperation = "replace" | "add" | "remove";
|
||||
|
||||
export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
@@ -50,6 +55,9 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
const isSelectingRows = ref(false);
|
||||
let selectionPointerClientX = 0;
|
||||
let selectionPointerClientY = 0;
|
||||
let selectionPointerDownClientX = 0;
|
||||
let selectionPointerDownClientY = 0;
|
||||
let selectionDragConfirmed = false;
|
||||
let selectionAutoScrollFrame = 0;
|
||||
let rowSelectionRangeAnchorIndex = -1;
|
||||
let rowSelectionFocusIndex = -1;
|
||||
@@ -419,8 +427,17 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
applyDraggedRowRange(rowIndex);
|
||||
}
|
||||
|
||||
function confirmSelectionDrag(clientX: number, clientY: number): boolean {
|
||||
if (selectionDragConfirmed) return true;
|
||||
const distance = Math.hypot(clientX - selectionPointerDownClientX, clientY - selectionPointerDownClientY);
|
||||
if (distance < GRID_SELECTION_DRAG_THRESHOLD_PX) return false;
|
||||
selectionDragConfirmed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleRowSelectionPointerMove(event: MouseEvent) {
|
||||
if (!isSelectingRows.value) return;
|
||||
if (!confirmSelectionDrag(event.clientX, event.clientY)) return;
|
||||
selectionPointerClientX = event.clientX;
|
||||
selectionPointerClientY = event.clientY;
|
||||
if (!selectionAutoScrollFrame) selectionAutoScrollFrame = requestAnimationFrame(runSelectionAutoScroll);
|
||||
@@ -428,11 +445,11 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
|
||||
function finishRowSelection(event?: MouseEvent) {
|
||||
if (!isSelectingRows.value) return;
|
||||
if (event) {
|
||||
if (event && confirmSelectionDrag(event.clientX, event.clientY)) {
|
||||
selectionPointerClientX = event.clientX;
|
||||
selectionPointerClientY = event.clientY;
|
||||
updateRowSelectionFromPointer();
|
||||
}
|
||||
updateRowSelectionFromPointer();
|
||||
isSelectingRows.value = false;
|
||||
// Keep the range origin stable across repeated Shift selections. For a
|
||||
// plain or meta selection this is the row where the gesture started; for
|
||||
@@ -461,6 +478,9 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
isSelectingRows.value = true;
|
||||
selectionPointerClientX = event.clientX;
|
||||
selectionPointerClientY = event.clientY;
|
||||
selectionPointerDownClientX = event.clientX;
|
||||
selectionPointerDownClientY = event.clientY;
|
||||
selectionDragConfirmed = false;
|
||||
document.addEventListener("mouseup", finishRowSelection);
|
||||
document.addEventListener("mousemove", handleRowSelectionPointerMove);
|
||||
}
|
||||
@@ -529,6 +549,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
|
||||
function handleSelectionPointerMove(event: MouseEvent) {
|
||||
if (!isSelectingCells.value) return;
|
||||
if (!confirmSelectionDrag(event.clientX, event.clientY)) return;
|
||||
selectionPointerClientX = event.clientX;
|
||||
selectionPointerClientY = event.clientY;
|
||||
updateSelectionFromPointer();
|
||||
@@ -549,6 +570,9 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
isSelectingCells.value = true;
|
||||
selectionPointerClientX = event.clientX;
|
||||
selectionPointerClientY = event.clientY;
|
||||
selectionPointerDownClientX = event.clientX;
|
||||
selectionPointerDownClientY = event.clientY;
|
||||
selectionDragConfirmed = false;
|
||||
lastClickedColumnIndex.value = colIndex;
|
||||
if (showTranspose.value) transposeRowIndex.value = rowIndex;
|
||||
document.addEventListener("mouseup", finishCellSelection);
|
||||
@@ -563,6 +587,13 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
if (options.runtimeScope) options.runtimeScope.addCleanup(finishSelection);
|
||||
else if (getCurrentScope()) onScopeDispose(finishSelection);
|
||||
|
||||
// Some callers extend the selection off native DOM hover events rather than the pointermove
|
||||
// handler above, with no pixel-distance info of their own. They should check this first, so
|
||||
// hovering into an adjacent cell during click jitter doesn't grow the selection.
|
||||
function isCellSelectionDragConfirmed(): boolean {
|
||||
return selectionDragConfirmed;
|
||||
}
|
||||
|
||||
function extendCellSelection(rowIndex: number, colIndex: number) {
|
||||
if (!isSelectingCells.value || !selectionAnchor.value) return;
|
||||
const current = selectionFocus.value;
|
||||
@@ -645,6 +676,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
restoreCellSelectionState,
|
||||
beginCellSelection,
|
||||
extendCellSelection,
|
||||
isCellSelectionDragConfirmed,
|
||||
cellIsSelected,
|
||||
columnIsSelected,
|
||||
selectedRangeStart,
|
||||
|
||||
Reference in New Issue
Block a user