Skip to content

Commit 3887ea8

Browse files
authored
Fix column auto resize (#3746)
Fixes #3723 Root cause: #3724 (comment)
1 parent 2c26504 commit 3887ea8

File tree

4 files changed

+123
-45
lines changed

4 files changed

+123
-45
lines changed

src/HeaderRow.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { css } from '@linaria/core';
33
import clsx from 'clsx';
44

55
import { getColSpan } from './utils';
6-
import type { CalculatedColumn, Direction, Position } from './types';
6+
import type { CalculatedColumn, Direction, Position, ResizedWidth } from './types';
77
import type { DataGridProps } from './DataGrid';
88
import HeaderCell from './HeaderCell';
99
import { cell, cellFrozen } from './style/cell';
@@ -17,7 +17,7 @@ type SharedDataGridProps<R, SR, K extends React.Key> = Pick<
1717
export interface HeaderRowProps<R, SR, K extends React.Key> extends SharedDataGridProps<R, SR, K> {
1818
rowIdx: number;
1919
columns: readonly CalculatedColumn<R, SR>[];
20-
onColumnResize: (column: CalculatedColumn<R, SR>, width: number | 'max-content') => void;
20+
onColumnResize: (column: CalculatedColumn<R, SR>, width: ResizedWidth) => void;
2121
selectCell: (position: Position) => void;
2222
lastFrozenColumnIndex: number;
2323
selectedCellIdx: number | undefined;

src/hooks/useColumnWidths.ts

+58-36
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useLayoutEffect, useRef } from 'react';
1+
import { useLayoutEffect, useState } from 'react';
22
import { flushSync } from 'react-dom';
33

4-
import type { CalculatedColumn, StateSetter } from '../types';
4+
import type { CalculatedColumn, ResizedWidth, StateSetter } from '../types';
55
import type { DataGridProps } from '../DataGrid';
66

77
export function useColumnWidths<R, SR>(
@@ -16,17 +16,27 @@ export function useColumnWidths<R, SR>(
1616
setMeasuredColumnWidths: StateSetter<ReadonlyMap<string, number>>,
1717
onColumnResize: DataGridProps<R, SR>['onColumnResize']
1818
) {
19-
const prevGridWidthRef = useRef(gridWidth);
19+
const [columnToAutoResize, setColumnToAutoResize] = useState<{
20+
readonly key: string;
21+
readonly width: ResizedWidth;
22+
} | null>(null);
23+
const [prevGridWidth, setPreviousGridWidth] = useState(gridWidth);
2024
const columnsCanFlex: boolean = columns.length === viewportColumns.length;
2125
// Allow columns to flex again when...
2226
const ignorePreviouslyMeasuredColumns: boolean =
2327
// there is enough space for columns to flex and the grid was resized
24-
columnsCanFlex && gridWidth !== prevGridWidthRef.current;
28+
columnsCanFlex && gridWidth !== prevGridWidth;
2529
const newTemplateColumns = [...templateColumns];
2630
const columnsToMeasure: string[] = [];
2731

2832
for (const { key, idx, width } of viewportColumns) {
29-
if (
33+
if (key === columnToAutoResize?.key) {
34+
newTemplateColumns[idx] =
35+
columnToAutoResize.width === 'max-content'
36+
? columnToAutoResize.width
37+
: `${columnToAutoResize.width}px`;
38+
columnsToMeasure.push(key);
39+
} else if (
3040
typeof width === 'string' &&
3141
(ignorePreviouslyMeasuredColumns || !measuredColumnWidths.has(key)) &&
3242
!resizedColumnWidths.has(key)
@@ -38,12 +48,10 @@ export function useColumnWidths<R, SR>(
3848

3949
const gridTemplateColumns = newTemplateColumns.join(' ');
4050

41-
useLayoutEffect(() => {
42-
prevGridWidthRef.current = gridWidth;
43-
updateMeasuredWidths(columnsToMeasure);
44-
});
51+
useLayoutEffect(updateMeasuredWidths);
4552

46-
function updateMeasuredWidths(columnsToMeasure: readonly string[]) {
53+
function updateMeasuredWidths() {
54+
setPreviousGridWidth(gridWidth);
4755
if (columnsToMeasure.length === 0) return;
4856

4957
setMeasuredColumnWidths((measuredColumnWidths) => {
@@ -62,40 +70,54 @@ export function useColumnWidths<R, SR>(
6270

6371
return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths;
6472
});
65-
}
6673

67-
function handleColumnResize(column: CalculatedColumn<R, SR>, nextWidth: number | 'max-content') {
68-
const { key: resizingKey } = column;
69-
const newTemplateColumns = [...templateColumns];
70-
const columnsToMeasure: string[] = [];
71-
72-
for (const { key, idx, width } of viewportColumns) {
73-
if (resizingKey === key) {
74-
const width = typeof nextWidth === 'number' ? `${nextWidth}px` : nextWidth;
75-
newTemplateColumns[idx] = width;
76-
} else if (columnsCanFlex && typeof width === 'string' && !resizedColumnWidths.has(key)) {
77-
newTemplateColumns[idx] = width;
78-
columnsToMeasure.push(key);
79-
}
74+
if (columnToAutoResize !== null) {
75+
const resizingKey = columnToAutoResize.key;
76+
setResizedColumnWidths((resizedColumnWidths) => {
77+
const oldWidth = resizedColumnWidths.get(resizingKey);
78+
const newWidth = measureColumnWidth(gridRef, resizingKey);
79+
if (newWidth !== undefined && oldWidth !== newWidth) {
80+
const newResizedColumnWidths = new Map(resizedColumnWidths);
81+
newResizedColumnWidths.set(resizingKey, newWidth);
82+
return newResizedColumnWidths;
83+
}
84+
return resizedColumnWidths;
85+
});
86+
setColumnToAutoResize(null);
8087
}
88+
}
8189

82-
gridRef.current!.style.gridTemplateColumns = newTemplateColumns.join(' ');
83-
const measuredWidth =
84-
typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey)!;
90+
function handleColumnResize(column: CalculatedColumn<R, SR>, nextWidth: ResizedWidth) {
91+
const { key: resizingKey } = column;
8592

86-
// TODO: remove
87-
// need flushSync to keep frozen column offsets in sync
88-
// we may be able to use `startTransition` or even `requestIdleCallback` instead
8993
flushSync(() => {
90-
setResizedColumnWidths((resizedColumnWidths) => {
91-
const newResizedColumnWidths = new Map(resizedColumnWidths);
92-
newResizedColumnWidths.set(resizingKey, measuredWidth);
93-
return newResizedColumnWidths;
94+
if (columnsCanFlex) {
95+
// remeasure all the columns that can flex and are not resized by the user
96+
setMeasuredColumnWidths((measuredColumnWidths) => {
97+
const newMeasuredColumnWidths = new Map(measuredColumnWidths);
98+
for (const { key, width } of viewportColumns) {
99+
if (resizingKey !== key && typeof width === 'string' && !resizedColumnWidths.has(key)) {
100+
newMeasuredColumnWidths.delete(key);
101+
}
102+
}
103+
return newMeasuredColumnWidths;
104+
});
105+
}
106+
107+
setColumnToAutoResize({
108+
key: resizingKey,
109+
width: nextWidth
94110
});
95-
updateMeasuredWidths(columnsToMeasure);
96111
});
97112

98-
onColumnResize?.(column, measuredWidth);
113+
if (onColumnResize) {
114+
const previousWidth = resizedColumnWidths.get(resizingKey);
115+
const newWidth =
116+
typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey);
117+
if (newWidth !== undefined && newWidth !== previousWidth) {
118+
onColumnResize(column, newWidth);
119+
}
120+
}
99121
}
100122

101123
return {

src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,5 @@ export interface Renderers<TRow, TSummaryRow> {
322322
}
323323

324324
export type Direction = 'ltr' | 'rtl';
325+
326+
export type ResizedWidth = number | 'max-content';

test/browser/column/resizable.test.tsx

+61-7
Original file line numberDiff line numberDiff line change
@@ -66,48 +66,54 @@ test('cannot not resize or auto resize column when resizable is not specified',
6666
test('should resize column when dragging the handle', async () => {
6767
const onColumnResize = vi.fn();
6868
setup<Row, unknown>({ columns, rows: [], onColumnResize });
69-
const [, col2] = getHeaderCells();
7069
const grid = getGrid();
7170
expect(onColumnResize).not.toHaveBeenCalled();
7271
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
72+
const [, col2] = getHeaderCells();
7373
await resize({ column: col2, resizeBy: -50 });
7474
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 150px' });
7575
expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 150);
7676
});
7777

7878
test('should use the maxWidth if specified', async () => {
7979
setup<Row, unknown>({ columns, rows: [] });
80-
const [, col2] = getHeaderCells();
8180
const grid = getGrid();
8281
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px ' });
82+
const [, col2] = getHeaderCells();
8383
await resize({ column: col2, resizeBy: 1000 });
8484
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' });
8585
});
8686

8787
test('should use the minWidth if specified', async () => {
8888
setup<Row, unknown>({ columns, rows: [] });
89-
const [, col2] = getHeaderCells();
9089
const grid = getGrid();
9190
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
91+
const [, col2] = getHeaderCells();
9292
await resize({ column: col2, resizeBy: -150 });
9393
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' });
9494
});
9595

9696
test('should auto resize column when resize handle is double clicked', async () => {
97+
const onColumnResize = vi.fn();
9798
setup<Row, unknown>({
9899
columns,
99100
rows: [
100101
{
101102
col1: 1,
102103
col2: 'a'.repeat(50)
103104
}
104-
]
105+
],
106+
onColumnResize
105107
});
106-
const [, col2] = getHeaderCells();
107108
const grid = getGrid();
108109
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
110+
const [, col2] = getHeaderCells();
109111
await autoResize(col2);
110112
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 327.703px' });
113+
expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(
114+
expect.objectContaining(columns[1]),
115+
327.703125
116+
);
111117
});
112118

113119
test('should use the maxWidth if specified on auto resize', async () => {
@@ -120,9 +126,9 @@ test('should use the maxWidth if specified on auto resize', async () => {
120126
}
121127
]
122128
});
123-
const [, col2] = getHeaderCells();
124129
const grid = getGrid();
125130
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
131+
const [, col2] = getHeaderCells();
126132
await autoResize(col2);
127133
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' });
128134
});
@@ -137,9 +143,57 @@ test('should use the minWidth if specified on auto resize', async () => {
137143
}
138144
]
139145
});
140-
const [, col2] = getHeaderCells();
141146
const grid = getGrid();
142147
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
148+
const [, col2] = getHeaderCells();
143149
await autoResize(col2);
144150
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' });
145151
});
152+
153+
test('should remeasure flex columns when resizing a column', async () => {
154+
const onColumnResize = vi.fn();
155+
setup<
156+
{
157+
readonly col1: string;
158+
readonly col2: string;
159+
readonly col3: string;
160+
},
161+
unknown
162+
>({
163+
columns: [
164+
{
165+
key: 'col1',
166+
name: 'col1',
167+
resizable: true
168+
},
169+
{
170+
key: 'col2',
171+
name: 'col2',
172+
resizable: true
173+
},
174+
{
175+
key: 'col3',
176+
name: 'col3',
177+
resizable: true
178+
}
179+
],
180+
rows: [
181+
{
182+
col1: 'a'.repeat(10),
183+
col2: 'a'.repeat(10),
184+
col3: 'a'.repeat(10)
185+
}
186+
],
187+
onColumnResize
188+
});
189+
const grid = getGrid();
190+
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '639.328px 639.328px 639.344px' });
191+
const [col1] = getHeaderCells();
192+
await autoResize(col1);
193+
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' });
194+
expect(onColumnResize).toHaveBeenCalledOnce();
195+
// onColumnResize is not called if width is not changed
196+
await autoResize(col1);
197+
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' });
198+
expect(onColumnResize).toHaveBeenCalledOnce();
199+
});

0 commit comments

Comments
 (0)