fix(grid): keep column names fully visible

This commit is contained in:
t8y2
2026-07-17 08:39:16 +08:00
parent 6193f8b860
commit 51f87976b4
5 changed files with 84 additions and 15 deletions
@@ -1774,12 +1774,26 @@ function scrollToColumnIndex(columnIndex: number) {
// --- Column resize composable ---
const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWidthDensity);
let columnHeaderMeasureContext: CanvasRenderingContext2D | null | undefined;
function measureColumnHeaderText(text: string): number | undefined {
if (typeof document === "undefined") return undefined;
if (columnHeaderMeasureContext === undefined) columnHeaderMeasureContext = document.createElement("canvas").getContext("2d");
if (!columnHeaderMeasureContext) return undefined;
// Match the rendered semibold header font instead of estimating proportional glyphs by character count.
const fontFamily = getComputedStyle(gridRef.value ?? document.body).fontFamily || "sans-serif";
columnHeaderMeasureContext.font = `600 ${tableFontSize.value}px ${fontFamily}`;
return Math.ceil(columnHeaderMeasureContext.measureText(text).width);
}
const { initColumnWidths, onResizeStart, autoFitColumn, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({
columns: visibleColumns,
sourceRows: computed(() => props.result.rows),
columnIndexes: visibleColumnIndexes,
density: columnWidthDensity,
compactColumnHeaderActions,
measureHeaderText: measureColumnHeaderText,
headerMeasurementKey: tableFontSize,
});
const gridStyle = computed(() => ({
...columnVars.value,
@@ -3,20 +3,28 @@ import { describe, expect, it } from "vitest";
import { DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, DATA_GRID_COL_MIN_WIDTH } from "@/lib/dataGrid/dataGridColumnWidth";
import { DATA_GRID_ROW_NUM_WIDTH, resizeDataGridColumnWidth, useDataGridColumnResize } from "@/composables/useDataGridColumnResize";
function createResizeState(options: { columns: string[]; rows: Array<Array<string | number | boolean | null>>; columnIndexes?: number[]; density?: "compact" | "standard" | "comfortable"; compactColumnHeaderActions?: boolean }) {
function createResizeState(options: { columns: string[]; rows: Array<Array<string | number | boolean | null>>; columnIndexes?: number[]; density?: "compact" | "standard" | "comfortable"; compactColumnHeaderActions?: boolean; headerTextWidth?: number }) {
const compact = ref(options.compactColumnHeaderActions ?? true);
const headerTextWidth = ref(options.headerTextWidth);
const headerMeasurementKey = ref(0);
const state = useDataGridColumnResize({
columns: computed(() => options.columns),
sourceRows: computed(() => options.rows),
columnIndexes: computed(() => options.columnIndexes ?? options.columns.map((_, index) => index)),
density: ref(options.density ?? "standard"),
compactColumnHeaderActions: computed(() => compact.value),
measureHeaderText: () => headerTextWidth.value,
headerMeasurementKey,
});
return {
...state,
setCompact(v: boolean) {
compact.value = v;
},
setHeaderTextWidth(width: number) {
headerTextWidth.value = width;
headerMeasurementKey.value += 1;
},
};
}
@@ -109,7 +117,25 @@ describe("useDataGridColumnResize", () => {
expect(widthNonCompact).toBeGreaterThan(widthCompact);
});
it("compact mode bases width on field name only, capping long names", () => {
it("recalculates column widths when rendered header font metrics change", async () => {
const state = createResizeState({
columns: ["AMOUNT"],
rows: [[1]],
density: "comfortable",
compactColumnHeaderActions: true,
headerTextWidth: 54,
});
state.initColumnWidths();
expect(state.columnWidths.value[0]).toBe(113);
state.setHeaderTextWidth(70);
await nextTick();
expect(state.columnWidths.value[0]).toBe(129);
});
it("compact mode bases width on field name without truncating long names", () => {
// 短字段名:列宽=字段名宽度,值不参与撑宽
const short = createResizeState({
columns: ["id"],
@@ -132,16 +158,16 @@ describe("useDataGridColumnResize", () => {
// 9×7+45=108
expect(mid.columnWidths.value[0]).toBe(108);
// 过长字段名:截断到边界
const capped = createResizeState({
// 过长字段名:字段名仍完整展示,不受内容宽度上限影响
const longName = createResizeState({
columns: ["x".repeat(70)],
rows: [["a"]],
density: "compact",
compactColumnHeaderActions: true,
});
capped.initColumnWidths();
// 70×7+45=535 > maxWidth 480 → 480
expect(capped.columnWidths.value[0]).toBe(480);
longName.initColumnWidths();
// 70×7+45=535,紧凑模式只限制内容,不限制字段名
expect(longName.columnWidths.value[0]).toBe(535);
});
it("comfortable mode uses percentile to ignore outlier values", () => {
@@ -16,10 +16,12 @@ export interface UseDataGridColumnResizeOptions {
columnIndexes: ComputedRef<number[]>;
density: Ref<ColumnWidthDensity>;
compactColumnHeaderActions: ComputedRef<boolean>;
measureHeaderText?: (text: string) => number | undefined;
headerMeasurementKey?: Ref<unknown>;
}
export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) {
const { columns, sourceRows, columnIndexes, density, compactColumnHeaderActions } = options;
const { columns, sourceRows, columnIndexes, density, compactColumnHeaderActions, measureHeaderText } = options;
const columnWidths = ref<number[]>([]);
let isResizing = false;
@@ -55,6 +57,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
sampleValues: sampleColumnValues(colIdx),
density: density.value,
compactColumnHeaderActions: compactColumnHeaderActions.value,
headerTextWidth: measureHeaderText?.(colName),
});
});
}
@@ -114,6 +117,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
density: density.value,
compactColumnHeaderActions: compactColumnHeaderActions.value,
includeValues: true,
headerTextWidth: measureHeaderText?.(colName),
});
}
@@ -139,7 +143,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
() => columnIndexes.value.join("\0"),
() => initColumnWidths(),
);
watch([density, compactColumnHeaderActions], () => initColumnWidths(true));
watch([density, compactColumnHeaderActions, () => options.headerMeasurementKey?.value], () => initColumnWidths(true));
return {
columnWidths,
@@ -91,17 +91,18 @@ export function percentileValue(values: number[], percentile: number): number {
return sorted[idx];
}
export function calculateDataGridColumnWidth(options: { columnName: string; sampleValues: readonly CellValue[]; maxWidth?: number; valueTextLimit?: number; density?: ColumnWidthDensity; compactColumnHeaderActions?: boolean; includeValues?: boolean }): number {
export function calculateDataGridColumnWidth(options: { columnName: string; sampleValues: readonly CellValue[]; maxWidth?: number; valueTextLimit?: number; density?: ColumnWidthDensity; compactColumnHeaderActions?: boolean; includeValues?: boolean; headerTextWidth?: number }): number {
const density = options.density ?? "standard";
const preset = COLUMN_WIDTH_DENSITY_PRESETS[density];
const maxAllowedWidth = options.maxWidth ?? preset.maxWidth;
const valueTextLimit = options.valueTextLimit ?? preset.valueTextLimit;
const headerControl = options.compactColumnHeaderActions ? preset.headerControlWidthCompact : preset.headerControlWidth;
const headerWidth = estimateTextWidth(options.columnName, headerControl, preset.charWidth);
const headerTextWidth = options.headerTextWidth ?? estimateTextWidth(options.columnName, 0, preset.charWidth);
const headerWidth = headerTextWidth + headerControl;
// Compact defaults to header-only sizing, while explicit auto-fit still measures values.
// Density limits cell content, never the column name and its header controls.
if (density === "compact" && !options.includeValues) {
return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.min(maxAllowedWidth, Math.round(headerWidth)));
return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.round(headerWidth));
}
const valueWidths: number[] = [];
@@ -113,7 +114,7 @@ export function calculateDataGridColumnWidth(options: { columnName: string; samp
}
const valueWidth = percentileValue(valueWidths, preset.valueWidthPercentile);
const maxContentWidth = Math.max(headerWidth, valueWidth);
const maxContentWidth = Math.max(headerWidth, Math.min(maxAllowedWidth, valueWidth));
return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.min(maxAllowedWidth, Math.round(maxContentWidth)));
return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.round(maxContentWidth));
}
+24
View File
@@ -32,3 +32,27 @@ test("auto-fit data grid column width stays bounded for very long values", () =>
expect(width).toBe(DATA_GRID_COL_AUTO_FIT_MAX_WIDTH);
});
test("data grid column width uses measured header text as an uncapped minimum", () => {
const width = calculateDataGridColumnWidth({
columnName: "AMOUNT",
sampleValues: ["1"],
density: "comfortable",
compactColumnHeaderActions: true,
headerTextWidth: 54,
});
expect(width).toBe(113);
});
test("compact data grid columns keep long field names complete", () => {
const width = calculateDataGridColumnWidth({
columnName: "x".repeat(100),
sampleValues: ["1"],
density: "compact",
compactColumnHeaderActions: true,
headerTextWidth: 700,
});
expect(width).toBe(745);
});