-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix column auto resize #3746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix column auto resize #3746
Changes from 9 commits
005c9f2
b66f3bd
51c2bdd
d4afa2b
ccaa437
69c46a2
f60e759
f736a3b
0e90906
523ec5a
8a6bb28
0bc54d9
58b38d3
8bae511
012a2b9
3f04905
21205c8
06d7e92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
import { useLayoutEffect, useRef } from 'react'; | ||
import { flushSync } from 'react-dom'; | ||
import { useLayoutEffect, useRef, useState } from 'react'; | ||
|
||
import type { CalculatedColumn, StateSetter } from '../types'; | ||
import type { DataGridProps } from '../DataGrid'; | ||
|
@@ -16,6 +15,7 @@ export function useColumnWidths<R, SR>( | |
setMeasuredColumnWidths: StateSetter<ReadonlyMap<string, number>>, | ||
onColumnResize: DataGridProps<R, SR>['onColumnResize'] | ||
) { | ||
const [columnToAutoResize, setColumnToAutoResize] = useState<string | null>(null); | ||
const prevGridWidthRef = useRef(gridWidth); | ||
const columnsCanFlex: boolean = columns.length === viewportColumns.length; | ||
// Allow columns to flex again when... | ||
|
@@ -26,7 +26,10 @@ export function useColumnWidths<R, SR>( | |
const columnsToMeasure: string[] = []; | ||
|
||
for (const { key, idx, width } of viewportColumns) { | ||
if ( | ||
if (key === columnToAutoResize) { | ||
newTemplateColumns[idx] = 'max-content'; | ||
columnsToMeasure.push(key); | ||
} else if ( | ||
typeof width === 'string' && | ||
(ignorePreviouslyMeasuredColumns || !measuredColumnWidths.has(key)) && | ||
!resizedColumnWidths.has(key) | ||
|
@@ -40,62 +43,81 @@ export function useColumnWidths<R, SR>( | |
|
||
useLayoutEffect(() => { | ||
prevGridWidthRef.current = gridWidth; | ||
updateMeasuredWidths(columnsToMeasure); | ||
updateMeasuredWidths(); | ||
}); | ||
|
||
function updateMeasuredWidths(columnsToMeasure: readonly string[]) { | ||
if (columnsToMeasure.length === 0) return; | ||
nstepien marked this conversation as resolved.
Show resolved
Hide resolved
|
||
function updateMeasuredWidths() { | ||
if (columnsToMeasure.length > 0) { | ||
setMeasuredColumnWidths((measuredColumnWidths) => { | ||
const newMeasuredColumnWidths = new Map(measuredColumnWidths); | ||
let hasChanges = false; | ||
|
||
for (const key of columnsToMeasure) { | ||
const measuredWidth = measureColumnWidth(gridRef, key); | ||
hasChanges ||= measuredWidth !== measuredColumnWidths.get(key); | ||
if (measuredWidth === undefined) { | ||
newMeasuredColumnWidths.delete(key); | ||
} else { | ||
newMeasuredColumnWidths.set(key, measuredWidth); | ||
} | ||
} | ||
|
||
setMeasuredColumnWidths((measuredColumnWidths) => { | ||
const newMeasuredColumnWidths = new Map(measuredColumnWidths); | ||
let hasChanges = false; | ||
return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths; | ||
}); | ||
} | ||
|
||
for (const key of columnsToMeasure) { | ||
const measuredWidth = measureColumnWidth(gridRef, key); | ||
hasChanges ||= measuredWidth !== measuredColumnWidths.get(key); | ||
if (measuredWidth === undefined) { | ||
newMeasuredColumnWidths.delete(key); | ||
} else { | ||
newMeasuredColumnWidths.set(key, measuredWidth); | ||
if (columnToAutoResize !== null) { | ||
setColumnToAutoResize(null); | ||
setResizedColumnWidths((resizedColumnWidths) => { | ||
const oldWidth = resizedColumnWidths.get(columnToAutoResize); | ||
const newWidth = measureColumnWidth(gridRef, columnToAutoResize); | ||
if (newWidth !== undefined && oldWidth !== newWidth) { | ||
const newResizedColumnWidths = new Map(resizedColumnWidths); | ||
newResizedColumnWidths.set(columnToAutoResize, newWidth); | ||
onColumnResize?.(viewportColumns.find((c) => c.key === columnToAutoResize)!, newWidth); | ||
amanmahajan7 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return newResizedColumnWidths; | ||
} | ||
} | ||
|
||
return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths; | ||
}); | ||
return resizedColumnWidths; | ||
}); | ||
} | ||
} | ||
|
||
function handleColumnResize(column: CalculatedColumn<R, SR>, nextWidth: number | 'max-content') { | ||
const { key: resizingKey } = column; | ||
const newTemplateColumns = [...templateColumns]; | ||
const columnsToMeasure: string[] = []; | ||
|
||
for (const { key, idx, width } of viewportColumns) { | ||
if (resizingKey === key) { | ||
const width = typeof nextWidth === 'number' ? `${nextWidth}px` : nextWidth; | ||
newTemplateColumns[idx] = width; | ||
} else if (columnsCanFlex && typeof width === 'string' && !resizedColumnWidths.has(key)) { | ||
newTemplateColumns[idx] = width; | ||
// remeasure all columns that can flex and are not resized by the user | ||
for (const { key, width } of viewportColumns) { | ||
if ( | ||
columnsCanFlex && | ||
resizingKey !== key && | ||
typeof width === 'string' && | ||
!resizedColumnWidths.has(key) | ||
) { | ||
columnsToMeasure.push(key); | ||
} | ||
} | ||
|
||
gridRef.current!.style.gridTemplateColumns = newTemplateColumns.join(' '); | ||
const measuredWidth = | ||
typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey)!; | ||
if (columnsToMeasure.length > 0) { | ||
nstepien marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
setMeasuredColumnWidths((measuredColumnWidths) => { | ||
const newMeasuredColumnWidths = new Map(measuredColumnWidths); | ||
for (const columnKey of columnsToMeasure) { | ||
newMeasuredColumnWidths.delete(columnKey); | ||
} | ||
return newMeasuredColumnWidths; | ||
}); | ||
} | ||
|
||
// TODO: remove | ||
// need flushSync to keep frozen column offsets in sync | ||
// we may be able to use `startTransition` or even `requestIdleCallback` instead | ||
flushSync(() => { | ||
if (typeof nextWidth === 'number') { | ||
|
||
setResizedColumnWidths((resizedColumnWidths) => { | ||
const newResizedColumnWidths = new Map(resizedColumnWidths); | ||
newResizedColumnWidths.set(resizingKey, measuredWidth); | ||
newResizedColumnWidths.set(resizingKey, nextWidth); | ||
return newResizedColumnWidths; | ||
}); | ||
updateMeasuredWidths(columnsToMeasure); | ||
}); | ||
|
||
onColumnResize?.(column, measuredWidth); | ||
onColumnResize?.(column, nextWidth); | ||
|
||
} else { | ||
setColumnToAutoResize(resizingKey); | ||
} | ||
} | ||
|
||
return { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,48 +66,52 @@ test('cannot not resize or auto resize column when resizable is not specified', | |
test('should resize column when dragging the handle', async () => { | ||
const onColumnResize = vi.fn(); | ||
setup<Row, unknown>({ columns, rows: [], onColumnResize }); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
expect(onColumnResize).not.toHaveBeenCalled(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); | ||
const [, col2] = getHeaderCells(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand, surely the cell element should still be in the document if we get it earlier, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be. I will investigate in a separate PR |
||
await resize({ column: col2, resizeBy: -50 }); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 150px' }); | ||
expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 150); | ||
}); | ||
|
||
test('should use the maxWidth if specified', async () => { | ||
setup<Row, unknown>({ columns, rows: [] }); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px ' }); | ||
const [, col2] = getHeaderCells(); | ||
await resize({ column: col2, resizeBy: 1000 }); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' }); | ||
}); | ||
|
||
test('should use the minWidth if specified', async () => { | ||
setup<Row, unknown>({ columns, rows: [] }); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); | ||
const [, col2] = getHeaderCells(); | ||
await resize({ column: col2, resizeBy: -150 }); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' }); | ||
}); | ||
|
||
test('should auto resize column when resize handle is double clicked', async () => { | ||
const onColumnResize = vi.fn(); | ||
setup<Row, unknown>({ | ||
columns, | ||
rows: [ | ||
{ | ||
col1: 1, | ||
col2: 'a'.repeat(50) | ||
} | ||
] | ||
], | ||
onColumnResize | ||
}); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); | ||
const [, col2] = getHeaderCells(); | ||
await autoResize(col2); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 327.703px' }); | ||
// This is called twice in strict mode | ||
|
||
expect(onColumnResize).toHaveBeenCalledWith(expect.objectContaining(columns[1]), 327.703125); | ||
}); | ||
|
||
test('should use the maxWidth if specified on auto resize', async () => { | ||
|
@@ -120,9 +124,9 @@ test('should use the maxWidth if specified on auto resize', async () => { | |
} | ||
] | ||
}); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); | ||
const [, col2] = getHeaderCells(); | ||
await autoResize(col2); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' }); | ||
}); | ||
|
@@ -137,9 +141,58 @@ test('should use the minWidth if specified on auto resize', async () => { | |
} | ||
] | ||
}); | ||
const [, col2] = getHeaderCells(); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); | ||
const [, col2] = getHeaderCells(); | ||
await autoResize(col2); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' }); | ||
}); | ||
|
||
test('should remeasure flex columns when resizing a column', async () => { | ||
const onColumnResize = vi.fn(); | ||
setup< | ||
{ | ||
readonly col1: string; | ||
readonly col2: string; | ||
readonly col3: string; | ||
}, | ||
unknown | ||
>({ | ||
columns: [ | ||
{ | ||
key: 'col1', | ||
name: 'col1', | ||
resizable: true | ||
}, | ||
{ | ||
key: 'col2', | ||
name: 'col2', | ||
resizable: true | ||
}, | ||
{ | ||
key: 'col3', | ||
name: 'col3', | ||
resizable: true | ||
} | ||
], | ||
rows: [ | ||
{ | ||
col1: 'a'.repeat(10), | ||
col2: 'a'.repeat(10), | ||
col3: 'a'.repeat(10) | ||
} | ||
], | ||
onColumnResize | ||
}); | ||
const grid = getGrid(); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '639.328px 639.328px 639.344px' }); | ||
const [col1] = getHeaderCells(); | ||
await autoResize(col1); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); | ||
expect(onColumnResize).toHaveBeenCalled(); | ||
amanmahajan7 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
onColumnResize.mockClear(); | ||
// if the width is the same, don't call onColumnResize | ||
await autoResize(col1); | ||
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); | ||
expect(onColumnResize).not.toHaveBeenCalled(); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@amanmahajan7
I think you better not use ref for the previous grid width. There is a bug with the initial render when:
This issue is reflected in my video from this comment: #3723 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check if this issues still exists in my PR? I think the issue you are describing was related to #3724 (comment)