From 09d8b625b592deb263a5a55b63475c67406039ed Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 27 Feb 2025 04:02:24 +0800 Subject: [PATCH 01/10] fix: columns resize sync --- src/hooks/useColumnWidths.ts | 75 ++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 2f70109a15..ddb01ef5d0 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 { startTransition, useLayoutEffect, useRef, useState } from 'react'; import type { CalculatedColumn, StateSetter } from '../types'; import type { DataGridProps } from '../DataGrid'; @@ -16,6 +15,9 @@ export function useColumnWidths( setMeasuredColumnWidths: StateSetter>, onColumnResize: DataGridProps['onColumnResize'] ) { + const [templateColumnsToMeasure, setTemplateColumnsToMeasure] = useState>( + () => new Map() + ); const prevGridWidthRef = useRef(gridWidth); const columnsCanFlex: boolean = columns.length === viewportColumns.length; // Allow columns to flex again when... @@ -23,7 +25,7 @@ export function useColumnWidths( // there is enough space for columns to flex and the grid was resized columnsCanFlex && gridWidth !== prevGridWidthRef.current; const newTemplateColumns = [...templateColumns]; - const columnsToMeasure: string[] = []; + const columnsToMeasure = new Set(); for (const { key, idx, width } of viewportColumns) { if ( @@ -32,15 +34,41 @@ export function useColumnWidths( !resizedColumnWidths.has(key) ) { newTemplateColumns[idx] = width; - columnsToMeasure.push(key); + columnsToMeasure.add(key); + } + + if (templateColumnsToMeasure.size > 0) { + const temp = templateColumnsToMeasure.get(key); + if (temp) { + newTemplateColumns[idx] = temp; + columnsToMeasure.add(key); + } else if (columnsCanFlex && typeof width === 'string' && !resizedColumnWidths.has(key)) { + newTemplateColumns[idx] = width; + } } } const gridTemplateColumns = newTemplateColumns.join(' '); useLayoutEffect(() => { - prevGridWidthRef.current = gridWidth; - updateMeasuredWidths(columnsToMeasure); + startTransition(() => { + prevGridWidthRef.current = gridWidth; + + for (const [resizingKey] of templateColumnsToMeasure) { + const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; + setTemplateColumnsToMeasure(new Map()); + setResizedColumnWidths((resizedColumnWidths) => { + const newResizedColumnWidths = new Map(resizedColumnWidths); + newResizedColumnWidths.set(resizingKey, measuredWidth); + return newResizedColumnWidths; + }); + + const column = columns.find((c) => c.key === resizingKey)!; + onColumnResize?.(column, measuredWidth); + } + + updateMeasuredWidths([...columnsToMeasure]); + }); }); function updateMeasuredWidths(columnsToMeasure: readonly string[]) { @@ -66,36 +94,15 @@ export function useColumnWidths( 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; - columnsToMeasure.push(key); - } - } - - gridRef.current!.style.gridTemplateColumns = newTemplateColumns.join(' '); - const measuredWidth = - typeof nextWidth === 'number' ? nextWidth : measureColumnWidth(gridRef, resizingKey)!; - - // TODO: remove - // need flushSync to keep frozen column offsets in sync - // we may be able to use `startTransition` or even `requestIdleCallback` instead - flushSync(() => { - setResizedColumnWidths((resizedColumnWidths) => { - const newResizedColumnWidths = new Map(resizedColumnWidths); - newResizedColumnWidths.set(resizingKey, measuredWidth); - return newResizedColumnWidths; - }); - updateMeasuredWidths(columnsToMeasure); + setTemplateColumnsToMeasure((templateColumnsToMeasure) => { + const newTemplateColumnsToMeasure = new Map(templateColumnsToMeasure); + newTemplateColumnsToMeasure.set( + resizingKey, + typeof nextWidth === 'number' ? `${nextWidth}px` : nextWidth + ); + return newTemplateColumnsToMeasure; }); - - onColumnResize?.(column, measuredWidth); } return { From db8761073086709b066c2519f2d9b137837ae0a2 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 27 Feb 2025 04:17:20 +0800 Subject: [PATCH 02/10] fix: remove extra set state calls --- 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 ddb01ef5d0..f306db1d1e 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -56,7 +56,6 @@ export function useColumnWidths( for (const [resizingKey] of templateColumnsToMeasure) { const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; - setTemplateColumnsToMeasure(new Map()); setResizedColumnWidths((resizedColumnWidths) => { const newResizedColumnWidths = new Map(resizedColumnWidths); newResizedColumnWidths.set(resizingKey, measuredWidth); @@ -68,6 +67,7 @@ export function useColumnWidths( } updateMeasuredWidths([...columnsToMeasure]); + setTemplateColumnsToMeasure(new Map()); }); }); From 015661adc7017330932eee44dcf2ebdb924c88f1 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 27 Feb 2025 04:23:48 +0800 Subject: [PATCH 03/10] fix: performance --- src/hooks/useColumnWidths.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index f306db1d1e..fa0b5c94a4 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -54,20 +54,24 @@ export function useColumnWidths( startTransition(() => { prevGridWidthRef.current = gridWidth; - for (const [resizingKey] of templateColumnsToMeasure) { - const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; - setResizedColumnWidths((resizedColumnWidths) => { - const newResizedColumnWidths = new Map(resizedColumnWidths); - newResizedColumnWidths.set(resizingKey, measuredWidth); - return newResizedColumnWidths; - }); - - const column = columns.find((c) => c.key === resizingKey)!; - onColumnResize?.(column, measuredWidth); + if (templateColumnsToMeasure.size > 0) { + for (const [resizingKey] of templateColumnsToMeasure) { + const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; + setResizedColumnWidths((resizedColumnWidths) => { + const newResizedColumnWidths = new Map(resizedColumnWidths); + newResizedColumnWidths.set(resizingKey, measuredWidth); + return newResizedColumnWidths; + }); + + const column = columns.find((c) => c.key === resizingKey)!; + onColumnResize?.(column, measuredWidth); + } + setTemplateColumnsToMeasure(new Map()); } - updateMeasuredWidths([...columnsToMeasure]); - setTemplateColumnsToMeasure(new Map()); + if (columnsToMeasure.size > 0) { + updateMeasuredWidths([...columnsToMeasure]); + } }); }); From 95b176a6d5c298c645d1601f0e0a83d32f063e6e Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 27 Feb 2025 09:49:33 +0800 Subject: [PATCH 04/10] fix: measure flexible columns --- 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 fa0b5c94a4..a3bf8ff6fc 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -41,10 +41,10 @@ export function useColumnWidths( const temp = templateColumnsToMeasure.get(key); if (temp) { newTemplateColumns[idx] = temp; - columnsToMeasure.add(key); } else if (columnsCanFlex && typeof width === 'string' && !resizedColumnWidths.has(key)) { newTemplateColumns[idx] = width; } + columnsToMeasure.add(key); } } From 71e7eeb2cb844f7c6670c39bd7fde544e568634f Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 28 Feb 2025 16:53:43 +0800 Subject: [PATCH 05/10] feat: add indication for measuring columns it makes possible to make css selector for applying special styles while measuring --- src/DataGrid.tsx | 4 ++-- src/hooks/useColumnWidths.ts | 1 + src/utils/renderMeasuringCells.tsx | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 5ef0e4b827..016c543198 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -432,7 +432,7 @@ export function DataGrid(props: DataGridPr bottomSummaryRows }); - const { gridTemplateColumns, handleColumnResize } = useColumnWidths( + const { gridTemplateColumns, columnsToMeasure, handleColumnResize } = useColumnWidths( columns, viewportColumns, templateColumns, @@ -1231,7 +1231,7 @@ export function DataGrid(props: DataGridPr {renderDragHandle()} {/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */} - {renderMeasuringCells(viewportColumns)} + {renderMeasuringCells(viewportColumns, columnsToMeasure)} {/* extra div is needed for row navigation in a treegrid */} {isTreeGrid && ( diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a3bf8ff6fc..400686e719 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -110,6 +110,7 @@ export function useColumnWidths( } return { + columnsToMeasure, gridTemplateColumns, handleColumnResize } as const; diff --git a/src/utils/renderMeasuringCells.tsx b/src/utils/renderMeasuringCells.tsx index ff2b88b86a..a045c23886 100644 --- a/src/utils/renderMeasuringCells.tsx +++ b/src/utils/renderMeasuringCells.tsx @@ -10,13 +10,17 @@ const measuringCellClassname = css` } `; -export function renderMeasuringCells(viewportColumns: readonly CalculatedColumn[]) { +export function renderMeasuringCells( + viewportColumns: readonly CalculatedColumn[], + columnsToMeasure: Set +) { return viewportColumns.map(({ key, idx, minWidth, maxWidth }) => (
)); } From a1c0b2548794e7363f988c42646b11af68ba3ab4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 3 Mar 2025 16:46:27 +0800 Subject: [PATCH 06/10] fix: use flag style attribute --- src/utils/renderMeasuringCells.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/renderMeasuringCells.tsx b/src/utils/renderMeasuringCells.tsx index a045c23886..587bb092f3 100644 --- a/src/utils/renderMeasuringCells.tsx +++ b/src/utils/renderMeasuringCells.tsx @@ -20,7 +20,7 @@ export function renderMeasuringCells( className={measuringCellClassname} style={{ gridColumnStart: idx + 1, minWidth, maxWidth }} data-measuring-cell-key={key} - data-measuring-cell={columnsToMeasure.has(key)} + data-measuring-cell={columnsToMeasure.has(key) || undefined} /> )); } From d02f66d555c95cf2576c1e3b3426dea4c4731df4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 3 Mar 2025 17:23:38 +0800 Subject: [PATCH 07/10] fix: skip measurement for manually resized columns --- src/HeaderRow.tsx | 3 ++- src/hooks/useColumnWidths.ts | 43 ++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 166e3efdaf..99345a3f66 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -2,6 +2,7 @@ import { memo, useId } from 'react'; import { css } from '@linaria/core'; import clsx from 'clsx'; +import type { ColumnResizeWidth } from './hooks'; import { getColSpan } from './utils'; import type { CalculatedColumn, Direction, Position } from './types'; import type { DataGridProps } from './DataGrid'; @@ -17,7 +18,7 @@ type SharedDataGridProps = Pick< export interface HeaderRowProps extends SharedDataGridProps { rowIdx: number; columns: readonly CalculatedColumn[]; - onColumnResize: (column: CalculatedColumn, width: number | 'max-content') => void; + onColumnResize: (column: CalculatedColumn, width: ColumnResizeWidth) => void; selectCell: (position: Position) => void; lastFrozenColumnIndex: number; selectedCellIdx: number | undefined; diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 400686e719..684d764039 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -3,6 +3,8 @@ import { startTransition, useLayoutEffect, useRef, useState } from 'react'; import type { CalculatedColumn, StateSetter } from '../types'; import type { DataGridProps } from '../DataGrid'; +export type ColumnResizeWidth = number | 'max-content'; + export function useColumnWidths( columns: readonly CalculatedColumn[], viewportColumns: readonly CalculatedColumn[], @@ -15,9 +17,9 @@ export function useColumnWidths( setMeasuredColumnWidths: StateSetter>, onColumnResize: DataGridProps['onColumnResize'] ) { - const [templateColumnsToMeasure, setTemplateColumnsToMeasure] = useState>( - () => new Map() - ); + const [resizedColumnsToMeasure, setResizedColumnsToMeasure] = useState< + Map + >(() => new Map()); const prevGridWidthRef = useRef(gridWidth); const columnsCanFlex: boolean = columns.length === viewportColumns.length; // Allow columns to flex again when... @@ -37,14 +39,20 @@ export function useColumnWidths( columnsToMeasure.add(key); } - if (templateColumnsToMeasure.size > 0) { - const temp = templateColumnsToMeasure.get(key); - if (temp) { - newTemplateColumns[idx] = temp; + if (resizedColumnsToMeasure.size > 0) { + const tempWidth = resizedColumnsToMeasure.get(key); + if (tempWidth !== undefined) { + if (typeof tempWidth === 'number') { + newTemplateColumns[idx] = `${tempWidth}px`; + columnsToMeasure.delete(key); + } else { + newTemplateColumns[idx] = tempWidth; + columnsToMeasure.add(key); + } } else if (columnsCanFlex && typeof width === 'string' && !resizedColumnWidths.has(key)) { newTemplateColumns[idx] = width; + columnsToMeasure.add(key); } - columnsToMeasure.add(key); } } @@ -54,8 +62,8 @@ export function useColumnWidths( startTransition(() => { prevGridWidthRef.current = gridWidth; - if (templateColumnsToMeasure.size > 0) { - for (const [resizingKey] of templateColumnsToMeasure) { + if (resizedColumnsToMeasure.size > 0) { + for (const [resizingKey] of resizedColumnsToMeasure) { const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; setResizedColumnWidths((resizedColumnWidths) => { const newResizedColumnWidths = new Map(resizedColumnWidths); @@ -66,7 +74,7 @@ export function useColumnWidths( const column = columns.find((c) => c.key === resizingKey)!; onColumnResize?.(column, measuredWidth); } - setTemplateColumnsToMeasure(new Map()); + setResizedColumnsToMeasure(new Map()); } if (columnsToMeasure.size > 0) { @@ -96,16 +104,13 @@ export function useColumnWidths( }); } - function handleColumnResize(column: CalculatedColumn, nextWidth: number | 'max-content') { + function handleColumnResize(column: CalculatedColumn, nextWidth: ColumnResizeWidth) { const { key: resizingKey } = column; - setTemplateColumnsToMeasure((templateColumnsToMeasure) => { - const newTemplateColumnsToMeasure = new Map(templateColumnsToMeasure); - newTemplateColumnsToMeasure.set( - resizingKey, - typeof nextWidth === 'number' ? `${nextWidth}px` : nextWidth - ); - return newTemplateColumnsToMeasure; + setResizedColumnsToMeasure((resizedColumnsToMeasure) => { + const newResizedColumnsToMeasure = new Map(resizedColumnsToMeasure); + newResizedColumnsToMeasure.set(resizingKey, nextWidth); + return newResizedColumnsToMeasure; }); } From cb5c1f376e204b398740b1368392a4240ef42ebd Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 3 Mar 2025 17:36:11 +0800 Subject: [PATCH 08/10] fix: remove `startTransition` to prevent flickering --- src/hooks/useColumnWidths.ts | 40 +++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 684d764039..a7d0a3a04d 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -1,4 +1,4 @@ -import { startTransition, useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; import type { CalculatedColumn, StateSetter } from '../types'; import type { DataGridProps } from '../DataGrid'; @@ -59,28 +59,26 @@ export function useColumnWidths( const gridTemplateColumns = newTemplateColumns.join(' '); useLayoutEffect(() => { - startTransition(() => { - prevGridWidthRef.current = gridWidth; - - if (resizedColumnsToMeasure.size > 0) { - for (const [resizingKey] of resizedColumnsToMeasure) { - const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; - setResizedColumnWidths((resizedColumnWidths) => { - const newResizedColumnWidths = new Map(resizedColumnWidths); - newResizedColumnWidths.set(resizingKey, measuredWidth); - return newResizedColumnWidths; - }); - - const column = columns.find((c) => c.key === resizingKey)!; - onColumnResize?.(column, measuredWidth); - } - setResizedColumnsToMeasure(new Map()); - } + prevGridWidthRef.current = gridWidth; - if (columnsToMeasure.size > 0) { - updateMeasuredWidths([...columnsToMeasure]); + if (resizedColumnsToMeasure.size > 0) { + for (const [resizingKey] of resizedColumnsToMeasure) { + const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; + setResizedColumnWidths((resizedColumnWidths) => { + const newResizedColumnWidths = new Map(resizedColumnWidths); + newResizedColumnWidths.set(resizingKey, measuredWidth); + return newResizedColumnWidths; + }); + + const column = columns.find((c) => c.key === resizingKey)!; + onColumnResize?.(column, measuredWidth); } - }); + setResizedColumnsToMeasure(new Map()); + } + + if (columnsToMeasure.size > 0) { + updateMeasuredWidths([...columnsToMeasure]); + } }); function updateMeasuredWidths(columnsToMeasure: readonly string[]) { From f5c658fc524f0ace1572db7f5eff6366afa37f86 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 18 Mar 2025 18:48:02 +0800 Subject: [PATCH 09/10] fix: persist grid layout after measurement --- src/hooks/useColumnWidths.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index a7d0a3a04d..900b8ad386 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -20,12 +20,12 @@ export function useColumnWidths( const [resizedColumnsToMeasure, setResizedColumnsToMeasure] = useState< Map >(() => new Map()); - const prevGridWidthRef = useRef(gridWidth); + const [prevGridWidth, setPrevGridWidth] = useState(gridWidth); const columnsCanFlex: boolean = columns.length === viewportColumns.length; // Allow columns to flex again when... const ignorePreviouslyMeasuredColumns: boolean = // there is enough space for columns to flex and the grid was resized - columnsCanFlex && gridWidth !== prevGridWidthRef.current; + columnsCanFlex && gridWidth !== prevGridWidth; const newTemplateColumns = [...templateColumns]; const columnsToMeasure = new Set(); @@ -59,12 +59,15 @@ export function useColumnWidths( const gridTemplateColumns = newTemplateColumns.join(' '); useLayoutEffect(() => { - prevGridWidthRef.current = gridWidth; + setPrevGridWidth(gridWidth); if (resizedColumnsToMeasure.size > 0) { for (const [resizingKey] of resizedColumnsToMeasure) { const measuredWidth = measureColumnWidth(gridRef, resizingKey)!; setResizedColumnWidths((resizedColumnWidths) => { + if (resizedColumnWidths.get(resizingKey) === measuredWidth) { + return resizedColumnWidths; + } const newResizedColumnWidths = new Map(resizedColumnWidths); newResizedColumnWidths.set(resizingKey, measuredWidth); return newResizedColumnWidths; @@ -77,12 +80,12 @@ export function useColumnWidths( } if (columnsToMeasure.size > 0) { - updateMeasuredWidths([...columnsToMeasure]); + updateMeasuredWidths(columnsToMeasure); } }); - function updateMeasuredWidths(columnsToMeasure: readonly string[]) { - if (columnsToMeasure.length === 0) return; + function updateMeasuredWidths(columnsToMeasure: Set) { + if (columnsToMeasure.size === 0) return; setMeasuredColumnWidths((measuredColumnWidths) => { const newMeasuredColumnWidths = new Map(measuredColumnWidths); From 631ea06324b9b1f4d85d17550ac37d681c84bbc4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 19 Mar 2025 16:30:20 +0800 Subject: [PATCH 10/10] chore: fix linting --- 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 900b8ad386..b340a17d80 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useState } from 'react'; import type { CalculatedColumn, StateSetter } from '../types'; import type { DataGridProps } from '../DataGrid';