fix(data-grid): preserve scroll when editing cells

This commit is contained in:
zipg 2026-07-22 23:50:51 +08:00 committed by GitHub
parent 7961004a2c
commit 2443a550e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 69 additions and 11 deletions

View File

@ -5793,6 +5793,9 @@ function prepareDataCellMouseDown(item: RowItem, actualColIdx: number) {
pendingQuickEntryDraftCellFocus.value = { rowId: item.id, col: actualColIdx };
} else {
pendingQuickEntryDraftCellFocus.value = null;
if (editing && (editing.rowId !== item.id || editing.col !== actualColIdx)) {
void commitEditFromCellBlur();
}
}
}

View File

@ -2,6 +2,7 @@ import { ref, computed, nextTick, watch, getCurrentInstance, onActivated, onBefo
import * as api from "@/lib/backend/api";
import type { CellValue } from "@/lib/dataGrid/cellValue";
import { coerceDataGridCellValue, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion";
import { focusDataGridEditorWithoutScrolling, preserveDataGridScrollPosition } from "@/lib/dataGrid/dataGridEditorFocus";
import { normalizeDataGridSaveError } from "@/lib/dataGrid/dataGridSql";
import { rowStatusFilterAfterAddingRow, type RowStatusFilter } from "@/lib/dataGrid/gridRowStatus";
import { supportsDataGridTransaction } from "@/lib/table/tableEditing";
@ -271,9 +272,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
function focusEditInput(select = true) {
const focusInput = () => {
if (typeof document === "undefined") return;
const root = getScrollerElement()?.closest("[data-grid-root]");
const scroller = getScrollerElement();
const root = scroller?.closest("[data-grid-root]");
const input = (root ?? document).querySelector(".cell-edit-input") as HTMLInputElement | HTMLTextAreaElement | null;
input?.focus();
if (input) focusDataGridEditorWithoutScrolling(input, scroller);
if (select && input) {
if (input instanceof HTMLTextAreaElement && input.dataset.expandedCellEditor === "true") {
input.setSelectionRange?.(0, 0);
@ -399,14 +401,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
}
function preserveScrollPosition() {
const el = getScrollerElement();
if (!el) return () => {};
const top = el.scrollTop;
const left = el.scrollLeft;
return () => {
el.scrollTop = top;
el.scrollLeft = left;
};
return preserveDataGridScrollPosition(getScrollerElement());
}
function readScrollPosition(): PendingChangesSnapshot["scroll"] | undefined {
@ -457,6 +452,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
restoreScroll();
nextTick(() => {
restoreScroll();
if (typeof requestAnimationFrame !== "function") {
isCancelling = false;
return;
}
let attempts = 0;
const restoreNextFrame = () => {
restoreScroll();
@ -731,7 +730,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
suppressNextBlurCommit = false;
return;
}
await commitEditAndMaybeAutoSave(options);
const restoreScroll = preserveScrollPosition();
const pendingCommit = commitEditAndMaybeAutoSave(options);
restoreScrollAcrossFrames(restoreScroll);
await pendingCommit;
}
function applyCellValue(rowId: number, col: number, value: string | null, options: ApplyCellValueOptions = {}) {

View File

@ -0,0 +1,34 @@
import { describe, expect, it, vi } from "vitest";
import { focusDataGridEditorWithoutScrolling, preserveDataGridScrollPosition } from "@/lib/dataGrid/dataGridEditorFocus";
describe("preserveDataGridScrollPosition", () => {
it("restores the viewport after an editor is removed", () => {
const scroller = { scrollLeft: 720, scrollTop: 180 };
const restoreScroll = preserveDataGridScrollPosition(scroller);
scroller.scrollLeft = 0;
scroller.scrollTop = 0;
restoreScroll();
expect(scroller).toEqual({ scrollLeft: 720, scrollTop: 180 });
});
});
describe("focusDataGridEditorWithoutScrolling", () => {
it("prevents focus from moving the data grid viewport", () => {
const scroller = { scrollLeft: 720, scrollTop: 180 };
const input = {
focus: vi.fn((options?: FocusOptions) => {
expect(options).toEqual({ preventScroll: true });
// Some WebViews still move a virtualized ancestor while mounting the editor.
scroller.scrollLeft = 0;
scroller.scrollTop = 0;
}),
};
focusDataGridEditorWithoutScrolling(input, scroller);
expect(input.focus).toHaveBeenCalledOnce();
expect(scroller).toEqual({ scrollLeft: 720, scrollTop: 180 });
});
});

View File

@ -0,0 +1,19 @@
type DataGridEditorInput = Pick<HTMLElement, "focus">;
type DataGridEditorScroller = Pick<HTMLElement, "scrollLeft" | "scrollTop">;
export function preserveDataGridScrollPosition(scroller?: DataGridEditorScroller | null) {
if (!scroller) return () => {};
const scrollLeft = scroller.scrollLeft;
const scrollTop = scroller.scrollTop;
return () => {
if (scroller.scrollLeft !== scrollLeft) scroller.scrollLeft = scrollLeft;
if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
};
}
export function focusDataGridEditorWithoutScrolling(input: DataGridEditorInput, scroller?: DataGridEditorScroller | null) {
const restoreScroll = preserveDataGridScrollPosition(scroller);
input.focus({ preventScroll: true });
restoreScroll();
}