fix(grid): preserve selection across column reordering

This commit is contained in:
hanxuanyu authored and GitHub committed 2026-08-14 12:57:05 +08:00
1 parent b406c0a52c
commit d63a407931
4 files changed
+189 -18

No files matched your search

@@ -45,7 +45,7 @@ describe("useDataGridSelection", () => {
selection.selectColumn(0);
selection.selectColumn(2, rowEvent({ meta: true }));
selection.remapColumnSelection([0, 1, 2], [1, 2, 0]);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedColumnIndexes.value).toEqual(new Set([1, 2]));
@@ -53,6 +53,92 @@ describe("useDataGridSelection", () => {
expect(selection.selectedColumnIndexes.value).toEqual(new Set([0, 1, 2]));
});
it("moves a single-column rectangular cell selection with its column after reordering", () => {
const selection = createSelection();
selection.selectSingleCell(1, 0);
selection.extendCellSelectionTo(2, 0);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 2, endCol: 2 });
expect(selection.cellIsSelected(1, 2)).toBe(true);
expect(selection.cellIsSelected(2, 0)).toBe(false);
});
it("moves a discrete single-column cell selection with its column after reordering", () => {
const selection = createSelection();
selection.selectedCellKeys.value = new Set(["0:0", "2:0"]);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedCellKeys.value).toEqual(new Set(["0:2", "2:2"]));
expect(selection.cellIsSelected(0, 2)).toBe(true);
expect(selection.cellIsSelected(2, 0)).toBe(false);
});
it("keeps a multi-column rectangular cell selection when its columns remain contiguous", () => {
const selection = createSelection();
selection.selectSingleCell(1, 0);
selection.extendCellSelectionTo(2, 1);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [2, 0, 1]);
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 1, endCol: 2 });
expect(selection.cellIsSelected(1, 1)).toBe(true);
expect(selection.cellIsSelected(2, 2)).toBe(true);
expect(selection.cellIsSelected(1, 0)).toBe(false);
});
it.each([
{ edge: "left", nextColumnIndexes: [1, 0, 2] },
{ edge: "right", nextColumnIndexes: [0, 2, 1] },
])("keeps the whole rectangular selection when an inner column moves to its $edge edge", ({ nextColumnIndexes }) => {
const selection = createSelection();
selection.selectSingleCell(1, 0);
selection.extendCellSelectionTo(2, 2);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], nextColumnIndexes);
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 0, endCol: 2 });
expect(selection.cellIsSelected(1, nextColumnIndexes.indexOf(1))).toBe(true);
expect(selection.selectedCellCount.value).toBe(6);
});
it("clears a multi-column rectangular cell selection when its columns split apart", () => {
const selection = createSelection();
selection.selectSingleCell(1, 0);
selection.extendCellSelectionTo(2, 1);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedRange.value).toBeNull();
expect(selection.selectedCellKeys.value).toEqual(new Set());
expect(selection.hasCellSelection.value).toBe(false);
});
it("keeps a discrete multi-column cell selection when its columns become contiguous", () => {
const selection = createSelection();
selection.selectedCellKeys.value = new Set(["0:0", "0:2", "2:0", "2:2"]);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedCellKeys.value).toEqual(new Set(["0:2", "0:1", "2:2", "2:1"]));
expect(selection.cellIsSelected(0, 1)).toBe(true);
expect(selection.cellIsSelected(2, 2)).toBe(true);
expect(selection.cellIsSelected(0, 0)).toBe(false);
});
it("clears a discrete multi-column cell selection when its columns split apart", () => {
const selection = createSelection();
selection.selectedCellKeys.value = new Set(["0:0", "0:1", "2:0", "2:1"]);
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
expect(selection.selectedRange.value).toBeNull();
expect(selection.selectedCellKeys.value).toEqual(new Set());
expect(selection.hasCellSelection.value).toBe(false);
});
it("invalidates synthetic context state for ordinary, Ctrl, and Cmd cell selection", () => {
const originalDocument = globalThis.document;
const fakeDocument = {
@@ -247,13 +247,63 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
lastClickedColumnIndex.value = colIndex;
}
function remapColumnSelection(previousColumnIndexes: readonly number[], nextColumnIndexes: readonly number[]) {
const selectedColumns = new Set([...selectedColumnIndexes.value].map((index) => previousColumnIndexes[index]).filter((index): index is number => index !== undefined));
selectedColumnIndexes.value = new Set(nextColumnIndexes.flatMap((columnIndex, index) => (selectedColumns.has(columnIndex) ? [index] : [])));
function reconcileSelectionAfterColumnReorder(previousColumnIndexes: readonly number[], nextColumnIndexes: readonly number[]) {
const lastClickedColumn = lastClickedColumnIndex.value === null ? undefined : previousColumnIndexes[lastClickedColumnIndex.value];
const nextLastClickedColumnIndex = lastClickedColumn === undefined ? -1 : nextColumnIndexes.indexOf(lastClickedColumn);
lastClickedColumnIndex.value = nextLastClickedColumnIndex >= 0 ? nextLastClickedColumnIndex : null;
if (hasColumnSelection.value) {
const selectedColumns = new Set([...selectedColumnIndexes.value].map((index) => previousColumnIndexes[index]).filter((index): index is number => index !== undefined));
selectedColumnIndexes.value = new Set(nextColumnIndexes.flatMap((columnIndex, index) => (selectedColumns.has(columnIndex) ? [index] : [])));
return;
}
const selectedCellColumnIndexes = cellSelectionColumnIndexes();
const remappedCellColumnIndexes = selectedCellColumnIndexes.map((index) => {
const selectedColumn = previousColumnIndexes[index];
return selectedColumn === undefined ? -1 : nextColumnIndexes.indexOf(selectedColumn);
});
const sortedRemappedCellColumnIndexes = [...remappedCellColumnIndexes].sort((a, b) => a - b);
const selectionRemainsContiguous = sortedRemappedCellColumnIndexes.length > 0 && sortedRemappedCellColumnIndexes.every((index, position) => index >= 0 && index === sortedRemappedCellColumnIndexes[0]! + position);
if (selectionRemainsContiguous) {
const remappedColumnIndexByPreviousIndex = new Map(selectedCellColumnIndexes.map((index, position) => [index, remappedCellColumnIndexes[position]!]));
if (selectedCellKeys.value.size > 0) {
selectedCellKeys.value = new Set(
[...selectedCellKeys.value].map((key) => {
const position = parseCellKey(key)!;
return cellKey(position.rowIndex, remappedColumnIndexByPreviousIndex.get(position.colIndex)!);
}),
);
if (selectionAnchor.value) selectionAnchor.value = { ...selectionAnchor.value, colIndex: remappedColumnIndexByPreviousIndex.get(selectionAnchor.value.colIndex)! };
if (selectionFocus.value) selectionFocus.value = { ...selectionFocus.value, colIndex: remappedColumnIndexByPreviousIndex.get(selectionFocus.value.colIndex)! };
} else if (selectionAnchor.value && selectionFocus.value) {
const selectionStartsLeft = selectionAnchor.value.colIndex <= selectionFocus.value.colIndex;
const startCol = sortedRemappedCellColumnIndexes[0]!;
const endCol = sortedRemappedCellColumnIndexes[sortedRemappedCellColumnIndexes.length - 1]!;
selectionAnchor.value = { ...selectionAnchor.value, colIndex: selectionStartsLeft ? startCol : endCol };
selectionFocus.value = { ...selectionFocus.value, colIndex: selectionStartsLeft ? endCol : startCol };
}
return;
}
clearCellSelection();
lastClickedColumnIndex.value = null;
}
function cellSelectionColumnIndexes(): number[] {
if (selectedCellKeys.value.size > 0) {
const selectedColumnIndexes = new Set<number>();
for (const key of selectedCellKeys.value) {
const position = parseCellKey(key);
if (!position) return [];
selectedColumnIndexes.add(position.colIndex);
}
return [...selectedColumnIndexes];
}
const range = selectedRange.value;
if (!range) return [];
return Array.from({ length: range.endCol - range.startCol + 1 }, (_, index) => range.startCol + index);
}
function selectAllCells() {
@@ -588,7 +638,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
selectRow,
selectColumn,
selectColumns,
remapColumnSelection,
reconcileSelectionAfterColumnReorder,
selectAllCells,
extendCellSelectionTo,
finishCellSelection,