fix(grid): improve transposed selection and navigation
This commit is contained in:
parent
2ec9328eea
commit
96038d2b8a
|
|
@ -5890,6 +5890,18 @@ function currentSelectedCellPosition() {
|
|||
}
|
||||
|
||||
function scrollCellIntoView(rowIndex: number, colIndex: number) {
|
||||
if (isTransposeMode.value) {
|
||||
nextTick(() => {
|
||||
const scroller = transposeScrollRef.value;
|
||||
if (scroller && !(scroller instanceof HTMLElement)) {
|
||||
(scroller as { scrollToItem?: (index: number) => void }).scrollToItem?.(colIndex);
|
||||
} else if (scroller instanceof HTMLElement) {
|
||||
scroller.scrollTop = colIndex * 30;
|
||||
}
|
||||
scrollTransposeRecordIntoView(rowIndex);
|
||||
});
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
scrollGridColumnIntoView(colIndex);
|
||||
if (useCanvasGridRows.value) {
|
||||
|
|
@ -5961,9 +5973,22 @@ function scrollGridRowIntoView(rowIndex: number) {
|
|||
});
|
||||
}
|
||||
|
||||
function currentTransposeRequestedRowIndex(): number {
|
||||
function selectedTransposeRowIndex(): number | null {
|
||||
const position = currentSelectedCellPosition();
|
||||
if (position) return position.rowIndex;
|
||||
const lastSelectedRowIndex = selection.lastClickedRowIndex.value;
|
||||
if (lastSelectedRowIndex !== null) {
|
||||
const item = displayItemAt(lastSelectedRowIndex);
|
||||
if (item && selectedRowIds.value.has(item.id)) return lastSelectedRowIndex;
|
||||
}
|
||||
const selectedRowIndex = displayRowRefs.value.findIndex((row) => selectedRowIds.value.has(row.id));
|
||||
if (selectedRowIndex >= 0) return selectedRowIndex;
|
||||
return null;
|
||||
}
|
||||
|
||||
function currentTransposeRequestedRowIndex(): number {
|
||||
const selectedRowIndex = selectedTransposeRowIndex();
|
||||
if (selectedRowIndex !== null) return selectedRowIndex;
|
||||
if (transposeRowIndex.value !== null) return transposeRowIndex.value;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -6191,13 +6216,23 @@ async function onGridKeydown(event: KeyboardEvent) {
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowLeft" && moveTransposeRecordSelection(-1)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowRight" && moveTransposeRecordSelection(1)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
if (isTransposeMode.value) {
|
||||
if (event.key === "ArrowUp" && moveSelectedCell(0, -1)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowDown" && moveSelectedCell(0, 1)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowLeft" && (moveSelectedCell(-1, 0) || moveTransposeRecordSelection(-1))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowRight" && (moveSelectedCell(1, 0) || moveTransposeRecordSelection(1))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.key === "ArrowUp" && moveSelectedCell(-1, 0)) {
|
||||
event.preventDefault();
|
||||
|
|
@ -6374,6 +6409,13 @@ function copyColumnDetailFieldValue(field: DataGridCellDetail) {
|
|||
|
||||
const transposeRecordWidths = ref<number[]>([]);
|
||||
const transposeManualRecordWidthIndexes = ref(new Set<number>());
|
||||
const transposeRecordOffsets = computed(() => {
|
||||
const offsets = [0];
|
||||
for (let index = 0; index < displayRowCount.value; index += 1) {
|
||||
offsets.push(offsets[index] + getTransposeRecordWidth(index));
|
||||
}
|
||||
return offsets;
|
||||
});
|
||||
|
||||
function calcTransposeRecordWidth(recordIndex: number): number {
|
||||
const item = displayItemAt(recordIndex);
|
||||
|
|
@ -6424,6 +6466,7 @@ const transposeRecordWindow = computed(() =>
|
|||
viewportWidth: transposeViewportWidth.value,
|
||||
pinnedWidth: transposePinnedWidth.value,
|
||||
recordWidth: estimatedTransposeRecordWidth(),
|
||||
recordOffsets: transposeRecordOffsets.value,
|
||||
overscan: 2,
|
||||
}),
|
||||
);
|
||||
|
|
@ -6487,6 +6530,8 @@ function scrollTransposeRecordIntoView(rowIndex: number) {
|
|||
viewportWidth: el.clientWidth,
|
||||
pinnedWidth: transposePinnedWidth.value,
|
||||
recordWidth: estimatedTransposeRecordWidth(),
|
||||
recordOffsets: transposeRecordOffsets.value,
|
||||
currentScrollLeft: el.scrollLeft,
|
||||
});
|
||||
updateTransposeViewport();
|
||||
});
|
||||
|
|
@ -6594,10 +6639,12 @@ function openContextTranspose() {
|
|||
return;
|
||||
}
|
||||
if (!contextCell.value) return;
|
||||
const selectedRowIndex = selectedTransposeRowIndex();
|
||||
const requestedRowIndex = selectedRowIds.value.size === 1 && selectedRowIndex !== null ? selectedRowIndex : contextCell.value.rowIndex;
|
||||
const next = nextContextTransposeState({
|
||||
showTranspose: showTranspose.value,
|
||||
transposeRowIndex: transposeRowIndex.value,
|
||||
requestedRowIndex: contextCell.value.rowIndex,
|
||||
requestedRowIndex,
|
||||
rowIds: displayRowRefs.value.map((ref) => ref.id),
|
||||
selectedRowIds: selectedRowIds.value,
|
||||
selectedRange: selectedRange.value,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { averageTransposeRecordWidth, calculateTransposeRecordWidth, defaultTransposeRecordWidth, minTransposeFieldWidth, shouldAutoTransposeSingleRow, transposeFieldWidth, transposeRecordWidthsForDensity, visibleTransposeRecordWindow } from "@/lib/dataGrid/dataGridTranspose";
|
||||
import {
|
||||
averageTransposeRecordWidth,
|
||||
calculateTransposeRecordWidth,
|
||||
defaultTransposeRecordWidth,
|
||||
minTransposeFieldWidth,
|
||||
shouldAutoTransposeSingleRow,
|
||||
transposeAnchorRowIndex,
|
||||
transposeFieldWidth,
|
||||
transposeScrollLeftForRecord,
|
||||
transposeRecordWidthsForDensity,
|
||||
visibleTransposeRecordWindow,
|
||||
} from "@/lib/dataGrid/dataGridTranspose";
|
||||
|
||||
describe("single-row automatic transpose", () => {
|
||||
it("only opens for enabled multi-column results that are not preserving a manual transpose", () => {
|
||||
|
|
@ -12,6 +23,21 @@ describe("single-row automatic transpose", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("transpose row anchor", () => {
|
||||
it("keeps the requested row when multiple rows or cells are selected", () => {
|
||||
const rowIds = [1, 2, 3, 4];
|
||||
|
||||
expect(
|
||||
transposeAnchorRowIndex({
|
||||
requestedRowIndex: 3,
|
||||
rowIds,
|
||||
selectedRowIds: new Set([1, 4]),
|
||||
selectedRange: { startRow: 0, endRow: 3, startCol: 0, endCol: 1 },
|
||||
}),
|
||||
).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dataGridTranspose density widths", () => {
|
||||
it("uses the shared density preset for record and field widths", () => {
|
||||
const values = ["x".repeat(40)];
|
||||
|
|
@ -78,3 +104,46 @@ describe("dataGridTranspose density widths", () => {
|
|||
expect(averageTransposeRecordWidth([], "compact")).toBe(defaultTransposeRecordWidth("compact"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("transpose record scrolling", () => {
|
||||
it("uses the scroll position after the sticky field for virtualization", () => {
|
||||
expect(
|
||||
visibleTransposeRecordWindow({
|
||||
totalRecords: 3,
|
||||
scrollLeft: 500,
|
||||
viewportWidth: 300,
|
||||
pinnedWidth: 100,
|
||||
recordWidth: 230,
|
||||
recordOffsets: [0, 500, 596, 692],
|
||||
overscan: 0,
|
||||
}),
|
||||
).toEqual({ start: 1, end: 3, beforeWidth: 500, afterWidth: 0 });
|
||||
});
|
||||
|
||||
it("uses actual record widths and nearest scrolling", () => {
|
||||
const recordOffsets = [0, 500, 596, 692];
|
||||
|
||||
expect(
|
||||
transposeScrollLeftForRecord({
|
||||
recordIndex: 2,
|
||||
totalRecords: 3,
|
||||
viewportWidth: 300,
|
||||
pinnedWidth: 100,
|
||||
recordWidth: 230,
|
||||
recordOffsets,
|
||||
currentScrollLeft: 0,
|
||||
}),
|
||||
).toBe(492);
|
||||
expect(
|
||||
transposeScrollLeftForRecord({
|
||||
recordIndex: 1,
|
||||
totalRecords: 3,
|
||||
viewportWidth: 300,
|
||||
pinnedWidth: 100,
|
||||
recordWidth: 230,
|
||||
recordOffsets,
|
||||
currentScrollLeft: 450,
|
||||
}),
|
||||
).toBe(450);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export interface TransposeRecordWindowOptions {
|
|||
viewportWidth: number;
|
||||
pinnedWidth: number;
|
||||
recordWidth: number;
|
||||
recordOffsets?: readonly number[];
|
||||
overscan?: number;
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +160,8 @@ export interface TransposeScrollLeftOptions {
|
|||
viewportWidth: number;
|
||||
pinnedWidth: number;
|
||||
recordWidth: number;
|
||||
recordOffsets?: readonly number[];
|
||||
currentScrollLeft?: number;
|
||||
}
|
||||
|
||||
export interface TransposeRecordIndexesForModeOptions {
|
||||
|
|
@ -269,8 +272,25 @@ export function visibleTransposeRecordWindow(options: TransposeRecordWindowOptio
|
|||
}
|
||||
|
||||
const overscan = options.overscan ?? 2;
|
||||
const recordScrollLeft = Math.max(0, options.scrollLeft - options.pinnedWidth);
|
||||
const recordScrollLeft = Math.max(0, options.scrollLeft);
|
||||
const recordViewportWidth = Math.max(0, options.viewportWidth - options.pinnedWidth);
|
||||
if (options.recordOffsets?.length === options.totalRecords + 1) {
|
||||
const offsets = options.recordOffsets;
|
||||
const firstVisible = Math.max(
|
||||
0,
|
||||
offsets.findIndex((_offset, index) => index < options.totalRecords && offsets[index + 1] > recordScrollLeft),
|
||||
);
|
||||
const firstAfterViewport = offsets.findIndex((offset, index) => index > firstVisible && offset >= recordScrollLeft + recordViewportWidth);
|
||||
const start = Math.max(0, firstVisible - overscan);
|
||||
const end = Math.min(options.totalRecords, (firstAfterViewport < 0 ? options.totalRecords : firstAfterViewport) + overscan);
|
||||
const totalWidth = offsets[options.totalRecords];
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
beforeWidth: offsets[start],
|
||||
afterWidth: Math.max(0, totalWidth - offsets[end]),
|
||||
};
|
||||
}
|
||||
const start = Math.max(0, Math.floor(recordScrollLeft / options.recordWidth) - overscan);
|
||||
const end = Math.min(options.totalRecords, Math.ceil((recordScrollLeft + recordViewportWidth) / options.recordWidth) + overscan + 1);
|
||||
|
||||
|
|
@ -290,18 +310,7 @@ export function transposeRecordIndexesForMode(options: TransposeRecordIndexesFor
|
|||
}
|
||||
|
||||
export function transposeAnchorRowIndex(options: TransposeAnchorOptions): number {
|
||||
const requestedRowId = options.rowIds[options.requestedRowIndex];
|
||||
if (requestedRowId !== undefined && options.selectedRowIds.size > 1 && options.selectedRowIds.has(requestedRowId)) {
|
||||
const firstSelectedIndex = options.rowIds.findIndex((rowId) => options.selectedRowIds.has(rowId));
|
||||
if (firstSelectedIndex >= 0) return firstSelectedIndex;
|
||||
}
|
||||
|
||||
const range = options.selectedRange;
|
||||
if (range && range.startRow !== range.endRow && options.requestedRowIndex >= range.startRow && options.requestedRowIndex <= range.endRow) {
|
||||
return range.startRow;
|
||||
}
|
||||
|
||||
return options.requestedRowIndex;
|
||||
return Math.max(0, Math.min(options.rowIds.length - 1, options.requestedRowIndex));
|
||||
}
|
||||
|
||||
export function transposeFieldWidth(columns: string[], options: TransposeFieldWidthOptions = {}): number {
|
||||
|
|
@ -317,8 +326,15 @@ export function transposeFieldWidth(columns: string[], options: TransposeFieldWi
|
|||
|
||||
export function transposeScrollLeftForRecord(options: TransposeScrollLeftOptions): number {
|
||||
if (options.recordWidth <= 0 || options.totalRecords <= 0) return 0;
|
||||
const desired = Math.max(0, options.recordIndex) * options.recordWidth;
|
||||
const totalWidth = options.pinnedWidth + options.totalRecords * options.recordWidth;
|
||||
const recordIndex = Math.max(0, Math.min(options.totalRecords - 1, options.recordIndex));
|
||||
const offsets = options.recordOffsets?.length === options.totalRecords + 1 ? options.recordOffsets : undefined;
|
||||
const recordStart = offsets ? offsets[recordIndex] : recordIndex * options.recordWidth;
|
||||
const recordEnd = offsets ? offsets[recordIndex + 1] : recordStart + options.recordWidth;
|
||||
const recordsWidth = offsets ? offsets[options.totalRecords] : options.totalRecords * options.recordWidth;
|
||||
const recordViewportWidth = Math.max(0, options.viewportWidth - options.pinnedWidth);
|
||||
const currentScrollLeft = Math.max(0, options.currentScrollLeft ?? recordStart);
|
||||
const desired = recordStart < currentScrollLeft ? recordStart : recordEnd > currentScrollLeft + recordViewportWidth ? recordEnd - recordViewportWidth : currentScrollLeft;
|
||||
const totalWidth = options.pinnedWidth + recordsWidth;
|
||||
const maxScrollLeft = Math.max(0, totalWidth - options.viewportWidth);
|
||||
return Math.min(desired, maxScrollLeft);
|
||||
return Math.max(0, Math.min(desired, maxScrollLeft));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ test("row number double click closes transpose for the same row", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("context menu transpose closes when invoked for the current anchor row", () => {
|
||||
test("context menu transpose keeps the requested row when multiple rows are selected", () => {
|
||||
assert.deepEqual(
|
||||
nextContextTransposeState({
|
||||
showTranspose: true,
|
||||
|
|
@ -47,8 +47,8 @@ test("context menu transpose closes when invoked for the current anchor row", ()
|
|||
selectedRange: null,
|
||||
}),
|
||||
{
|
||||
showTranspose: false,
|
||||
transposeRowIndex: null,
|
||||
showTranspose: true,
|
||||
transposeRowIndex: 3,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -256,15 +256,15 @@ test("calculates a horizontal record window with spacer widths", () => {
|
|||
overscan: 1,
|
||||
}),
|
||||
{
|
||||
start: 1,
|
||||
end: 7,
|
||||
beforeWidth: 160,
|
||||
afterWidth: 14880,
|
||||
start: 3,
|
||||
end: 9,
|
||||
beforeWidth: 480,
|
||||
afterWidth: 14560,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the first selected row as the transpose anchor when context row is inside row selection", () => {
|
||||
test("keeps the requested row as the transpose anchor when context row is inside row selection", () => {
|
||||
assert.equal(
|
||||
transposeAnchorRowIndex({
|
||||
requestedRowIndex: 3,
|
||||
|
|
@ -272,11 +272,11 @@ test("uses the first selected row as the transpose anchor when context row is in
|
|||
selectedRowIds: new Set([12, 13, 14]),
|
||||
selectedRange: null,
|
||||
}),
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the first selected cell range row as the transpose anchor when context row is inside range", () => {
|
||||
test("keeps the requested row as the transpose anchor when context row is inside range", () => {
|
||||
assert.equal(
|
||||
transposeAnchorRowIndex({
|
||||
requestedRowIndex: 5,
|
||||
|
|
@ -284,7 +284,7 @@ test("uses the first selected cell range row as the transpose anchor when contex
|
|||
selectedRowIds: new Set(),
|
||||
selectedRange: { startRow: 2, endRow: 5, startCol: 0, endCol: 2 },
|
||||
}),
|
||||
2,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue