diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 92a1a1d21e..c497a38d55 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -5997,6 +5997,11 @@ function captureViewportAnchorForRefresh(): { row?: PersistedDataGridSelection; if (showTranspose.value || displayItems.value.length === 0) return null; const scroller = useCanvasGridRows.value ? canvasScrollerElement() : gridScrollerElement(); if (!scroller) return null; + // Already viewing the top of the grid: don't anchor to the row currently + // there. Anchoring would re-pin that row to the same on-screen offset even + // after a refresh prepends new rows above it (e.g. newly inserted rows + // under a DESC sort), pushing the new rows above the visible area (#8339). + if (scroller.scrollTop <= 0) return null; const rowHeight = useCanvasGridRows.value ? CANVAS_DATA_GRID_ROW_HEIGHT : DOM_DATA_GRID_ROW_HEIGHT; const fallbackDisplayIndex = Math.max(0, Math.min(displayItems.value.length - 1, Math.floor(scroller.scrollTop / rowHeight))); const item = displayItems.value[fallbackDisplayIndex]; diff --git a/apps/desktop/src/components/grid/__tests__/DataGridExternalResultScroll.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridExternalResultScroll.spec.ts index 610faf996a..379785049f 100644 --- a/apps/desktop/src/components/grid/__tests__/DataGridExternalResultScroll.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/DataGridExternalResultScroll.spec.ts @@ -11,6 +11,14 @@ function resultWatchSource(): string { return dataGridSource.slice(start, end); } +function captureViewportAnchorForRefreshSource(): string { + const start = dataGridSource.indexOf("function captureViewportAnchorForRefresh()"); + const end = dataGridSource.indexOf("function restoreViewportAnchorAfterRefresh("); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + return dataGridSource.slice(start, end); +} + describe("DataGrid result-replacement vertical scroll", () => { it("resets to the first row when the result is replaced without an internal grid action", () => { const watchSource = resultWatchSource(); @@ -53,4 +61,19 @@ describe("DataGrid result-replacement vertical scroll", () => { expect(appendEarlyReturn).toBeGreaterThan(appendReturn); expect(resetBranch).toBeGreaterThan(appendEarlyReturn); }); + + it("does not anchor a toolbar refresh to the current top row when already scrolled to the top (#8339)", () => { + // Anchoring the row that happens to be at scrollTop 0 would re-pin it to the + // same on-screen offset after refresh, which pushes newly inserted rows + // (e.g. rows prepended by an external insert under a DESC sort) above the + // visible viewport instead of showing them. Skipping the anchor when already + // at the top lets the toolbar-refresh path land on the freshest first row. + const anchorSource = captureViewportAnchorForRefreshSource(); + + expect(anchorSource).toContain("#8339"); + const guard = anchorSource.indexOf("if (scroller.scrollTop <= 0) return null;"); + const rowHeightUse = anchorSource.indexOf("const rowHeight ="); + expect(guard).toBeGreaterThanOrEqual(0); + expect(rowHeightUse).toBeGreaterThan(guard); + }); });