Skip to content

Commit 4a136ad

Browse files
Feature: Add support for flex column width (adazzle#2839)
* Add support for "auto" width * Calculate columns when grid width is set * Remove unnecessary code * Add support for css column units (fr, %, max-content) * Bring back virtualization * Remove console * Fix minWidth and examples * Add min width * Disable @typescript-eslint/naming-convention * Add comments * Revent some changes * Use getBoundingClientRect * Rename variables * support flex width on all the columns * Fix tests * Update src/DataGrid.tsx Co-authored-by: Nicolas Stepien <[email protected]> * Address a few comments * Move width type close to minWidth * Do not reset max-content columns * Remove flexColumnWidths state * Add max-content example * Update src/DataGrid.tsx Co-authored-by: Nicolas Stepien <[email protected]> * Fix classname * Update src/hooks/useGridDimensions.ts Co-authored-by: Nicolas Stepien <[email protected]> * Remove useMemo * Add back useMemo * tweak autosizeColumnsClassname for better measuring * Revert "tweak autosizeColumnsClassname for better measuring" This reverts commit 151d724. Co-authored-by: Aman Mahajan <[email protected]> Co-authored-by: Nicolas Stepien <[email protected]> Co-authored-by: Nicolas Stepien <[email protected]>
1 parent dd90693 commit 4a136ad

14 files changed

+86
-69
lines changed

src/DataGrid.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
rootClassname,
88
viewportDraggingClassname,
99
focusSinkClassname,
10-
cellAutoResizeClassname,
10+
autosizeColumnsClassname,
1111
rowSelected,
1212
rowSelectedWithFrozenCell
1313
} from './style';
@@ -282,7 +282,7 @@ function DataGrid<R, SR, K extends Key>(
282282
/**
283283
* computed values
284284
*/
285-
const [gridRef, gridWidth, gridHeight] = useGridDimensions();
285+
const [gridRef, gridWidth, gridHeight, isWidthInitialized] = useGridDimensions();
286286
const headerRowsCount = 1;
287287
const topSummaryRowsCount = topSummaryRows?.length ?? 0;
288288
const bottomSummaryRowsCount = bottomSummaryRows?.length ?? 0;
@@ -355,7 +355,7 @@ function DataGrid<R, SR, K extends Key>(
355355
enableVirtualization
356356
});
357357

358-
const viewportColumns = useViewportColumns({
358+
const { viewportColumns, flexWidthViewportColumns } = useViewportColumns({
359359
columns,
360360
colSpanColumns,
361361
colOverscanStartIdx,
@@ -366,6 +366,7 @@ function DataGrid<R, SR, K extends Key>(
366366
rows,
367367
topSummaryRows,
368368
bottomSummaryRows,
369+
columnWidths,
369370
isGroupRow
370371
});
371372

@@ -432,6 +433,25 @@ function DataGrid<R, SR, K extends Key>(
432433
}
433434
});
434435

436+
useLayoutEffect(() => {
437+
if (!isWidthInitialized || flexWidthViewportColumns.length === 0) return;
438+
const newColumnWidths = new Map<string, number>();
439+
for (const column of flexWidthViewportColumns) {
440+
const columnElement = gridRef.current!.querySelector<HTMLDivElement>(
441+
`[aria-colindex="${column.idx + 1}"]`
442+
);
443+
if (columnElement) {
444+
// Set the actual width of the column after it is rendered
445+
const { width } = columnElement.getBoundingClientRect();
446+
newColumnWidths.set(column.key, width);
447+
}
448+
}
449+
if (newColumnWidths.size === 0) return;
450+
setColumnWidths((columnWidths) => {
451+
return new Map([...columnWidths, ...newColumnWidths]);
452+
});
453+
}, [isWidthInitialized, flexWidthViewportColumns, gridRef]);
454+
435455
useLayoutEffect(() => {
436456
if (autoResizeColumn === null) return;
437457
const columnElement = gridRef.current!.querySelector(
@@ -465,8 +485,8 @@ function DataGrid<R, SR, K extends Key>(
465485
* callbacks
466486
*/
467487
const handleColumnResize = useCallback(
468-
(column: CalculatedColumn<R, SR>, width: number | 'auto') => {
469-
if (width === 'auto') {
488+
(column: CalculatedColumn<R, SR>, width: number | 'max-content') => {
489+
if (width === 'max-content') {
470490
setAutoResizeColumn(column);
471491
return;
472492
}
@@ -909,10 +929,16 @@ function DataGrid<R, SR, K extends Key>(
909929
}
910930

911931
function getLayoutCssVars() {
912-
if (autoResizeColumn === null) return layoutCssVars;
932+
if (autoResizeColumn === null && flexWidthViewportColumns.length === 0) return layoutCssVars;
913933
const { gridTemplateColumns } = layoutCssVars;
914934
const newSizes = gridTemplateColumns.split(' ');
915-
newSizes[autoResizeColumn.idx] = 'max-content';
935+
if (autoResizeColumn !== null) {
936+
newSizes[autoResizeColumn.idx] = 'max-content';
937+
}
938+
for (const column of flexWidthViewportColumns) {
939+
newSizes[column.idx] = column.width as string;
940+
}
941+
916942
return {
917943
...layoutCssVars,
918944
gridTemplateColumns: newSizes.join(' ')
@@ -1146,7 +1172,8 @@ function DataGrid<R, SR, K extends Key>(
11461172
rootClassname,
11471173
{
11481174
[viewportDraggingClassname]: isDragging,
1149-
[cellAutoResizeClassname]: autoResizeColumn !== null
1175+
[autosizeColumnsClassname]:
1176+
autoResizeColumn !== null || flexWidthViewportColumns.length > 0
11501177
},
11511178
className
11521179
)}

src/HeaderCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export default function HeaderCell<R, SR>({
155155
return;
156156
}
157157

158-
onColumnResize(column, 'auto');
158+
onColumnResize(column, 'max-content');
159159
}
160160

161161
function handleFocus(event: React.FocusEvent<HTMLDivElement>) {

src/HeaderRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface HeaderRowProps<R, SR, K extends React.Key> extends SharedDataGr
1717
columns: readonly CalculatedColumn<R, SR>[];
1818
allRowsSelected: boolean;
1919
onAllRowsSelectionChange: (checked: boolean) => void;
20-
onColumnResize: (column: CalculatedColumn<R, SR>, width: number | 'auto') => void;
20+
onColumnResize: (column: CalculatedColumn<R, SR>, width: number | 'max-content') => void;
2121
selectCell: (columnIdx: number) => void;
2222
lastFrozenColumnIndex: number;
2323
selectedCellIdx: number | undefined;

src/hooks/useCalculatedColumns.ts

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { CalculatedColumn, Column, Maybe } from '../types';
44
import type { DataGridProps } from '../DataGrid';
55
import { valueFormatter, toggleGroupFormatter } from '../formatters';
66
import { SELECT_COLUMN_KEY } from '../Columns';
7-
import { clampColumnWidth, floor, max, min, round } from '../utils';
7+
import { clampColumnWidth, max, min } from '../utils';
88

99
type Mutable<T> = {
1010
-readonly [P in keyof T]: T[P];
@@ -15,6 +15,9 @@ interface ColumnMetric {
1515
left: number;
1616
}
1717

18+
const DEFAULT_COLUMN_WIDTH = 'auto';
19+
const DEFAULT_COLUMN_MIN_WIDTH = 80;
20+
1821
interface CalculatedColumnsArgs<R, SR> extends Pick<DataGridProps<R, SR>, 'defaultColumnOptions'> {
1922
rawColumns: readonly Column<R, SR>[];
2023
rawGroupBy: Maybe<readonly string[]>;
@@ -33,8 +36,8 @@ export function useCalculatedColumns<R, SR>({
3336
rawGroupBy,
3437
enableVirtualization
3538
}: CalculatedColumnsArgs<R, SR>) {
36-
const defaultWidth = defaultColumnOptions?.width;
37-
const defaultMinWidth = defaultColumnOptions?.minWidth ?? 80;
39+
const defaultWidth = defaultColumnOptions?.width ?? DEFAULT_COLUMN_WIDTH;
40+
const defaultMinWidth = defaultColumnOptions?.minWidth ?? DEFAULT_COLUMN_MIN_WIDTH;
3841
const defaultMaxWidth = defaultColumnOptions?.maxWidth;
3942
const defaultFormatter = defaultColumnOptions?.formatter ?? valueFormatter;
4043
const defaultSortable = defaultColumnOptions?.sortable ?? false;
@@ -148,38 +151,19 @@ export function useCalculatedColumns<R, SR>({
148151
let left = 0;
149152
let totalFrozenColumnWidth = 0;
150153
let templateColumns = '';
151-
let allocatedWidth = 0;
152-
let unassignedColumnsCount = 0;
153154

154155
for (const column of columns) {
155-
let width = getSpecifiedWidth(column, columnWidths, viewportWidth);
156-
157-
if (width === undefined) {
158-
unassignedColumnsCount++;
159-
} else {
156+
let width = columnWidths.get(column.key) ?? column.width;
157+
if (typeof width === 'number') {
160158
width = clampColumnWidth(width, column);
161-
allocatedWidth += width;
162-
columnMetrics.set(column, { width, left: 0 });
163-
}
164-
}
165-
166-
for (const column of columns) {
167-
let width: number;
168-
if (columnMetrics.has(column)) {
169-
const columnMetric = columnMetrics.get(column)!;
170-
columnMetric.left = left;
171-
({ width } = columnMetric);
172159
} else {
173-
// avoid decimals as subpixel positioning can lead to cell borders not being displayed
174-
const unallocatedWidth = viewportWidth - allocatedWidth;
175-
const unallocatedColumnWidth = round(unallocatedWidth / unassignedColumnsCount);
176-
width = clampColumnWidth(unallocatedColumnWidth, column);
177-
allocatedWidth += width;
178-
unassignedColumnsCount--;
179-
columnMetrics.set(column, { width, left });
160+
// This is a placeholder width so we can continue to use virtualization.
161+
// The actual value is set after the column is rendered
162+
width = column.minWidth;
180163
}
181-
left += width;
182164
templateColumns += `${width}px `;
165+
columnMetrics.set(column, { width, left });
166+
left += width;
183167
}
184168

185169
if (lastFrozenColumnIndex !== -1) {
@@ -197,7 +181,7 @@ export function useCalculatedColumns<R, SR>({
197181
}
198182

199183
return { layoutCssVars, totalFrozenColumnWidth, columnMetrics };
200-
}, [columnWidths, columns, viewportWidth, lastFrozenColumnIndex]);
184+
}, [columnWidths, columns, lastFrozenColumnIndex]);
201185

202186
const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => {
203187
if (!enableVirtualization) {
@@ -265,22 +249,3 @@ export function useCalculatedColumns<R, SR>({
265249
groupBy
266250
};
267251
}
268-
269-
function getSpecifiedWidth<R, SR>(
270-
{ key, width }: Column<R, SR>,
271-
columnWidths: ReadonlyMap<string, number>,
272-
viewportWidth: number
273-
): number | undefined {
274-
if (columnWidths.has(key)) {
275-
// Use the resized width if available
276-
return columnWidths.get(key);
277-
}
278-
279-
if (typeof width === 'number') {
280-
return width;
281-
}
282-
if (typeof width === 'string' && /^\d+%$/.test(width)) {
283-
return floor((viewportWidth * parseInt(width, 10)) / 100);
284-
}
285-
return undefined;
286-
}

src/hooks/useGridDimensions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { ceil } from '../utils';
66
export function useGridDimensions(): [
77
ref: React.RefObject<HTMLDivElement>,
88
width: number,
9-
height: number
9+
height: number,
10+
isWidthInitialized: boolean
1011
] {
1112
const gridRef = useRef<HTMLDivElement>(null);
1213
const [inlineSize, setInlineSize] = useState(1);
1314
const [blockSize, setBlockSize] = useState(1);
15+
const [isWidthInitialized, setWidthInitialized] = useState(false);
1416

1517
useLayoutEffect(() => {
1618
const { ResizeObserver } = window;
@@ -31,6 +33,7 @@ export function useGridDimensions(): [
3133
const size = entries[0].contentBoxSize[0];
3234
setInlineSize(handleDevicePixelRatio(size.inlineSize));
3335
setBlockSize(size.blockSize);
36+
setWidthInitialized(true);
3437
});
3538
resizeObserver.observe(gridRef.current!);
3639

@@ -39,7 +42,7 @@ export function useGridDimensions(): [
3942
};
4043
}, []);
4144

42-
return [gridRef, inlineSize, blockSize];
45+
return [gridRef, inlineSize, blockSize, isWidthInitialized];
4346
}
4447

4548
// TODO: remove once fixed upstream

src/hooks/useViewportColumns.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface ViewportColumnsArgs<R, SR> {
1414
lastFrozenColumnIndex: number;
1515
rowOverscanStartIdx: number;
1616
rowOverscanEndIdx: number;
17+
columnWidths: ReadonlyMap<string, number>;
1718
isGroupRow: (row: R | GroupRow<R>) => row is GroupRow<R>;
1819
}
1920

@@ -28,6 +29,7 @@ export function useViewportColumns<R, SR>({
2829
lastFrozenColumnIndex,
2930
rowOverscanStartIdx,
3031
rowOverscanEndIdx,
32+
columnWidths,
3133
isGroupRow
3234
}: ViewportColumnsArgs<R, SR>) {
3335
// find the column that spans over a column within the visible columns range and adjust colOverscanStartIdx
@@ -104,15 +106,31 @@ export function useViewportColumns<R, SR>({
104106
isGroupRow
105107
]);
106108

107-
return useMemo((): readonly CalculatedColumn<R, SR>[] => {
109+
const { viewportColumns, flexWidthViewportColumns } = useMemo((): {
110+
viewportColumns: readonly CalculatedColumn<R, SR>[];
111+
flexWidthViewportColumns: readonly CalculatedColumn<R, SR>[];
112+
} => {
108113
const viewportColumns: CalculatedColumn<R, SR>[] = [];
114+
const flexWidthViewportColumns: CalculatedColumn<R, SR>[] = [];
109115
for (let colIdx = 0; colIdx <= colOverscanEndIdx; colIdx++) {
110116
const column = columns[colIdx];
111117

112118
if (colIdx < startIdx && !column.frozen) continue;
113119
viewportColumns.push(column);
120+
if (typeof column.width === 'string') {
121+
flexWidthViewportColumns.push(column);
122+
}
114123
}
115124

116-
return viewportColumns;
125+
return { viewportColumns, flexWidthViewportColumns };
117126
}, [startIdx, colOverscanEndIdx, columns]);
127+
128+
const unsizedFlexWidthViewportColumns = useMemo((): readonly CalculatedColumn<R, SR>[] => {
129+
return flexWidthViewportColumns.filter((column) => !columnWidths.has(column.key));
130+
}, [flexWidthViewportColumns, columnWidths]);
131+
132+
return {
133+
viewportColumns,
134+
flexWidthViewportColumns: unsizedFlexWidthViewportColumns
135+
};
118136
}

src/style/cell.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const cell = css`
3333
export const cellClassname = `rdg-cell ${cell}`;
3434

3535
// max-content does not calculate width when contain is set to style or size
36-
export const cellAutoResizeClassname = css`
36+
export const autosizeColumnsClassname = css`
3737
@layer rdg.Root {
3838
.${cell} {
3939
contain: content;

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface Column<TRow, TSummaryRow = unknown> {
5555

5656
export interface CalculatedColumn<TRow, TSummaryRow = unknown> extends Column<TRow, TSummaryRow> {
5757
readonly idx: number;
58+
readonly width: number | string;
5859
readonly minWidth: number;
5960
readonly resizable: boolean;
6061
readonly sortable: boolean;

test/column/formatter.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('Custom formatter component', () => {
108108
resizable: false,
109109
rowGroup: false,
110110
sortable: false,
111-
width: undefined
111+
width: 'auto'
112112
},
113113
indexes: [0]
114114
});

website/demos/AllFeatures.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const columns: readonly Column<Row>[] = [
8989
{
9090
key: 'email',
9191
name: 'Email',
92-
width: 200,
92+
width: 'max-content',
9393
resizable: true,
9494
editor: textEditor
9595
},
@@ -124,7 +124,7 @@ const columns: readonly Column<Row>[] = [
124124
{
125125
key: 'catchPhrase',
126126
name: 'Catch Phrase',
127-
width: 200,
127+
width: 'max-content',
128128
resizable: true,
129129
editor: textEditor
130130
},
@@ -138,7 +138,7 @@ const columns: readonly Column<Row>[] = [
138138
{
139139
key: 'sentence',
140140
name: 'Sentence',
141-
width: 200,
141+
width: 'max-content',
142142
resizable: true,
143143
editor: textEditor
144144
}

website/demos/ColumnSpanning.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function ColumnSpanning({ direction }: Props) {
3232
key,
3333
name: key,
3434
frozen: i < 5,
35+
width: 80,
3536
resizable: true,
3637
formatter: cellFormatter,
3738
colSpan(args) {

website/demos/ColumnsReordering.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default function ColumnsReordering({ direction }: Props) {
126126
sortColumns={sortColumns}
127127
onSortColumnsChange={onSortColumnsChange}
128128
direction={direction}
129+
defaultColumnOptions={{ width: '1fr' }}
129130
/>
130131
</DndProvider>
131132
);

website/demos/CommonFeatures.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function getColumns(countries: string[], direction: Direction): readonly Column<
100100
{
101101
key: 'client',
102102
name: 'Client',
103-
width: 220,
103+
width: 'max-content',
104104
editor: textEditor
105105
},
106106
{

website/demos/MillionCells.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function MillionCells({ direction }: Props) {
2424
key,
2525
name: key,
2626
frozen: i < 5,
27+
width: 80,
2728
resizable: true,
2829
formatter: cellFormatter
2930
});

0 commit comments

Comments
 (0)