Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
95fdb38
Refactor column width and resizing handling
nstepien Mar 16, 2026
3acfb9b
Merge branch 'main' into resizecolumnobserver
nstepien Mar 16, 2026
fffbe08
Merge branch 'main' into resizecolumnobserver
nstepien Mar 16, 2026
ba85807
avoid re-rendering if the width hasn't changed
nstepien Mar 16, 2026
5e0fab7
dedupe clampColumnWidth usage
nstepien Mar 17, 2026
4c40678
Merge branch 'main' into resizecolumnobserver
amanmahajan7 Mar 25, 2026
65daaa0
unskip test
nstepien Mar 27, 2026
64c5166
fix lints
nstepien Mar 27, 2026
cc3084f
&
nstepien Apr 7, 2026
2afc823
Merge branch 'main' into resizecolumnobserver
nstepien Apr 7, 2026
334e23e
Merge branch 'main' into resizecolumnobserver
nstepien Apr 8, 2026
7e156b6
Merge branch 'main' into resizecolumnobserver
amanmahajan7 Apr 21, 2026
f27adc9
Merge branch 'main' into resizecolumnobserver
nstepien Apr 23, 2026
ec6b7dd
Merge branch 'main' into resizecolumnobserver
nstepien Apr 24, 2026
16caf64
Merge branch 'main' into resizecolumnobserver
amanmahajan7 May 26, 2026
3fa984b
Merge branch 'main' into resizecolumnobserver
nstepien Jun 17, 2026
33767fc
Merge branch 'main' into resizecolumnobserver
nstepien Jun 17, 2026
f704ab6
fix lint
nstepien Jun 17, 2026
ccf8fc1
Merge branch 'main' into resizecolumnobserver
nstepien Jun 23, 2026
ad17ec0
Merge branch 'main' into resizecolumnobserver
nstepien Jun 24, 2026
c2160e9
Merge branch 'main' into resizecolumnobserver
nstepien Jul 26, 2026
a1f3cf7
review
nstepien Jul 28, 2026
b5065ef
internal vs public types
nstepien Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1959,16 +1959,18 @@ interface GroupRow<TRow> {
A map of column widths.

```tsx
type ColumnWidths = ReadonlyMap<string, ColumnWidth>;

interface ColumnWidth {
readonly type: 'resized' | 'measured';
readonly type: 'measured' | 'resized';
readonly width: number;
}

type ColumnWidths = ReadonlyMap<string, ColumnWidth>;
```

Used with `columnWidths` and `onColumnWidthsChange` props to control column widths externally.

`measured` widths are re-evaluated against the current column definitions, so they are only a cache. `resized` widths are intentional, and are preserved and clamped, including for columns that are not currently rendered.

#### `Position`

Represents a cell position in the grid.
Expand Down
121 changes: 49 additions & 72 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
import type { Key, KeyboardEvent } from 'react';
import { flushSync } from 'react-dom';

Expand Down Expand Up @@ -315,53 +315,45 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
* states
*/
const { scrollTop, scrollLeft } = useScrollState(gridRef);
const [gridWidth, gridHeight] = useGridDimensions(gridRef);
const [columnWidthsInternal, setColumnWidthsInternal] = useState(
(): ColumnWidths => columnWidthsRaw ?? new Map()
);
const [isColumnResizing, setIsColumnResizing] = useState(false);
const [gridWidth, gridHeight, isResizingWidth] = useGridDimensions(gridRef);
const [isDragging, setIsDragging] = useState(false);
const [draggedOverRowIdx, setDraggedOverRowIdx] = useState<number | undefined>(undefined);
const [previousRowIdx, setPreviousRowIdx] = useState(-1);

const isColumnWidthsControlled =
columnWidthsRaw != null && onColumnWidthsChangeRaw != null && !isColumnResizing;
const columnWidths = isColumnWidthsControlled ? columnWidthsRaw : columnWidthsInternal;
const onColumnWidthsChange = isColumnWidthsControlled
? (columnWidths: ColumnWidths) => {
// we keep the internal state in sync with the prop but this prevents an extra render
setColumnWidthsInternal(columnWidths);
onColumnWidthsChangeRaw(columnWidths);
}
: setColumnWidthsInternal;

const getColumnWidth = useCallback(
(column: CalculatedColumn<R, SR>) => {
return columnWidths.get(column.key)?.width ?? column.width;
},
[columnWidths]
);

const {
columns,
colSpanColumns,
lastStartFrozenColumnIndex,
firstEndFrozenColumnIndex,
headerRowsCount,
headerRowsCount
} = useCalculatedColumns({
rawColumns,
defaultColumnOptions
});

const {
colOverscanStartIdx,
colOverscanEndIdx,
templateColumns,
layoutCssVars,
totalStartFrozenColumnWidth,
totalEndFrozenColumnWidth
} = useCalculatedColumns({
rawColumns,
defaultColumnOptions,
getColumnWidth,
totalEndFrozenColumnWidth,
layoutCssVars,
columnMetrics,
observeMeasuringCellRef,
handleColumnResizeLatest,
handleColumnResizeEndLatest
} = useColumnWidths(
gridRef,
columns,
lastStartFrozenColumnIndex,
firstEndFrozenColumnIndex,
gridWidth,
scrollLeft,
viewportWidth: gridWidth,
enableVirtualization
});
isResizingWidth,
enableVirtualization,
columnWidthsRaw,
onColumnResize,
onColumnWidthsChangeRaw
);

/**
* computed values
Expand Down Expand Up @@ -481,23 +473,9 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
bottomSummaryRows
});

const { gridTemplateColumns, handleColumnResize } = useColumnWidths(
columns,
viewportColumns,
templateColumns,
gridRef,
gridWidth,
columnWidths,
onColumnWidthsChange,
onColumnResize,
setIsColumnResizing
);

/**
* The identity of the wrapper function is stable so it won't break memoization
*/
const handleColumnResizeLatest = useLatestFunc(handleColumnResize);
const handleColumnResizeEndLatest = useLatestFunc(handleColumnResizeEnd);
const onColumnsReorderLastest = useLatestFunc(onColumnsReorder);
const onSortColumnsChangeLatest = useLatestFunc(onSortColumnsChange);
const onCellMouseDownLatest = useLatestFunc(onCellMouseDown);
Expand Down Expand Up @@ -697,21 +675,17 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
}

if (isCellEditable(activePosition) && isDefaultCellInput(event, onCellPaste != null)) {
setActivePosition(({ idx, rowIdx }) => ({
idx,
rowIdx,
mode: 'EDIT',
row,
originalRow: row
}));
}
}

function handleColumnResizeEnd() {
// This check is needed as double click on the resize handle triggers onPointerMove
if (isColumnResizing) {
onColumnWidthsChangeRaw?.(columnWidths);
setIsColumnResizing(false);
// ensure we render the editor quickly enough,
// otherwise the user input might be lost in Firefox
flushSync(() => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved a TODO in tests

setActivePosition(({ idx, rowIdx }) => ({
idx,
rowIdx,
mode: 'EDIT',
row,
originalRow: row
}));
});
}
}

Expand Down Expand Up @@ -953,8 +927,12 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
}

const isLastRow = rowIdx === maxRowIdx;
const columnWidth = getColumnWidth(column);
const colSpan = column.colSpan?.({ type: 'ROW', row: getActiveRow() }) ?? 1;
const columnWidth = columnMetrics.get(column)?.width ?? 0;
const colSpan =
getColSpan(column, lastStartFrozenColumnIndex, firstEndFrozenColumnIndex, {
type: 'ROW',
row: getActiveRow()
}) ?? 1;
const { insetInlineStart, ...style } = getCellStyle(column, colSpan);
const marginEnd = 'calc(var(--rdg-drag-handle-size) * -0.5 + 1px)';
const isLastColumn = column.idx + colSpan - 1 === maxColIdx;
Expand Down Expand Up @@ -1148,10 +1126,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
}

// Keep the state and prop in sync
if (isColumnWidthsControlled && columnWidthsInternal !== columnWidthsRaw) {
setColumnWidthsInternal(columnWidthsRaw);
}

let templateRows = `repeat(${headerRowsCount}, ${headerRowHeight}px)`;
if (topSummaryRowsCount > 0) {
templateRows += ` repeat(${topSummaryRowsCount}, ${summaryRowHeight}px)`;
Expand Down Expand Up @@ -1184,7 +1158,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
scrollPaddingInlineEnd: totalEndFrozenColumnWidth,
scrollPaddingBlockStart: headerRowsHeight + topSummaryRowsCount * summaryRowHeight,
scrollPaddingBlockEnd: bottomSummaryRowsCount * summaryRowHeight,
gridTemplateColumns,
gridTemplateRows: templateRows,
'--rdg-header-row-height': `${headerRowHeight}px`,
...layoutCssVars
Expand Down Expand Up @@ -1312,7 +1285,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
{getDragHandle()}

{/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */}
{renderMeasuringCells(viewportColumns)}
{renderMeasuringCells(
viewportColumns,
// eslint-disable-next-line @eslint-react/refs
observeMeasuringCellRef
)}

{scrollToPositionElement}
</div>
Expand Down
34 changes: 22 additions & 12 deletions src/HeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { css } from 'ecij';

import { useRovingTabIndex } from './hooks';
import {
clampColumnWidth,
getCellClassname,
getCellStyle,
getHeaderCellRowSpan,
Expand Down Expand Up @@ -102,6 +101,7 @@ export default function HeaderCell<R, SR>({
setDraggedColumnKey
}: HeaderCellProps<R, SR>) {
const [isOver, setIsOver] = useState(false);
const resizingRef = useRef(false);
const dragImageRef = useRef<HTMLDivElement>(null);
const isDragging = draggedColumnKey === column.key;
const rowSpan = getHeaderCellRowSpan(column, rowIdx);
Expand Down Expand Up @@ -195,13 +195,18 @@ export default function HeaderCell<R, SR>({
// prevent navigation
// TODO: check if we can use `preventDefault` instead
event.stopPropagation();
resizingRef.current = true;
const { width } = event.currentTarget.getBoundingClientRect();
const { leftKey } = getLeftRightKey(direction);
const offset = key === leftKey ? -10 : 10;
const newWidth = clampColumnWidth(width + offset, column);
if (newWidth !== width) {
onColumnResize(column, newWidth);
}
onColumnResize(column, width + offset);
}
}

function onKeyUp() {
if (resizingRef.current) {
resizingRef.current = false;
onColumnResizeEnd();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed an issue where we were not resetting the resizing state when resizing via the keyboard.

}
}

Expand Down Expand Up @@ -299,6 +304,7 @@ export default function HeaderCell<R, SR>({
onFocus={handleFocus}
onClick={onClick}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
{...dragTargetProps}
{...dropTargetProps}
>
Expand Down Expand Up @@ -328,6 +334,7 @@ function ResizeHandle<R, SR>({
onColumnResize,
onColumnResizeEnd
}: ResizeHandleProps<R, SR>) {
const resizingRef = useRef(false);
const resizingOffsetRef = useRef<number>(undefined);
const isRtl = direction === 'rtl';

Expand All @@ -349,16 +356,19 @@ function ResizeHandle<R, SR>({
function onPointerMove(event: React.PointerEvent<HTMLDivElement>) {
const offset = resizingOffsetRef.current;
if (offset === undefined) return;
const { width, right, left } = event.currentTarget.parentElement!.getBoundingClientRect();
let newWidth = isRtl ? right + offset - event.clientX : event.clientX + offset - left;
newWidth = clampColumnWidth(newWidth, column);
if (width > 0 && newWidth !== width) {
onColumnResize(column, newWidth);
}
resizingRef.current = true;
const { right, left } = event.currentTarget.parentElement!.getBoundingClientRect();
const newWidth = isRtl ? right + offset - event.clientX : event.clientX + offset - left;
onColumnResize(column, newWidth);
}

function onLostPointerCapture() {
onColumnResizeEnd();
// avoid calling onColumnResizeEnd if the pointer has not moved after pointed down,
// also to avoid conflicts with double-clicking
if (resizingRef.current) {
resizingRef.current = false;
onColumnResizeEnd();
}
resizingOffsetRef.current = undefined;
}

Expand Down
Loading