fix(grid): stop restored selection from extending on hover

Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
zipgandzipg authored and GitHub committed 2026-08-11 00:35:49 +08:00
1 parent 04580355d0
commit 823c260316
5 files changed
+50 -45

No files matched your search

+4 -14
View File
@@ -178,7 +178,7 @@ import { buildDataGridColumnLookupItems, dataGridColumnCommentFor, filterDataGri
import { uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
import { dataGridColumnLayoutScopeKey, TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
import { summarizeSelection } from "@/lib/dataGrid/gridSelection";
import { captureDataGridSelection, reactivatesCellSelectionAfterRestore, restoreDataGridSelection, type PersistedDataGridSelection } from "@/lib/dataGrid/dataGridSelectionPersistence";
import { captureDataGridSelection, restoreDataGridSelection, type PersistedDataGridSelection } from "@/lib/dataGrid/dataGridSelectionPersistence";
import { dataGridFrameCoversRow, dataGridSelectionEdgeMask, dataGridSelectionFrameKindAtCell, dataGridSelectionUsesOuterFrame, resolveDataGridSelectionFrames } from "@/lib/dataGrid/dataGridSelectionFrames";
import {
createDataGridCellContextMenuItems,
@@ -3874,6 +3874,7 @@ const {
selectionFocus,
finishCellSelection,
extendCellSelection,
restoreCellSelectionState,
cellIsSelected,
columnIsSelected,
selectedRangeStart,
@@ -3924,20 +3925,9 @@ function restoreSelectionAfterRefresh(snapshot: PersistedDataGridSelection) {
} else if (restored.kind === "columns") {
selectedColumnIndexes.value = new Set(restored.columnIndexes);
} else if (restored.kind === "range") {
selectionAnchor.value = restored.anchor;
selectionFocus.value = restored.focus;
isSelectingAll.value = restored.selectingAll;
restoreCellSelectionState({ anchor: restored.anchor, focus: restored.focus, selectingAll: restored.selectingAll });
} else {
selectedCellKeys.value = restored.cellKeys;
}
// 恢复 range/离散单元格选区后重新激活 cell-selection 拖拽状态机:refresh watcher
// 在 restore 前调的 clearCellSelection 已把 isSelectingCells 置 false,而它仅在
// beginCellSelection(鼠标按下)时才重新置 true。不在此补偿,则恢复的高亮虽能显示,
// 但 extendCellSelection/handleSelectionPointerMove 的 isSelectingCells 守卫会挡住
// 拖拽扩展,用户必须重新点一次单元格。行选/列选是独立状态机,不应激活。
if (reactivatesCellSelectionAfterRestore(restored)) {
isSelectingCells.value = true;
restoreCellSelectionState({ cellKeys: restored.cellKeys });
}
if (restored.kind === "columns") return;
@@ -271,4 +271,32 @@ describe("useDataGridSelection", () => {
Object.defineProperty(globalThis, "document", { configurable: true, value: originalDocument });
}
});
it("keeps a restored range stable until a new pointer gesture begins", () => {
const originalDocument = globalThis.document;
const fakeDocument = {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as unknown as Document;
Object.defineProperty(globalThis, "document", { configurable: true, value: fakeDocument });
const selection = createSelection();
try {
selection.restoreCellSelectionState({
anchor: { rowIndex: 0, colIndex: 0 },
focus: { rowIndex: 1, colIndex: 1 },
});
selection.extendCellSelection(3, 2);
expect(selection.isSelectingCells.value).toBe(false);
expect(selection.selectedRange.value).toEqual({ startRow: 0, endRow: 1, startCol: 0, endCol: 1 });
selection.beginCellSelection(1, 1, { button: 0, clientX: 0, clientY: 0, preventDefault() {} } as MouseEvent);
selection.extendCellSelection(3, 2);
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 3, startCol: 1, endCol: 2 });
} finally {
selection.finishCellSelection();
Object.defineProperty(globalThis, "document", { configurable: true, value: originalDocument });
}
});
});
@@ -30,6 +30,13 @@ export interface UseDataGridSelectionOptions {
runtimeScope?: DataGridRuntimeScope;
}
interface RestoredCellSelectionState {
anchor?: CellPosition;
focus?: CellPosition;
cellKeys?: ReadonlySet<string>;
selectingAll?: boolean;
}
const AUTO_SCROLL_EDGE_SIZE = 40;
const AUTO_SCROLL_MAX_SPEED = 28;
type RowSelectionOperation = "replace" | "add" | "remove";
@@ -406,6 +413,15 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
stopSelectionAutoScroll();
}
function restoreCellSelectionState(state: RestoredCellSelectionState) {
finishCellSelection();
selectedColumnIndexes.value = new Set();
selectedCellKeys.value = new Set(state.cellKeys ?? []);
selectionAnchor.value = state.anchor ?? null;
selectionFocus.value = state.focus ?? null;
isSelectingAll.value = state.selectingAll ?? false;
}
function stopSelectionAutoScroll() {
if (!selectionAutoScrollFrame) return;
cancelAnimationFrame(selectionAutoScrollFrame);
@@ -566,6 +582,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
selectAllCells,
extendCellSelectionTo,
finishCellSelection,
restoreCellSelectionState,
beginCellSelection,
extendCellSelection,
cellIsSelected,
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { captureDataGridSelection, reactivatesCellSelectionAfterRestore, restoreDataGridSelection, type CaptureDataGridSelectionOptions } from "@/lib/dataGrid/dataGridSelectionPersistence";
import { captureDataGridSelection, restoreDataGridSelection, type CaptureDataGridSelectionOptions } from "@/lib/dataGrid/dataGridSelectionPersistence";
function baseOptions(overrides: Partial<CaptureDataGridSelectionOptions> = {}): CaptureDataGridSelectionOptions {
return {
@@ -256,26 +256,4 @@ describe("data grid selection persistence", () => {
).toBeNull();
expect(restore(fallbackSnapshot, { columns: ["id", "name"], rows: [[2, "Grace"]], visibleColumnIndexes: [0, 1], displayItems: [{ id: 0, sourceIndex: 0 }] })).toBeNull();
});
it("reactivates the cell-selection drag state machine only for range and sparse-cell restores", () => {
// 刷新后恢复 range/离散单元格选区时,组件层需重新置 isSelectingCells=true,
// 否则 clearCellSelection 已将其清零、拖拽扩展会被守卫挡住(见 extendCellSelection/
// handleSelectionPointerMove)。行选/列选是独立状态机,不应激活 cell selection。
const rangeSnapshot = captureDataGridSelection(baseOptions({ selectionAnchor: { rowIndex: 0, colIndex: 0 }, selectionFocus: { rowIndex: 2, colIndex: 1 } }))!;
const cellsSnapshot = captureDataGridSelection(baseOptions({ selectedCellKeys: new Set(["0:0", "2:2"]) }))!;
const rowsSnapshot = captureDataGridSelection(baseOptions({ selectedRowIds: new Set([0, 2]), lastClickedRowIndex: 2 }))!;
const columnsSnapshot = captureDataGridSelection(baseOptions({ selectedColumnIndexes: new Set([0, 2]) }))!;
expect(reactivatesCellSelectionAfterRestore(restore(rangeSnapshot)!)).toBe(true);
expect(reactivatesCellSelectionAfterRestore(restore(cellsSnapshot)!)).toBe(true);
expect(reactivatesCellSelectionAfterRestore(restore(rowsSnapshot)!)).toBe(false);
expect(reactivatesCellSelectionAfterRestore(restore(columnsSnapshot)!)).toBe(false);
});
it("does not signal reactivation when the restored range falls back to null", () => {
// 行消失/被过滤导致 restore 返回 null 时,组件层不会走到激活分支;
// 这里锁定 restore 仍返回 null,确保不会误把失败恢复当作可扩展选区。
const snapshot = captureDataGridSelection(baseOptions({ selectionAnchor: { rowIndex: 1, colIndex: 1 }, selectionFocus: { rowIndex: 1, colIndex: 1 } }))!;
expect(restore(snapshot, { rows: [[1, 10, "Ada"]], displayItems: [{ id: 0, sourceIndex: 0 }] })).toBeNull();
});
});
@@ -53,14 +53,6 @@ export type RestoredDataGridSelection =
| { kind: "range"; anchor: CellPosition; focus: CellPosition; selectingAll: boolean; scrollRowIndex: number }
| { kind: "cells"; cellKeys: Set<string>; scrollRowIndex: number };
// 刷新后恢复 range/离散单元格选区时,需要重新激活 cell-selection 拖拽状态机
// (isSelectingCells)。否则 restore 前调用的 clearCellSelection 已将其置 false,
// 而 beginCellSelection 才会重新置 true,导致用户必须重新点一次单元格才能拖拽扩展
// 刚恢复的选区。行选/列选是独立状态机,不应激活 cell selection。
export function reactivatesCellSelectionAfterRestore(restored: RestoredDataGridSelection): boolean {
return restored.kind === "range" || restored.kind === "cells";
}
export interface RestoreDataGridSelectionOptions {
snapshot: PersistedDataGridSelection;
columns: readonly string[];