From 005c9f22d5c3e92209d2e71e7b4c432f08f970ab Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Wed, 19 Mar 2025 16:58:57 -0500 Subject: [PATCH 01/39] Use state to handle column auto resize --- src/hooks/useColumnWidths.ts | 60 +++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 2f70109a15..b6e5160c47 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -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( setMeasuredColumnWidths: StateSetter>, onColumnResize: DataGridProps['onColumnResize'] ) { + const [columnToAutoresize, setColumnToAutoResize] = useState(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( 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) @@ -62,40 +65,53 @@ export function useColumnWidths( return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths; }); + + if (columnToAutoresize !== null) { + setColumnToAutoResize(null); + setResizedColumnWidths((resizedColumnWidths) => { + const newResizedColumnWidths = new Map(resizedColumnWidths); + newResizedColumnWidths.set( + columnToAutoresize, + measureColumnWidth(gridRef, columnToAutoresize)! + ); + return newResizedColumnWidths; + }); + } } function handleColumnResize(column: CalculatedColumn, 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; + for (const { key, width } of viewportColumns) { + if ( + resizingKey !== key && + columnsCanFlex && + typeof width === 'string' && + !resizedColumnWidths.has(key) + ) { columnsToMeasure.push(key); } } - gridRef.current!.style.gridTemplateColumns = newTemplateColumns.join(' '); - const measuredWidth = - typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey)!; + 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 { From b66f3bd6204903486f676414dc27bcc65c2c5524 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Wed, 19 Mar 2025 18:07:22 -0500 Subject: [PATCH 02/39] Optimize --- src/hooks/useColumnWidths.ts | 69 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index b6e5160c47..9775535620 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -15,7 +15,7 @@ export function useColumnWidths( setMeasuredColumnWidths: StateSetter>, onColumnResize: DataGridProps['onColumnResize'] ) { - const [columnToAutoresize, setColumnToAutoResize] = useState(null); + const [columnToAutoResize, setColumnToAutoResize] = useState(null); const prevGridWidthRef = useRef(gridWidth); const columnsCanFlex: boolean = columns.length === viewportColumns.length; // Allow columns to flex again when... @@ -26,7 +26,7 @@ export function useColumnWidths( const columnsToMeasure: string[] = []; for (const { key, idx, width } of viewportColumns) { - if (key === columnToAutoresize) { + if (key === columnToAutoResize) { newTemplateColumns[idx] = 'max-content'; columnsToMeasure.push(key); } else if ( @@ -47,34 +47,37 @@ export function useColumnWidths( }); function updateMeasuredWidths(columnsToMeasure: readonly string[]) { - if (columnsToMeasure.length === 0) return; - - 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); + 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); + } } - } - return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths; - }); + return hasChanges ? newMeasuredColumnWidths : measuredColumnWidths; + }); + } - if (columnToAutoresize !== null) { + if (columnToAutoResize !== null) { setColumnToAutoResize(null); setResizedColumnWidths((resizedColumnWidths) => { - const newResizedColumnWidths = new Map(resizedColumnWidths); - newResizedColumnWidths.set( - columnToAutoresize, - measureColumnWidth(gridRef, columnToAutoresize)! - ); - return newResizedColumnWidths; + 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); + return newResizedColumnWidths; + } + return resizedColumnWidths; }); } } @@ -94,13 +97,15 @@ export function useColumnWidths( } } - setMeasuredColumnWidths((measuredColumnWidths) => { - const newMeasuredColumnWidths = new Map(measuredColumnWidths); - for (const columnKey of columnsToMeasure) { - newMeasuredColumnWidths.delete(columnKey); - } - return newMeasuredColumnWidths; - }); + if (columnsToMeasure.length > 0) { + setMeasuredColumnWidths((measuredColumnWidths) => { + const newMeasuredColumnWidths = new Map(measuredColumnWidths); + for (const columnKey of columnsToMeasure) { + newMeasuredColumnWidths.delete(columnKey); + } + return newMeasuredColumnWidths; + }); + } if (typeof nextWidth === 'number') { setResizedColumnWidths((resizedColumnWidths) => { From 51c2bdd209fd0abc3a13ee9adfdec3af1bd9a6e5 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 08:32:50 -0500 Subject: [PATCH 03/39] Cleanup --- src/hooks/useColumnWidths.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 9775535620..a558c8d4ed 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -43,10 +43,10 @@ export function useColumnWidths( useLayoutEffect(() => { prevGridWidthRef.current = gridWidth; - updateMeasuredWidths(columnsToMeasure); + updateMeasuredWidths(); }); - function updateMeasuredWidths(columnsToMeasure: readonly string[]) { + function updateMeasuredWidths() { if (columnsToMeasure.length > 0) { setMeasuredColumnWidths((measuredColumnWidths) => { const newMeasuredColumnWidths = new Map(measuredColumnWidths); From d4afa2ba342de86b77346e418e98a5c8584b1e7c Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 08:52:18 -0500 Subject: [PATCH 04/39] Test --- src/hooks/useColumnWidths.ts | 1 + test/browser/column/resizable.test.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a558c8d4ed..e61a48dd19 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -86,6 +86,7 @@ export function useColumnWidths( const { key: resizingKey } = column; const columnsToMeasure: string[] = []; + // remeasure all columns that can flex and are not resized by the user for (const { key, width } of viewportColumns) { if ( resizingKey !== key && diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index afd227bf25..e5bbb9aef9 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -94,6 +94,7 @@ test('should use the minWidth if specified', async () => { }); test('should auto resize column when resize handle is double clicked', async () => { + const onColumnResize = vi.fn(); setup({ columns, rows: [ @@ -101,13 +102,15 @@ test('should auto resize column when resize handle is double clicked', async () col1: 1, col2: 'a'.repeat(50) } - ] + ], + onColumnResize }); const [, col2] = getHeaderCells(); const grid = getGrid(); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); await autoResize(col2); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 327.703px' }); + expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(col2), 327.703); }); test('should use the maxWidth if specified on auto resize', async () => { From ccaa4376e3d0193227f9fc2e3ff9e1281e2545cc Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 09:30:49 -0500 Subject: [PATCH 05/39] Fix tests --- test/browser/column/resizable.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index e5bbb9aef9..8a2dca8e6f 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -110,7 +110,8 @@ test('should auto resize column when resize handle is double clicked', async () await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); await autoResize(col2); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 327.703px' }); - expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(col2), 327.703); + // 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 () => { From 69c46a221de67a58f662291d8cd01be8eb43bef1 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 09:56:41 -0500 Subject: [PATCH 06/39] Add flex columns test, make tests more robust --- test/browser/column/resizable.test.tsx | 53 +++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 8a2dca8e6f..0860456933 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -66,10 +66,10 @@ 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({ columns, rows: [], onColumnResize }); - const [, col2] = getHeaderCells(); const grid = getGrid(); expect(onColumnResize).not.toHaveBeenCalled(); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' }); + const [, col2] = getHeaderCells(); await resize({ column: col2, resizeBy: -50 }); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 150px' }); expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 150); @@ -77,18 +77,18 @@ test('should resize column when dragging the handle', async () => { test('should use the maxWidth if specified', async () => { setup({ 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({ 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' }); }); @@ -105,9 +105,9 @@ test('should auto resize column when resize handle is double clicked', async () ], 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 @@ -124,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' }); }); @@ -141,9 +141,50 @@ 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 () => { + 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) + } + ] + }); + 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' }); +}); From f60e759e6ea2caf9ce48490f20e5650e5701be1d Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 10:03:07 -0500 Subject: [PATCH 07/39] Improve coverage --- test/browser/column/resizable.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 0860456933..ea622b5fc9 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -187,4 +187,6 @@ test('should remeasure flex columns when resizing a column', async () => { const [col1] = getHeaderCells(); await autoResize(col1); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); + await autoResize(col1); + await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); }); From f736a3b0a73251e7b0fd14f99240d29b5cdaf805 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 10:14:04 -0500 Subject: [PATCH 08/39] test `onColumnResize` --- test/browser/column/resizable.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index ea622b5fc9..7f2fd8c1fd 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -149,6 +149,7 @@ test('should use the minWidth if specified on auto resize', async () => { }); test('should remeasure flex columns when resizing a column', async () => { + const onColumnResize = vi.fn(); setup< { readonly col1: string; @@ -180,13 +181,18 @@ test('should remeasure flex columns when resizing a column', async () => { 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(); + 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(); }); From 0e909061eb7bf3cdf69297d920ecd9f59de1a629 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 10:18:13 -0500 Subject: [PATCH 09/39] Tweak --- src/hooks/useColumnWidths.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index e61a48dd19..a103aa101f 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -89,8 +89,8 @@ export function useColumnWidths( // remeasure all columns that can flex and are not resized by the user for (const { key, width } of viewportColumns) { if ( - resizingKey !== key && columnsCanFlex && + resizingKey !== key && typeof width === 'string' && !resizedColumnWidths.has(key) ) { From 523ec5a827ca931b899fb3f70dc892ff3daeb8ae Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 10:27:44 -0500 Subject: [PATCH 10/39] Rename helper --- src/hooks/useColumnWidths.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a103aa101f..de095357d9 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -43,10 +43,10 @@ export function useColumnWidths( useLayoutEffect(() => { prevGridWidthRef.current = gridWidth; - updateMeasuredWidths(); + updateMeasuredAndResizedWidths(); }); - function updateMeasuredWidths() { + function updateMeasuredAndResizedWidths() { if (columnsToMeasure.length > 0) { setMeasuredColumnWidths((measuredColumnWidths) => { const newMeasuredColumnWidths = new Map(measuredColumnWidths); From 8a6bb2873ccf76cd340cd6139b0278d4426c1924 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 20 Mar 2025 11:09:22 -0500 Subject: [PATCH 11/39] Tweak comment --- test/browser/column/resizable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 7f2fd8c1fd..5a9e8189c8 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -191,7 +191,7 @@ test('should remeasure flex columns when resizing a column', async () => { await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); expect(onColumnResize).toHaveBeenCalled(); onColumnResize.mockClear(); - // if the width is the same, don't call onColumnResize + // onColumnResize is not called if width is not changed await autoResize(col1); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '79.1406px 919.422px 919.438px' }); expect(onColumnResize).not.toHaveBeenCalled(); From 38e8d2f2756e28191c8d7d2de0e368dc201080d8 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 21 Mar 2025 11:54:25 -0500 Subject: [PATCH 12/39] Initial commit --- src/HeaderCell.tsx | 4 +--- src/hooks/useColumnWidths.ts | 14 +++++++++----- src/utils/index.ts | 20 +++++++++++++++++++- website/routes/CommonFeatures.tsx | 1 + website/routes/ContextMenu.tsx | 18 ++++++++++++++---- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index eeb00441b4..0246918dc8 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -3,7 +3,6 @@ import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; import { - clampColumnWidth, getCellClassname, getCellStyle, getHeaderCellRowSpan, @@ -124,8 +123,7 @@ export default function HeaderCell({ function onPointerMove(event: PointerEvent) { const { width, right, left } = headerCell.getBoundingClientRect(); - let newWidth = isRtl ? right + offset - event.clientX : event.clientX + offset - left; - newWidth = clampColumnWidth(newWidth, column); + const newWidth = isRtl ? right + offset - event.clientX : event.clientX + offset - left; if (width > 0 && newWidth !== width) { onColumnResize(column, newWidth); } diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index de095357d9..e97f5c3935 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -1,5 +1,6 @@ import { useLayoutEffect, useRef, useState } from 'react'; +import { clampColumnWidth, getColumnWidthForMeasurement } from '../utils'; import type { CalculatedColumn, StateSetter } from '../types'; import type { DataGridProps } from '../DataGrid'; @@ -25,16 +26,18 @@ export function useColumnWidths( const newTemplateColumns = [...templateColumns]; const columnsToMeasure: string[] = []; - for (const { key, idx, width } of viewportColumns) { + for (const column of viewportColumns) { + const { key, idx, width } = column; if (key === columnToAutoResize) { - newTemplateColumns[idx] = 'max-content'; + newTemplateColumns[idx] = getColumnWidthForMeasurement('max-content', column); columnsToMeasure.push(key); } else if ( typeof width === 'string' && (ignorePreviouslyMeasuredColumns || !measuredColumnWidths.has(key)) && + // If the column is resized by the user, we don't want to measure it again !resizedColumnWidths.has(key) ) { - newTemplateColumns[idx] = width; + newTemplateColumns[idx] = getColumnWidthForMeasurement(width, column); columnsToMeasure.push(key); } } @@ -109,12 +112,13 @@ export function useColumnWidths( } if (typeof nextWidth === 'number') { + const clampedNextWidth = clampColumnWidth(nextWidth, column); setResizedColumnWidths((resizedColumnWidths) => { const newResizedColumnWidths = new Map(resizedColumnWidths); - newResizedColumnWidths.set(resizingKey, nextWidth); + newResizedColumnWidths.set(resizingKey, clampedNextWidth); return newResizedColumnWidths; }); - onColumnResize?.(column, nextWidth); + onColumnResize?.(column, clampedNextWidth); } else { setColumnToAutoResize(resizingKey); } diff --git a/src/utils/index.ts b/src/utils/index.ts index f6bd990888..0f58f3b71c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -21,7 +21,7 @@ export function assertIsValidKeyGetter( export function clampColumnWidth( width: number, { minWidth, maxWidth }: CalculatedColumn -): number { +) { width = max(width, minWidth); // ignore maxWidth if it less than minWidth @@ -32,6 +32,24 @@ export function clampColumnWidth( return width; } +export function getColumnWidthForMeasurement( + width: string, + { minWidth, maxWidth }: CalculatedColumn +) { + if ( + maxWidth != null && + // ignore maxWidth if it less than minWidth + maxWidth >= minWidth + ) { + // fit-content = min(max-width, max(auto, ${maxWidth}px)) + return width === 'max-content' + ? `fit-content(${maxWidth}px)` + : `minmax(${width}, ${maxWidth}px)`; + } + + return width; +} + export function getHeaderCellRowSpan( column: CalculatedColumnOrColumnGroup, rowIdx: number diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index b938773043..86d47de39d 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -117,6 +117,7 @@ function getColumns( { key: 'country', name: 'Country', + maxWidth: 150, renderEditCell: (p) => ( Date: Fri, 21 Mar 2025 16:37:09 -0500 Subject: [PATCH 21/39] Remove width --- website/routes/CommonFeatures.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index 453fb06e9e..86d47de39d 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -117,7 +117,6 @@ function getColumns( { key: 'country', name: 'Country', - width: 800, maxWidth: 150, renderEditCell: (p) => ( Date: Wed, 9 Apr 2025 09:24:22 -0500 Subject: [PATCH 36/39] Fix tests --- website/routes/CommonFeatures.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index 86d47de39d..f27c945f26 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -96,6 +96,7 @@ function getColumns( { key: 'title', name: 'Task', + maxWidth: 200, frozen: true, renderEditCell: textEditor, renderSummaryCell({ row }) { From 852a7b516fb09375f08d4c939fb6d5428d974732 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Wed, 9 Apr 2025 09:24:40 -0500 Subject: [PATCH 37/39] Fix tests --- src/hooks/useColumnWidths.ts | 3 +-- test/browser/column/resizable.test.tsx | 2 +- website/routes/CommonFeatures.tsx | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a4402913b7..a000d73859 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -110,8 +110,7 @@ export function useColumnWidths( if (onColumnResize) { const previousWidth = resizedColumnWidths.get(resizingKey); - const newWidth = - typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey); + const newWidth = measureColumnWidth(gridRef, resizingKey); if (newWidth !== undefined && newWidth !== previousWidth) { onColumnResize(column, newWidth); } diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 08c0cbe6ac..074de1742e 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -72,7 +72,7 @@ test('should resize column when dragging the handle', async () => { const [, col2] = getHeaderCells(); await resize({ column: col2, resizeBy: -50 }); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 150px' }); - expect(onColumnResize).toHaveBeenCalledWith(expect.objectContaining(columns[1]), 150); + expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 150); }); test('should use the maxWidth if specified', async () => { diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index f27c945f26..86d47de39d 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -96,7 +96,6 @@ function getColumns( { key: 'title', name: 'Task', - maxWidth: 200, frozen: true, renderEditCell: textEditor, renderSummaryCell({ row }) { From b79becf883bbb73f90726c7121286dba1d4dcd3c Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 10 Apr 2025 11:39:49 -0500 Subject: [PATCH 38/39] Update test/browser/column/resizable.test.tsx Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> --- test/browser/column/resizable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index 074de1742e..10b0104c12 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -84,7 +84,7 @@ test('should use the maxWidth if specified', async () => { const [, col2] = getHeaderCells(); await resize({ column: col2, resizeBy: 1000 }); await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' }); - expect(onColumnResize).toHaveBeenCalledWith(expect.objectContaining(columns[1]), 400); + expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 400); }); test('should use the minWidth if specified', async () => { From 96887bb1bc52117499c14567f7604c07498ebffe Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 10 Apr 2025 17:22:42 -0500 Subject: [PATCH 39/39] Rename --- src/hooks/useColumnWidths.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a000d73859..0e12b56c50 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -146,7 +146,7 @@ function getColumnWidthForMeasurement( : `max(${minWidth}px, ${widthWithUnit})`; // clamp() and max() do not handle all the css grid column width values - if (isValidCSSGridColumnWidth(clampedWidth)) { + if (isValidCSSGridColumn(clampedWidth)) { return clampedWidth; } @@ -161,14 +161,14 @@ function getColumnWidthForMeasurement( // We are setting maxWidth on the measuring cell but the browser only applies // it after all the widths are calculated. This results in left over space in some cases. const minMaxWidth = `minmax(${widthWithUnit}, ${maxWidth}px)`; - if (isValidCSSGridColumnWidth(minMaxWidth)) { + if (isValidCSSGridColumn(minMaxWidth)) { return minMaxWidth; } } - return isValidCSSGridColumnWidth(widthWithUnit) ? widthWithUnit : 'auto'; + return isValidCSSGridColumn(widthWithUnit) ? widthWithUnit : 'auto'; } -function isValidCSSGridColumnWidth(width: string) { +function isValidCSSGridColumn(width: string) { return CSS.supports('grid-template-columns', width); }