fix(grid): size columns by sampled values and distribute spare width

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-24 12:16:20 +08:00
1 parent e8e2625801
commit 81f963fe60
3 files changed
+97 -13

No files matched your search

@@ -2282,6 +2282,7 @@ function measureColumnHeaderText(text: string): number | undefined {
}
let columnFormatterForWidth: ((columnIndex: number) => ColumnFormatterConfig | undefined) | undefined;
const gridViewportWidth = ref(0);
function columnWidthDisplayValue(value: CellValue, columnIndex: number): CellValue {
const formatter = columnFormatterForWidth?.(columnIndex);
@@ -2301,6 +2302,7 @@ const { initColumnWidths, onResizeStart, autoFitColumn, autoFitAllColumns, rende
measureHeaderText: measureColumnHeaderText,
headerMeasurementKey: columnHeaderMeasurementKey,
rowNumberWidth,
viewportWidth: gridViewportWidth,
displayValue: columnWidthDisplayValue,
});
const gridStyle = computed(() => ({
@@ -2312,7 +2314,6 @@ const gridStyle = computed(() => ({
"--dbx-table-font-size": `${tableFontSize.value}px`,
}));
const gridHorizontalScrollLeft = ref(0);
const gridViewportWidth = ref(0);
let gridScrollLeftBeforeTranspose = 0;
let gridScrollTopBeforeKeyboardTranspose: number | null = null;
let restoreGridScrollTopAfterTranspose = false;
@@ -17,6 +17,7 @@ function createResizeState(options: {
compactColumnHeaderActions?: boolean;
indexIndicatorColumnIndexes?: number[] | ReturnType<typeof ref<number[]>>;
headerTextWidth?: number;
viewportWidth?: number | ReturnType<typeof ref<number>>;
displayValue?: (value: string | number | boolean | null, columnIndex: number) => string | number | boolean | null;
}) {
const compact = ref(options.compactColumnHeaderActions ?? true);
@@ -25,6 +26,7 @@ function createResizeState(options: {
const density = ref(options.density ?? "standard");
const rows = isRef(options.rows) ? options.rows : ref(options.rows);
const indexIndicatorColumnIndexes = isRef(options.indexIndicatorColumnIndexes) ? options.indexIndicatorColumnIndexes : ref(options.indexIndicatorColumnIndexes ?? []);
const viewportWidth = isRef(options.viewportWidth) ? options.viewportWidth : ref(options.viewportWidth ?? 0);
const state = useDataGridColumnResize({
columns: computed(() => options.columns),
sourceRows: computed(() => rows.value),
@@ -36,6 +38,7 @@ function createResizeState(options: {
columnStructureSignature: computed(() => createDataGridColumnStructureSignature(options.columns, options.columnTypes)),
measureHeaderText: () => headerTextWidth.value,
headerMeasurementKey,
viewportWidth,
displayValue: options.displayValue,
});
return {
@@ -87,6 +90,52 @@ describe("useDataGridColumnResize", () => {
expect(after.length).toBe(before.length);
});
it("distributes spare viewport width across non-user-sized columns", () => {
const state = createResizeState({
columns: ["id", "name"],
rows: [[1, "a"]],
viewportWidth: 400,
});
state.initColumnWidths();
const intrinsic = state.columnWidths.value.slice();
expect(state.renderedColumnWidths.value.reduce((sum, width) => sum + width, 0) + DATA_GRID_ROW_NUM_WIDTH).toBe(400);
expect(state.renderedColumnWidths.value[0]).toBeGreaterThan(intrinsic[0]);
expect(state.renderedColumnWidths.value[1]).toBeGreaterThan(intrinsic[1]);
state.onResizeStart(0, new MouseEvent("mousedown", { clientX: 100, cancelable: true }));
document.dispatchEvent(new MouseEvent("mouseup", { clientX: 100 }));
expect(state.renderedColumnWidths.value[0]).toBeGreaterThan(intrinsic[0]);
expect(state.renderedColumnWidths.value[1]).toBeGreaterThan(intrinsic[1]);
});
it("tracks the cursor while dragging under spare width and stores the final width without stale rendering", async () => {
const state = createResizeState({
columns: ["id", "name"],
rows: [[1, "a"]],
viewportWidth: 1000,
});
state.initColumnWidths();
const intrinsic = state.columnWidths.value.slice();
const startWidth = state.renderedColumnWidths.value[0] ?? 0;
// The grabbed column initially renders wider than its stored intrinsic width.
expect(startWidth).toBeGreaterThan(intrinsic[0] ?? 0);
state.onResizeStart(0, new MouseEvent("mousedown", { clientX: 100, cancelable: true }));
// Grabbing the handle must not shift the rendered edge.
expect(state.renderedColumnWidths.value[0]).toBe(startWidth);
document.dispatchEvent(new MouseEvent("mousemove", { clientX: 150 }));
await new Promise((resolve) => requestAnimationFrame(resolve));
// Mid-drag the rendered width is exactly startWidth + delta: the resizing
// column is not re-stretched with the remaining surplus.
expect(state.renderedColumnWidths.value[0]).toBe(startWidth + 50);
document.dispatchEvent(new MouseEvent("mouseup", { clientX: 150 }));
// After mouseup the rendered width equals the stored user width (no stale render).
expect(state.columnWidths.value[0]).toBe(startWidth + 50);
expect(state.renderedColumnWidths.value[0]).toBe(state.columnWidths.value[0]);
});
it("keeps auto fitted widths when the grid re-initialises for the same columns", () => {
const longText = "y".repeat(220);
const state = createResizeState({
@@ -112,13 +161,17 @@ describe("useDataGridColumnResize", () => {
expect(state.renderedColumnWidths.value).toEqual([]);
});
it("keeps compact query result columns at content width instead of filling the viewport", () => {
it("keeps compact query result columns at content width when no viewport width is reported", () => {
// viewportWidth unset (0) means no spare width is distributed, so compact
// columns keep their content width; with a real viewport they fill it — see
// "distributes spare viewport width across non-user-sized columns" above.
const state = createResizeState({
columns: ["id", "user_id"],
rows: [
[1, 10],
[2, 20],
],
viewportWidth: 0,
});
state.initColumnWidths();
@@ -1,4 +1,4 @@
import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
import { ref, shallowRef, computed, watch, type ComputedRef, type Ref } from "vue";
import { calculateDataGridColumnWidth, DATA_GRID_AUTO_FIT_VALUE_TEXT_LIMIT, DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, DATA_GRID_COL_MIN_WIDTH, COLUMN_WIDTH_DENSITY_PRESETS, sampleDataGridColumnValues } from "@/lib/dataGrid/dataGridColumnWidth";
import { createDataGridColumnMeasurementSignature, loadDataGridColumnWidthState, removeDataGridColumnWidthState, saveDataGridColumnWidthState } from "@/lib/dataGrid/dataGridColumnWidthState";
import type { ColumnWidthDensity } from "@/stores/settingsStore";
@@ -42,6 +42,7 @@ export interface UseDataGridColumnResizeOptions {
measureHeaderText?: (text: string) => number | undefined;
headerMeasurementKey?: Ref<unknown>;
rowNumberWidth?: Ref<number> | ComputedRef<number>;
viewportWidth?: Ref<number> | ComputedRef<number>;
displayValue?: (value: CellValue, columnIndex: number) => CellValue;
}
@@ -51,7 +52,9 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
const columnWidths = ref<number[]>([]);
let isResizing = false;
let previousColumnIndexes: number[] = [];
let userSizedColumnIndexes = new Set<number>();
// Replaced wholesale instead of mutated so reactive consumers (renderedColumnWidths)
// re-run when a column becomes user-sized and stops receiving spare width.
const userSizedColumnIndexes = shallowRef<Set<number>>(new Set());
function columnWidthStateIdentity() {
return {
@@ -62,7 +65,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
}
function persistColumnWidths() {
saveDataGridColumnWidthState(columnWidthStateIdentity(), previousColumnIndexes, columnWidths.value, userSizedColumnIndexes);
saveDataGridColumnWidthState(columnWidthStateIdentity(), previousColumnIndexes, columnWidths.value, userSizedColumnIndexes.value);
}
function sampleColumnValues(visibleColIdx: number): CellValue[] {
@@ -96,7 +99,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
const next = columnWidths.value.slice();
for (let colIdx = 0; colIdx < columns.value.length; colIdx++) {
const actualColIdx = columnIndexes.value[colIdx];
if (actualColIdx === undefined || userSizedColumnIndexes.has(actualColIdx)) continue;
if (actualColIdx === undefined || userSizedColumnIndexes.value.has(actualColIdx)) continue;
const needed = neededWidths[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
if (needed > (next[colIdx] ?? 0)) {
next[colIdx] = needed;
@@ -110,11 +113,14 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
function markColumnUserSized(visibleColIdx: number) {
const actualColIdx = columnIndexes.value[visibleColIdx];
if (actualColIdx !== undefined) userSizedColumnIndexes.add(actualColIdx);
if (actualColIdx === undefined || userSizedColumnIndexes.value.has(actualColIdx)) return;
const next = new Set(userSizedColumnIndexes.value);
next.add(actualColIdx);
userSizedColumnIndexes.value = next;
}
function initColumnWidths(force = false) {
if (force) userSizedColumnIndexes.clear();
if (force) userSizedColumnIndexes.value = new Set();
const previousWidthsByColumnIndex = new Map<number, number>();
previousColumnIndexes.forEach((columnIndex, visibleIndex) => {
const width = columnWidths.value[visibleIndex];
@@ -122,7 +128,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
});
const nextColumnIndexes = [...columnIndexes.value];
const cachedState = !force && previousColumnIndexes.length === 0 ? loadDataGridColumnWidthState(columnWidthStateIdentity(), nextColumnIndexes) : undefined;
if (cachedState) userSizedColumnIndexes = new Set(cachedState.userSizedColumnIndexes);
if (cachedState) userSizedColumnIndexes.value = new Set(cachedState.userSizedColumnIndexes);
const currentNeededWidths = neededColumnWidths.value ?? [];
const columnCount = columns.value?.length ?? 0;
if (force || columnWidths.value.length !== columnCount || previousColumnIndexes.join("\0") !== nextColumnIndexes.join("\0")) {
@@ -144,7 +150,15 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
event.preventDefault();
isResizing = true;
const startX = event.clientX;
const startWidth = columnWidths.value[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
// The drag base is the width the user grabbed, which may include spare width
// distributed at render time. Freezing that snapshot as the stored width and
// marking the column user-sized right away keeps the column out of the
// flexible set for the whole drag, so every frame renders startWidth + delta
// as-is instead of re-stretching it with the remaining surplus, and the
// stored (persisted) width matches what the cursor moved.
const startWidth = renderedColumnWidths.value[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
markColumnUserSized(colIdx);
columnWidths.value[colIdx] = startWidth;
let pendingClientX = startX;
let resizeFrame = 0;
@@ -232,10 +246,26 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
persistColumnWidths();
}
const renderedColumnWidths = computed(() => columnWidths.value.slice());
const resolvedRowNumberWidth = computed(() => options.rowNumberWidth?.value ?? DATA_GRID_ROW_NUM_WIDTH);
const renderedColumnWidths = computed(() => {
const widths = columnWidths.value.slice();
const viewportWidth = options.viewportWidth?.value ?? 0;
const surplus = viewportWidth - resolvedRowNumberWidth.value - widths.reduce((sum, width) => sum + width, 0);
if (surplus <= 0) return widths;
const flexibleIndexes = widths.flatMap((_, visibleIndex) => {
const actualIndex = columnIndexes.value[visibleIndex];
return actualIndex !== undefined && !userSizedColumnIndexes.value.has(actualIndex) ? [visibleIndex] : [];
});
if (flexibleIndexes.length === 0) return widths;
const base = Math.floor(surplus / flexibleIndexes.length);
let remainder = surplus - base * flexibleIndexes.length;
flexibleIndexes.forEach((index) => {
widths[index] += base + (remainder-- > 0 ? 1 : 0);
});
return widths;
});
const totalWidth = computed(() => renderedColumnWidths.value.reduce((a, b) => a + b, 0) + resolvedRowNumberWidth.value);
const columnVars = computed(() => {
@@ -259,7 +289,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
watch([() => options.cacheKey?.value, options.columnStructureSignature], () => {
columnWidths.value = [];
previousColumnIndexes = [];
userSizedColumnIndexes.clear();
userSizedColumnIndexes.value = new Set();
initColumnWidths();
});
watch([density, compactColumnHeaderActions, () => options.headerMeasurementKey?.value], () => {