diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index d73e568ad5..e372cf1c28 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -439,7 +439,7 @@ export function DataGrid(props: DataGridPr bottomSummaryRows }); - const { gridTemplateColumns, handleColumnResize } = useColumnWidths( + const { gridTemplateColumns, columnsToMeasure, handleColumnResize } = useColumnWidths( columns, viewportColumns, templateColumns, @@ -1238,7 +1238,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/HeaderRow.tsx b/src/HeaderRow.tsx index 7b4c7cb755..e3d2ca09f2 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 2f70109a15..b340a17d80 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -1,9 +1,10 @@ -import { useLayoutEffect, useRef } from 'react'; -import { flushSync } from 'react-dom'; +import { useLayoutEffect, 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[], @@ -16,14 +17,17 @@ export function useColumnWidths( setMeasuredColumnWidths: StateSetter>, onColumnResize: DataGridProps['onColumnResize'] ) { - const prevGridWidthRef = useRef(gridWidth); + const [resizedColumnsToMeasure, setResizedColumnsToMeasure] = useState< + Map + >(() => new Map()); + 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: string[] = []; + const columnsToMeasure = new Set(); for (const { key, idx, width } of viewportColumns) { if ( @@ -32,19 +36,56 @@ export function useColumnWidths( !resizedColumnWidths.has(key) ) { newTemplateColumns[idx] = width; - columnsToMeasure.push(key); + columnsToMeasure.add(key); + } + + 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); + } } } const gridTemplateColumns = newTemplateColumns.join(' '); useLayoutEffect(() => { - prevGridWidthRef.current = gridWidth; - updateMeasuredWidths(columnsToMeasure); + 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; + }); + + 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[]) { - if (columnsToMeasure.length === 0) return; + function updateMeasuredWidths(columnsToMeasure: Set) { + if (columnsToMeasure.size === 0) return; setMeasuredColumnWidths((measuredColumnWidths) => { const newMeasuredColumnWidths = new Map(measuredColumnWidths); @@ -64,41 +105,18 @@ export function useColumnWidths( }); } - function handleColumnResize(column: CalculatedColumn, nextWidth: number | 'max-content') { + function handleColumnResize(column: CalculatedColumn, nextWidth: ColumnResizeWidth) { 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); + setResizedColumnsToMeasure((resizedColumnsToMeasure) => { + const newResizedColumnsToMeasure = new Map(resizedColumnsToMeasure); + newResizedColumnsToMeasure.set(resizingKey, nextWidth); + return newResizedColumnsToMeasure; }); - - onColumnResize?.(column, measuredWidth); } return { + columnsToMeasure, gridTemplateColumns, handleColumnResize } as const; diff --git a/src/utils/renderMeasuringCells.tsx b/src/utils/renderMeasuringCells.tsx index ff2b88b86a..587bb092f3 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 }) => (
)); }