diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 62dd26be2d..78da367fe4 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -7,7 +7,7 @@ import { rootClassname, viewportDraggingClassname, focusSinkClassname, - cellAutoResizeClassname, + autosizeColumnsClassname, rowSelected, rowSelectedWithFrozenCell } from './style'; @@ -282,7 +282,7 @@ function DataGrid( /** * computed values */ - const [gridRef, gridWidth, gridHeight] = useGridDimensions(); + const [gridRef, gridWidth, gridHeight, isWidthInitialized] = useGridDimensions(); const headerRowsCount = 1; const topSummaryRowsCount = topSummaryRows?.length ?? 0; const bottomSummaryRowsCount = bottomSummaryRows?.length ?? 0; @@ -355,7 +355,7 @@ function DataGrid( enableVirtualization }); - const viewportColumns = useViewportColumns({ + const { viewportColumns, flexWidthViewportColumns } = useViewportColumns({ columns, colSpanColumns, colOverscanStartIdx, @@ -366,6 +366,7 @@ function DataGrid( rows, topSummaryRows, bottomSummaryRows, + columnWidths, isGroupRow }); @@ -432,6 +433,25 @@ function DataGrid( } }); + useLayoutEffect(() => { + if (!isWidthInitialized || flexWidthViewportColumns.length === 0) return; + const newColumnWidths = new Map(); + for (const column of flexWidthViewportColumns) { + const columnElement = gridRef.current!.querySelector( + `[aria-colindex="${column.idx + 1}"]` + ); + if (columnElement) { + // Set the actual width of the column after it is rendered + const { width } = columnElement.getBoundingClientRect(); + newColumnWidths.set(column.key, width); + } + } + if (newColumnWidths.size === 0) return; + setColumnWidths((columnWidths) => { + return new Map([...columnWidths, ...newColumnWidths]); + }); + }, [isWidthInitialized, flexWidthViewportColumns, gridRef]); + useLayoutEffect(() => { if (autoResizeColumn === null) return; const columnElement = gridRef.current!.querySelector( @@ -465,8 +485,8 @@ function DataGrid( * callbacks */ const handleColumnResize = useCallback( - (column: CalculatedColumn, width: number | 'auto') => { - if (width === 'auto') { + (column: CalculatedColumn, width: number | 'max-content') => { + if (width === 'max-content') { setAutoResizeColumn(column); return; } @@ -909,10 +929,16 @@ function DataGrid( } function getLayoutCssVars() { - if (autoResizeColumn === null) return layoutCssVars; + if (autoResizeColumn === null && flexWidthViewportColumns.length === 0) return layoutCssVars; const { gridTemplateColumns } = layoutCssVars; const newSizes = gridTemplateColumns.split(' '); - newSizes[autoResizeColumn.idx] = 'max-content'; + if (autoResizeColumn !== null) { + newSizes[autoResizeColumn.idx] = 'max-content'; + } + for (const column of flexWidthViewportColumns) { + newSizes[column.idx] = column.width as string; + } + return { ...layoutCssVars, gridTemplateColumns: newSizes.join(' ') @@ -1146,7 +1172,8 @@ function DataGrid( rootClassname, { [viewportDraggingClassname]: isDragging, - [cellAutoResizeClassname]: autoResizeColumn !== null + [autosizeColumnsClassname]: + autoResizeColumn !== null || flexWidthViewportColumns.length > 0 }, className )} diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 7a6fa7b0fc..2d8a70a3f6 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -155,7 +155,7 @@ export default function HeaderCell({ return; } - onColumnResize(column, 'auto'); + onColumnResize(column, 'max-content'); } function handleFocus(event: React.FocusEvent) { diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 8ab8e262ea..8783a4f682 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -17,7 +17,7 @@ export interface HeaderRowProps extends SharedDataGr columns: readonly CalculatedColumn[]; allRowsSelected: boolean; onAllRowsSelectionChange: (checked: boolean) => void; - onColumnResize: (column: CalculatedColumn, width: number | 'auto') => void; + onColumnResize: (column: CalculatedColumn, width: number | 'max-content') => void; selectCell: (columnIdx: number) => void; lastFrozenColumnIndex: number; selectedCellIdx: number | undefined; diff --git a/src/hooks/useCalculatedColumns.ts b/src/hooks/useCalculatedColumns.ts index e843edafce..7d7c40e3f4 100644 --- a/src/hooks/useCalculatedColumns.ts +++ b/src/hooks/useCalculatedColumns.ts @@ -4,7 +4,7 @@ import type { CalculatedColumn, Column, Maybe } from '../types'; import type { DataGridProps } from '../DataGrid'; import { valueFormatter, toggleGroupFormatter } from '../formatters'; import { SELECT_COLUMN_KEY } from '../Columns'; -import { clampColumnWidth, floor, max, min, round } from '../utils'; +import { clampColumnWidth, max, min } from '../utils'; type Mutable = { -readonly [P in keyof T]: T[P]; @@ -15,6 +15,9 @@ interface ColumnMetric { left: number; } +const DEFAULT_COLUMN_WIDTH = 'auto'; +const DEFAULT_COLUMN_MIN_WIDTH = 80; + interface CalculatedColumnsArgs extends Pick, 'defaultColumnOptions'> { rawColumns: readonly Column[]; rawGroupBy: Maybe; @@ -33,8 +36,8 @@ export function useCalculatedColumns({ rawGroupBy, enableVirtualization }: CalculatedColumnsArgs) { - const defaultWidth = defaultColumnOptions?.width; - const defaultMinWidth = defaultColumnOptions?.minWidth ?? 80; + const defaultWidth = defaultColumnOptions?.width ?? DEFAULT_COLUMN_WIDTH; + const defaultMinWidth = defaultColumnOptions?.minWidth ?? DEFAULT_COLUMN_MIN_WIDTH; const defaultMaxWidth = defaultColumnOptions?.maxWidth; const defaultFormatter = defaultColumnOptions?.formatter ?? valueFormatter; const defaultSortable = defaultColumnOptions?.sortable ?? false; @@ -148,38 +151,19 @@ export function useCalculatedColumns({ let left = 0; let totalFrozenColumnWidth = 0; let templateColumns = ''; - let allocatedWidth = 0; - let unassignedColumnsCount = 0; for (const column of columns) { - let width = getSpecifiedWidth(column, columnWidths, viewportWidth); - - if (width === undefined) { - unassignedColumnsCount++; - } else { + let width = columnWidths.get(column.key) ?? column.width; + if (typeof width === 'number') { width = clampColumnWidth(width, column); - allocatedWidth += width; - columnMetrics.set(column, { width, left: 0 }); - } - } - - for (const column of columns) { - let width: number; - if (columnMetrics.has(column)) { - const columnMetric = columnMetrics.get(column)!; - columnMetric.left = left; - ({ width } = columnMetric); } else { - // avoid decimals as subpixel positioning can lead to cell borders not being displayed - const unallocatedWidth = viewportWidth - allocatedWidth; - const unallocatedColumnWidth = round(unallocatedWidth / unassignedColumnsCount); - width = clampColumnWidth(unallocatedColumnWidth, column); - allocatedWidth += width; - unassignedColumnsCount--; - columnMetrics.set(column, { width, left }); + // This is a placeholder width so we can continue to use virtualization. + // The actual value is set after the column is rendered + width = column.minWidth; } - left += width; templateColumns += `${width}px `; + columnMetrics.set(column, { width, left }); + left += width; } if (lastFrozenColumnIndex !== -1) { @@ -197,7 +181,7 @@ export function useCalculatedColumns({ } return { layoutCssVars, totalFrozenColumnWidth, columnMetrics }; - }, [columnWidths, columns, viewportWidth, lastFrozenColumnIndex]); + }, [columnWidths, columns, lastFrozenColumnIndex]); const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { if (!enableVirtualization) { @@ -265,22 +249,3 @@ export function useCalculatedColumns({ groupBy }; } - -function getSpecifiedWidth( - { key, width }: Column, - columnWidths: ReadonlyMap, - viewportWidth: number -): number | undefined { - if (columnWidths.has(key)) { - // Use the resized width if available - return columnWidths.get(key); - } - - if (typeof width === 'number') { - return width; - } - if (typeof width === 'string' && /^\d+%$/.test(width)) { - return floor((viewportWidth * parseInt(width, 10)) / 100); - } - return undefined; -} diff --git a/src/hooks/useGridDimensions.ts b/src/hooks/useGridDimensions.ts index 749ba42004..19597868ff 100644 --- a/src/hooks/useGridDimensions.ts +++ b/src/hooks/useGridDimensions.ts @@ -6,11 +6,13 @@ import { ceil } from '../utils'; export function useGridDimensions(): [ ref: React.RefObject, width: number, - height: number + height: number, + isWidthInitialized: boolean ] { const gridRef = useRef(null); const [inlineSize, setInlineSize] = useState(1); const [blockSize, setBlockSize] = useState(1); + const [isWidthInitialized, setWidthInitialized] = useState(false); useLayoutEffect(() => { const { ResizeObserver } = window; @@ -31,6 +33,7 @@ export function useGridDimensions(): [ const size = entries[0].contentBoxSize[0]; setInlineSize(handleDevicePixelRatio(size.inlineSize)); setBlockSize(size.blockSize); + setWidthInitialized(true); }); resizeObserver.observe(gridRef.current!); @@ -39,7 +42,7 @@ export function useGridDimensions(): [ }; }, []); - return [gridRef, inlineSize, blockSize]; + return [gridRef, inlineSize, blockSize, isWidthInitialized]; } // TODO: remove once fixed upstream diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 17cf978922..82e762b290 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -14,6 +14,7 @@ interface ViewportColumnsArgs { lastFrozenColumnIndex: number; rowOverscanStartIdx: number; rowOverscanEndIdx: number; + columnWidths: ReadonlyMap; isGroupRow: (row: R | GroupRow) => row is GroupRow; } @@ -28,6 +29,7 @@ export function useViewportColumns({ lastFrozenColumnIndex, rowOverscanStartIdx, rowOverscanEndIdx, + columnWidths, isGroupRow }: ViewportColumnsArgs) { // find the column that spans over a column within the visible columns range and adjust colOverscanStartIdx @@ -104,15 +106,31 @@ export function useViewportColumns({ isGroupRow ]); - return useMemo((): readonly CalculatedColumn[] => { + const { viewportColumns, flexWidthViewportColumns } = useMemo((): { + viewportColumns: readonly CalculatedColumn[]; + flexWidthViewportColumns: readonly CalculatedColumn[]; + } => { const viewportColumns: CalculatedColumn[] = []; + const flexWidthViewportColumns: CalculatedColumn[] = []; for (let colIdx = 0; colIdx <= colOverscanEndIdx; colIdx++) { const column = columns[colIdx]; if (colIdx < startIdx && !column.frozen) continue; viewportColumns.push(column); + if (typeof column.width === 'string') { + flexWidthViewportColumns.push(column); + } } - return viewportColumns; + return { viewportColumns, flexWidthViewportColumns }; }, [startIdx, colOverscanEndIdx, columns]); + + const unsizedFlexWidthViewportColumns = useMemo((): readonly CalculatedColumn[] => { + return flexWidthViewportColumns.filter((column) => !columnWidths.has(column.key)); + }, [flexWidthViewportColumns, columnWidths]); + + return { + viewportColumns, + flexWidthViewportColumns: unsizedFlexWidthViewportColumns + }; } diff --git a/src/style/cell.ts b/src/style/cell.ts index 4dea068ed1..ddfde6be25 100644 --- a/src/style/cell.ts +++ b/src/style/cell.ts @@ -33,7 +33,7 @@ export const cell = css` export const cellClassname = `rdg-cell ${cell}`; // max-content does not calculate width when contain is set to style or size -export const cellAutoResizeClassname = css` +export const autosizeColumnsClassname = css` @layer rdg.Root { .${cell} { contain: content; diff --git a/src/types.ts b/src/types.ts index 8137f33a6d..493444b957 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,7 @@ export interface Column { export interface CalculatedColumn extends Column { readonly idx: number; + readonly width: number | string; readonly minWidth: number; readonly resizable: boolean; readonly sortable: boolean; diff --git a/test/column/formatter.test.tsx b/test/column/formatter.test.tsx index 766b8f6b26..66ac2bcbc2 100644 --- a/test/column/formatter.test.tsx +++ b/test/column/formatter.test.tsx @@ -108,7 +108,7 @@ describe('Custom formatter component', () => { resizable: false, rowGroup: false, sortable: false, - width: undefined + width: 'auto' }, indexes: [0] }); diff --git a/website/demos/AllFeatures.tsx b/website/demos/AllFeatures.tsx index adf030d083..a2b1a1980e 100644 --- a/website/demos/AllFeatures.tsx +++ b/website/demos/AllFeatures.tsx @@ -89,7 +89,7 @@ const columns: readonly Column[] = [ { key: 'email', name: 'Email', - width: 200, + width: 'max-content', resizable: true, editor: textEditor }, @@ -124,7 +124,7 @@ const columns: readonly Column[] = [ { key: 'catchPhrase', name: 'Catch Phrase', - width: 200, + width: 'max-content', resizable: true, editor: textEditor }, @@ -138,7 +138,7 @@ const columns: readonly Column[] = [ { key: 'sentence', name: 'Sentence', - width: 200, + width: 'max-content', resizable: true, editor: textEditor } diff --git a/website/demos/ColumnSpanning.tsx b/website/demos/ColumnSpanning.tsx index fa9325bfcd..a883836dc3 100644 --- a/website/demos/ColumnSpanning.tsx +++ b/website/demos/ColumnSpanning.tsx @@ -32,6 +32,7 @@ export default function ColumnSpanning({ direction }: Props) { key, name: key, frozen: i < 5, + width: 80, resizable: true, formatter: cellFormatter, colSpan(args) { diff --git a/website/demos/ColumnsReordering.tsx b/website/demos/ColumnsReordering.tsx index 3a6d6ed645..c6cac7e9d9 100644 --- a/website/demos/ColumnsReordering.tsx +++ b/website/demos/ColumnsReordering.tsx @@ -126,6 +126,7 @@ export default function ColumnsReordering({ direction }: Props) { sortColumns={sortColumns} onSortColumnsChange={onSortColumnsChange} direction={direction} + defaultColumnOptions={{ width: '1fr' }} /> ); diff --git a/website/demos/CommonFeatures.tsx b/website/demos/CommonFeatures.tsx index 4d04ca2819..7fc483641f 100644 --- a/website/demos/CommonFeatures.tsx +++ b/website/demos/CommonFeatures.tsx @@ -100,7 +100,7 @@ function getColumns(countries: string[], direction: Direction): readonly Column< { key: 'client', name: 'Client', - width: 220, + width: 'max-content', editor: textEditor }, { diff --git a/website/demos/MillionCells.tsx b/website/demos/MillionCells.tsx index 990f076d1e..1736521412 100644 --- a/website/demos/MillionCells.tsx +++ b/website/demos/MillionCells.tsx @@ -24,6 +24,7 @@ export default function MillionCells({ direction }: Props) { key, name: key, frozen: i < 5, + width: 80, resizable: true, formatter: cellFormatter });