From 7653ebfda6d1bbf184f423876d67cac0cc38d7bf Mon Sep 17 00:00:00 2001 From: t8y2 Date: Tue, 22 Sep 2026 14:26:09 +0800 Subject: [PATCH] fix(grid): stabilize buffered columns while scrolling Closes #8498 --- .../__tests__/useDataGridColumnLayout.spec.ts | 41 ++++++++++++++++++- .../composables/useDataGridColumnLayout.ts | 28 ++++++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts index 5d05a62f1f..131ae1a4ea 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import { effectScope, nextTick, ref } from "vue"; +import { effectScope, nextTick, ref, watch } from "vue"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { dataGridColumnOffsets, dataGridHorizontalColumnWindow, useDataGridColumnLayout, useDataGridColumnLayoutState, type ColumnHeaderReferenceDragController } from "@/composables/useDataGridColumnLayout"; import { columnHeaderDragAutoScrollDelta, columnHeaderDropTargetIndex } from "@/lib/dataGrid/dataGridColumnHeaderInteraction"; @@ -40,6 +40,45 @@ describe("useDataGridColumnLayout", () => { }); }); + it("keeps rendered columns stable while scrolling inside the same buffered window", async () => { + const scrollLeft = ref(100); + const columnNames = ref(Array.from({ length: 40 }, (_, index) => `column_${index}`)); + const visibleColumnIndexes = ref(columnNames.value.map((_, index) => index)); + const renderedColumnWidths = ref(columnNames.value.map(() => 100)); + const scope = effectScope(); + const layout = scope.run(() => + useDataGridColumnLayout({ + columnNames, + visibleColumnIndexes, + renderedColumnWidths, + scrollLeft, + viewportWidth: ref(500), + rowNumberWidth: ref(40), + }), + )!; + + const firstWindow = layout.horizontalColumnWindow.value; + const firstColumns = layout.renderedGridColumns.value; + const renderedColumnUpdates = vi.fn(); + const stopWatching = watch(layout.renderedGridColumns, renderedColumnUpdates, { flush: "sync" }); + + for (let position = 101; position <= 140; position += 1) scrollLeft.value = position; + await nextTick(); + + expect(layout.horizontalColumnWindow.value).toBe(firstWindow); + expect(layout.renderedGridColumns.value).toBe(firstColumns); + expect(renderedColumnUpdates).not.toHaveBeenCalled(); + + scrollLeft.value = 1_500; + await nextTick(); + + expect(layout.horizontalColumnWindow.value).not.toBe(firstWindow); + expect(layout.renderedGridColumns.value).not.toBe(firstColumns); + expect(renderedColumnUpdates).toHaveBeenCalledTimes(1); + stopWatching(); + scope.stop(); + }); + it("accelerates column drag scrolling toward the viewport edges", () => { expect(columnHeaderDragAutoScrollDelta({ clientX: 100, viewportLeft: 0, viewportRight: 400 })).toBe(0); expect(columnHeaderDragAutoScrollDelta({ clientX: 32, viewportLeft: 0, viewportRight: 400 })).toBeLessThan(0); diff --git a/apps/desktop/src/composables/useDataGridColumnLayout.ts b/apps/desktop/src/composables/useDataGridColumnLayout.ts index 1d12f8e1df..033d8d60f2 100644 --- a/apps/desktop/src/composables/useDataGridColumnLayout.ts +++ b/apps/desktop/src/composables/useDataGridColumnLayout.ts @@ -99,6 +99,11 @@ export function dataGridHorizontalColumnWindow(options: { widths: readonly numbe return { start, end, beforeWidth: offsets[start] ?? 0, afterWidth: Math.max(0, columnsWidth - visibleWidth) }; } +export function stableDataGridHorizontalColumnWindow(previous: DataGridHorizontalColumnWindow | undefined, next: DataGridHorizontalColumnWindow): DataGridHorizontalColumnWindow { + if (previous && previous.start === next.start && previous.end === next.end && previous.beforeWidth === next.beforeWidth && previous.afterWidth === next.afterWidth) return previous; + return next; +} + export function useDataGridColumnLayoutState(options: { columns: MaybeRefOrGetter; sourceColumns?: MaybeRefOrGetter; @@ -503,16 +508,19 @@ export function useDataGridColumnLayout(options: { }) { const renderedColumnOffsets = computed(() => dataGridColumnOffsets(toValue(options.renderedColumnWidths))); const frozenColumnCount = computed(() => toValue(options.frozenColumnCount ?? 0)); - const horizontalColumnWindow = computed(() => - dataGridHorizontalColumnWindow({ - widths: toValue(options.renderedColumnWidths), - offsets: renderedColumnOffsets.value, - columnCount: toValue(options.visibleColumnIndexes).length, - scrollLeft: toValue(options.scrollLeft), - viewportWidth: toValue(options.viewportWidth), - rowNumberWidth: toValue(options.rowNumberWidth), - bufferPx: options.bufferPx ?? 900, - }), + const horizontalColumnWindow = computed((previous) => + stableDataGridHorizontalColumnWindow( + previous, + dataGridHorizontalColumnWindow({ + widths: toValue(options.renderedColumnWidths), + offsets: renderedColumnOffsets.value, + columnCount: toValue(options.visibleColumnIndexes).length, + scrollLeft: toValue(options.scrollLeft), + viewportWidth: toValue(options.viewportWidth), + rowNumberWidth: toValue(options.rowNumberWidth), + bufferPx: options.bufferPx ?? 900, + }), + ), ); const renderedGridColumns = computed(() => { const columnNames = toValue(options.columnNames);