Add data grid sizing helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Developing-Gamer 2026-05-27 12:31:14 -07:00
parent 15f4573bfb
commit 9d3d5865fb
2 changed files with 117 additions and 0 deletions

View File

@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { fitColumnsToContainer } from "./data-grid-sizing";
import type { DataGridColumnDef } from "./types";
type Row = { id: string };
describe("fitColumnsToContainer", () => {
const columns: DataGridColumnDef<Row>[] = [
{ id: "name", header: "Name", width: 320, minWidth: 80, type: "string", accessor: () => "" },
{ id: "email", header: "Email", width: 420, minWidth: 80, type: "string", accessor: () => "" },
];
it("shrinks fixed columns to fit a narrow container", () => {
const sizes = { name: 320, email: 420 };
fitColumnsToContainer(sizes, columns, 320, 0);
expect(sizes.name + sizes.email).toBe(320);
expect(sizes.name).toBeGreaterThanOrEqual(80);
expect(sizes.email).toBeGreaterThanOrEqual(80);
});
it("does not treat total column width as chrome width", () => {
const sizes = { name: 200, email: 200 };
const buggySizes = { name: 200, email: 200 };
fitColumnsToContainer(sizes, columns, 500, 0);
// Buggy call passed pre-summed column total as chrome, double-counting widths.
fitColumnsToContainer(buggySizes, columns, 500, 400);
expect(sizes.name + sizes.email).toBe(400);
expect(buggySizes.name + buggySizes.email).toBeLessThan(400);
});
it("grows flex columns when the container is wider than the base total", () => {
const flexColumns: DataGridColumnDef<Row>[] = [
{ id: "name", header: "Name", width: 100, minWidth: 80, flex: 1, type: "string", accessor: () => "" },
{ id: "email", header: "Email", width: 100, minWidth: 80, type: "string", accessor: () => "" },
];
const sizes = { name: 100, email: 100 };
fitColumnsToContainer(sizes, flexColumns, 400, 0);
expect(sizes.name + sizes.email).toBe(400);
expect(sizes.name).toBeGreaterThan(100);
});
});

View File

@ -49,3 +49,79 @@ export function getEffectiveMaxWidth<TRow>(col: DataGridColumnDef<TRow>): number
export function clampColumnWidth<TRow>(col: DataGridColumnDef<TRow>, width: number): number {
return Math.max(getEffectiveMinWidth(col), Math.min(getEffectiveMaxWidth(col), width));
}
function distributeFlexWidths<TRow>(
sizes: Record<string, number>,
visibleColumns: readonly DataGridColumnDef<TRow>[],
available: number,
): void {
const flexCols = visibleColumns.filter((c) => c.flex != null && c.flex > 0);
if (flexCols.length === 0 || available <= 0) return;
const totalFlex = flexCols.reduce((acc, c) => acc + (c.flex ?? 0), 0);
let remaining = available;
flexCols.forEach((col, i) => {
const isLast = i === flexCols.length - 1;
const share = isLast
? remaining
: Math.floor(available * ((col.flex ?? 0) / totalFlex));
const max = getEffectiveMaxWidth(col);
const add = Math.max(0, Math.min(share, max - sizes[col.id]));
sizes[col.id] += add;
remaining -= add;
});
}
/** Grow flex columns when there is extra space; shrink flex (then fixed) columns when overflowing. */
export function fitColumnsToContainer<TRow>(
sizes: Record<string, number>,
visibleColumns: readonly DataGridColumnDef<TRow>[],
containerWidth: number,
chromeWidth: number,
): void {
const getTotal = () =>
chromeWidth + visibleColumns.reduce((sum, col) => sum + sizes[col.id], 0);
if (containerWidth <= 0) return;
const total = getTotal();
if (total <= containerWidth) {
distributeFlexWidths(sizes, visibleColumns, containerWidth - total);
return;
}
let overflow = total - containerWidth;
const flexCols = visibleColumns.filter((c) => (c.flex ?? 0) > 0);
for (const col of flexCols) {
if (overflow <= 0) break;
const min = getEffectiveMinWidth(col);
const reducible = sizes[col.id] - min;
if (reducible <= 0) continue;
const delta = Math.min(overflow, reducible);
sizes[col.id] -= delta;
overflow -= delta;
}
if (overflow <= 0) return;
const shrinkable = visibleColumns
.map((col) => ({
col,
reducible: sizes[col.id] - getEffectiveMinWidth(col),
}))
.filter((entry) => entry.reducible > 0);
const totalReducible = shrinkable.reduce((sum, entry) => sum + entry.reducible, 0);
if (totalReducible <= 0) return;
let remaining = overflow;
shrinkable.forEach((entry, index) => {
const isLast = index === shrinkable.length - 1;
const share = isLast
? remaining
: Math.floor(overflow * (entry.reducible / totalReducible));
const delta = Math.min(remaining, share, entry.reducible);
sizes[entry.col.id] -= delta;
remaining -= delta;
});
}