From 9d3d5865fb58706637e61b561d6c6b6f78ebe5fa Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Wed, 27 May 2026 12:31:14 -0700 Subject: [PATCH] Add data grid sizing helpers. Co-authored-by: Cursor --- .../data-grid/data-grid-sizing.test.ts | 41 ++++++++++ .../components/data-grid/data-grid-sizing.ts | 76 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.test.ts diff --git a/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.test.ts b/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.test.ts new file mode 100644 index 000000000..1338d5966 --- /dev/null +++ b/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.test.ts @@ -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[] = [ + { 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[] = [ + { 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); + }); +}); diff --git a/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.ts b/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.ts index 9db4f8f23..21a4edf76 100644 --- a/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.ts +++ b/packages/dashboard-ui-components/src/components/data-grid/data-grid-sizing.ts @@ -49,3 +49,79 @@ export function getEffectiveMaxWidth(col: DataGridColumnDef): number export function clampColumnWidth(col: DataGridColumnDef, width: number): number { return Math.max(getEffectiveMinWidth(col), Math.min(getEffectiveMaxWidth(col), width)); } + +function distributeFlexWidths( + sizes: Record, + visibleColumns: readonly DataGridColumnDef[], + 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( + sizes: Record, + visibleColumns: readonly DataGridColumnDef[], + 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; + }); +}