mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 14:54:46 +00:00
fix(grid): keep column names fully visible
This commit is contained in:
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user