fix(grid): prevent horizontal scrollbar row overlap
This commit is contained in:
parent
ee3de0525e
commit
b509520382
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue