fix(grid): handle horizontal wheel input

Closes #6793
This commit is contained in:
t8y2 committed 2026-08-22 10:17:35 +00:00
1 parent de99931c04
commit 53b7be3c6f
4 files changed
+287 -31

No files matched your search

+30 -31
View File
@@ -195,6 +195,7 @@ import {
type ResultScopedRowCache,
} from "@/lib/dataGrid/dataGridLargeValues";
import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, isDataGridPrefixAppend, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
@@ -6721,14 +6722,9 @@ function onCanvasScroll(event: Event) {
scheduleCanvasDraw();
}
function canvasWheelDeltaToPixels(delta: number, deltaMode: number, pageSize: number): number {
if (deltaMode === WheelEvent.DOM_DELTA_LINE) return delta * CANVAS_DATA_GRID_ROW_HEIGHT;
if (deltaMode === WheelEvent.DOM_DELTA_PAGE) return delta * pageSize;
return delta;
}
function shouldAccelerateCanvasWheel(event: WheelEvent): boolean {
if (event.ctrlKey || event.metaKey) return false;
if (event.deltaX !== 0) return true;
if (event.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) return true;
return event.shiftKey && Math.abs(event.deltaY) > Math.abs(event.deltaX) && Math.abs(event.deltaY) >= CANVAS_TRACKPAD_DELTA_THRESHOLD;
}
@@ -6738,42 +6734,45 @@ function onCanvasWheel(event: WheelEvent) {
const scroller = canvasScrollerElement();
if (!scroller) return;
const verticalDelta = canvasWheelDeltaToPixels(event.deltaY, event.deltaMode, scroller.clientHeight);
const horizontalDelta = canvasWheelDeltaToPixels(event.deltaX, event.deltaMode, scroller.clientWidth);
const shiftedHorizontalDelta = event.shiftKey && Math.abs(verticalDelta) > Math.abs(horizontalDelta) ? verticalDelta : 0;
const nextTop = shiftedHorizontalDelta === 0 ? Math.max(0, Math.min(scroller.scrollHeight - scroller.clientHeight, scroller.scrollTop + verticalDelta * CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER)) : scroller.scrollTop;
const nextLeft = Math.max(0, Math.min(scroller.scrollWidth - scroller.clientWidth, scroller.scrollLeft + (horizontalDelta + shiftedHorizontalDelta) * CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER));
if (nextTop === scroller.scrollTop && nextLeft === scroller.scrollLeft) return;
const wheelScroll = resolveDataGridWheelScroll({
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaMode: event.deltaMode,
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
lineSize: CANVAS_DATA_GRID_ROW_HEIGHT,
metrics: scroller,
accelerationFactor: CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER,
});
if (!wheelScroll.moved) return;
event.preventDefault();
scroller.scrollTop = nextTop;
scroller.scrollLeft = nextLeft;
event.stopPropagation();
scroller.scrollTop = wheelScroll.nextScrollTop;
scroller.scrollLeft = wheelScroll.nextScrollLeft;
onCanvasScroll({ target: scroller } as unknown as Event);
}
function onDomGridWheel(event: WheelEvent) {
if (event.ctrlKey || event.metaKey) return;
const scroller = event.currentTarget instanceof HTMLElement ? event.currentTarget : gridScrollerElement();
if (!scroller) return;
const verticalDelta = canvasWheelDeltaToPixels(event.deltaY, event.deltaMode, scroller.clientHeight);
const horizontalDelta = canvasWheelDeltaToPixels(event.deltaX, event.deltaMode, scroller.clientWidth);
const shiftedHorizontalDelta = event.shiftKey && Math.abs(verticalDelta) > Math.abs(horizontalDelta) ? verticalDelta : 0;
const effectiveVerticalDelta = shiftedHorizontalDelta === 0 ? verticalDelta : 0;
const effectiveHorizontalDelta = horizontalDelta + shiftedHorizontalDelta;
if (effectiveVerticalDelta === 0 && effectiveHorizontalDelta === 0) return;
const maxTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
const maxLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
const nextTop = Math.max(0, Math.min(maxTop, scroller.scrollTop + effectiveVerticalDelta));
const nextLeft = Math.max(0, Math.min(maxLeft, scroller.scrollLeft + effectiveHorizontalDelta));
// Let an outer scroll container handle wheel input once the grid reaches its boundary.
if (nextTop === scroller.scrollTop && nextLeft === scroller.scrollLeft) return;
const wheelScroll = resolveDataGridWheelScroll({
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaMode: event.deltaMode,
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
lineSize: CANVAS_DATA_GRID_ROW_HEIGHT,
metrics: scroller,
});
if (!wheelScroll.moved) return;
event.preventDefault();
event.stopPropagation();
scroller.scrollTop = nextTop;
scroller.scrollLeft = nextLeft;
scroller.scrollTop = wheelScroll.nextScrollTop;
scroller.scrollLeft = wheelScroll.nextScrollLeft;
onScrollerScroll({ target: scroller } as unknown as Event);
}
@@ -0,0 +1,36 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
function sourceBetween(start: string, end: string): string {
const startIndex = dataGridSource.indexOf(start);
const endIndex = dataGridSource.indexOf(end, startIndex + start.length);
expect(startIndex).toBeGreaterThanOrEqual(0);
expect(endIndex).toBeGreaterThan(startIndex);
return dataGridSource.slice(startIndex, endIndex);
}
describe("data grid wheel bindings", () => {
it("routes Canvas and DOM wheel calculations through the shared helper", () => {
const canvasHandler = sourceBetween("function onCanvasWheel", "function onDomGridWheel");
const domHandler = sourceBetween("function onDomGridWheel", "function onCanvasMouseMove");
for (const handler of [canvasHandler, domHandler]) {
expect(handler).toContain("resolveDataGridWheelScroll({");
expect(handler).toContain("if (!wheelScroll.moved) return;");
expect(handler.indexOf("if (!wheelScroll.moved) return;")).toBeLessThan(handler.indexOf("event.preventDefault();"));
expect(handler).toContain("event.stopPropagation();");
}
});
it("accepts genuine pixel deltaX in the Canvas handler", () => {
const predicate = sourceBetween("function shouldAccelerateCanvasWheel", "function onCanvasWheel");
expect(predicate).toContain("if (event.deltaX !== 0) return true;");
});
it("keeps wheel handlers bound in both render modes", () => {
expect(dataGridSource).toContain('@wheel="onCanvasWheel"');
expect(dataGridSource.match(/@wheel="onDomGridWheel"/g)).toHaveLength(2);
});
});