fix(grid): prevent horizontal scrollbar row overlap

This commit is contained in:
gggaiitx 2026-07-10 19:38:54 +08:00 committed by GitHub
parent ee3de0525e
commit b509520382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 4 deletions

View File

@ -119,7 +119,7 @@ import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/d
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
import { canGoNextDataGridPage } from "@/lib/dataGrid/dataGridPagination";
import { dataGridScrollPosition, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
import { CANVAS_DATA_GRID_ROW_HEIGHT, drawCanvasDataGrid } from "@/lib/dataGrid/canvasDataGridRenderer";
import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid/dataGridSaveUi";
import type { QueryEditabilityReason } from "@/lib/sql/sqlAnalysis";
@ -2790,8 +2790,22 @@ function updateGridVerticalScrollbar(element: HTMLElement | null = gridScrollerE
function setGridHorizontalOverflow(overflow: boolean) {
if (hasGridHorizontalOverflow.value === overflow) return;
const scroller = gridScrollerElement();
const preserveBottom = overflow && !!scroller && isDataGridAtScrollBottom(scroller);
hasGridHorizontalOverflow.value = overflow;
if (overflow) nextTick(applyGridHorizontalScrollbarThumbStyle);
if (!overflow) return;
nextTick(() => {
applyGridHorizontalScrollbarThumbStyle();
if (!preserveBottom || gridScrollerElement() !== scroller) return;
// The custom horizontal scrollbar changes the scrollable geometry after render;
// restore bottom anchoring through the normal handlers so every grid mode stays synchronized.
scroller.scrollTop = dataGridBottomScrollTop(scroller);
if (useCanvasGridRows.value) {
onCanvasScroll({ target: scroller } as unknown as Event);
} else {
onScrollerScroll({ target: scroller } as unknown as Event);
}
});
}
function setGridVerticalOverflow(overflow: boolean) {
@ -9752,7 +9766,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</div>
<div v-else-if="useCanvasGridRows" ref="scrollerRef" class="data-grid-scroller canvas-grid-scroller flex-1 overflow-auto overscroll-none bg-background relative" :class="{ 'is-scrolling': isScrolling }" @scroll="onCanvasScroll" @wheel="onCanvasWheel">
<div
v-else-if="useCanvasGridRows"
ref="scrollerRef"
class="data-grid-scroller canvas-grid-scroller flex-1 overflow-auto overscroll-none bg-background relative"
:class="{ 'is-scrolling': isScrolling, 'has-horizontal-scrollbar': hasGridHorizontalOverflow }"
@scroll="onCanvasScroll"
@wheel="onCanvasWheel"
>
<div class="relative" :style="{ width: `${totalWidth}px`, height: `${canvasContentHeight}px` }">
<canvas
ref="canvasRef"
@ -9855,7 +9876,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
v-else-if="hasVisibleRows"
ref="scrollerRef"
class="data-grid-scroller dbx-data-grid-font-family flex-1 overflow-x-auto overscroll-none"
:class="{ 'is-scrolling': isScrolling }"
:class="{ 'is-scrolling': isScrolling, 'has-horizontal-scrollbar': hasGridHorizontalOverflow }"
:items="displayItems"
:item-size="26"
:buffer="600"
@ -11257,6 +11278,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
display: none;
}
.canvas-grid-scroller.has-horizontal-scrollbar {
margin-bottom: 10px;
}
.data-grid-scroller.has-horizontal-scrollbar:not(.canvas-grid-scroller) {
padding-bottom: 10px;
}
.data-grid-scroller :deep(.vue-recycle-scroller__item-wrapper) {
min-width: var(--total-w);
overflow: visible;

View File

@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { dataGridBottomScrollTop, isDataGridAtScrollBottom } from "@/lib/dataGrid/dataGridInfiniteScroll";
describe("data grid bottom anchoring", () => {
it("keeps DOM rows anchored when scrollbar padding increases the scroll height", () => {
const before = { scrollTop: 740, scrollHeight: 1000, clientHeight: 260 };
const after = { scrollHeight: 1010, clientHeight: 260 };
expect(isDataGridAtScrollBottom(before)).toBe(true);
expect(dataGridBottomScrollTop(after)).toBe(750);
});
it("keeps canvas rows anchored when scrollbar margin reduces the viewport", () => {
const before = { scrollTop: 740, scrollHeight: 1000, clientHeight: 260 };
const after = { scrollHeight: 1000, clientHeight: 250 };
expect(isDataGridAtScrollBottom(before)).toBe(true);
expect(dataGridBottomScrollTop(after)).toBe(750);
});
it("keeps the quick-entry draft row visible after the horizontal scrollbar appears", () => {
const before = { scrollTop: 766, scrollHeight: 1026, clientHeight: 260 };
const after = { scrollHeight: 1036, clientHeight: 260 };
expect(isDataGridAtScrollBottom(before)).toBe(true);
expect(dataGridBottomScrollTop(after)).toBe(776);
});
it("does not anchor a user who is away from the bottom", () => {
expect(isDataGridAtScrollBottom({ scrollTop: 700, scrollHeight: 1000, clientHeight: 260 })).toBe(false);
});
});

View File

@ -25,3 +25,12 @@ export function shouldCheckInfiniteScrollAfterScroll(previous: DataGridScrollPos
export function isDataGridNearScrollBottom(metrics: DataGridScrollMetrics, threshold = 100): boolean {
return metrics.scrollHeight - metrics.scrollTop - metrics.clientHeight < threshold;
}
export function isDataGridAtScrollBottom(metrics: DataGridScrollMetrics, tolerance = 1): boolean {
const maxScrollTop = Math.max(0, metrics.scrollHeight - metrics.clientHeight);
return maxScrollTop - metrics.scrollTop <= tolerance;
}
export function dataGridBottomScrollTop(metrics: Pick<DataGridScrollMetrics, "scrollHeight" | "clientHeight">): number {
return Math.max(0, metrics.scrollHeight - metrics.clientHeight);
}