From 823c26031623440ea3348e38e89743a6c6bc8e3d Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 11 Aug 2026 00:35:49 +0800 Subject: [PATCH] fix(grid): stop restored selection from extending on hover Co-authored-by: zipg <4047349+zipg@users.noreply.github.com> --- apps/desktop/src/components/grid/DataGrid.vue | 18 +++--------- .../__tests__/useDataGridSelection.spec.ts | 28 +++++++++++++++++++ .../src/composables/useDataGridSelection.ts | 17 +++++++++++ .../dataGridSelectionPersistence.spec.ts | 24 +--------------- .../dataGrid/dataGridSelectionPersistence.ts | 8 ------ 5 files changed, 50 insertions(+), 45 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 04d496ccfe..406bac024f 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -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; diff --git a/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts index 56de10d7dd..b559d3d2d8 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts @@ -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 }); + } + }); }); diff --git a/apps/desktop/src/composables/useDataGridSelection.ts b/apps/desktop/src/composables/useDataGridSelection.ts index 1922d945b4..06e033e025 100644 --- a/apps/desktop/src/composables/useDataGridSelection.ts +++ b/apps/desktop/src/composables/useDataGridSelection.ts @@ -30,6 +30,13 @@ export interface UseDataGridSelectionOptions { runtimeScope?: DataGridRuntimeScope; } +interface RestoredCellSelectionState { + anchor?: CellPosition; + focus?: CellPosition; + cellKeys?: ReadonlySet; + 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, diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridSelectionPersistence.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridSelectionPersistence.spec.ts index 598c7d3dcc..07b449190b 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridSelectionPersistence.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridSelectionPersistence.spec.ts @@ -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 { 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(); - }); }); diff --git a/apps/desktop/src/lib/dataGrid/dataGridSelectionPersistence.ts b/apps/desktop/src/lib/dataGrid/dataGridSelectionPersistence.ts index 438aa72fda..e3536fffc3 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridSelectionPersistence.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridSelectionPersistence.ts @@ -53,14 +53,6 @@ export type RestoredDataGridSelection = | { kind: "range"; anchor: CellPosition; focus: CellPosition; selectingAll: boolean; scrollRowIndex: number } | { kind: "cells"; cellKeys: Set; 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[];