Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1957,12 +1957,18 @@ interface GroupRow<TRow> {
A map of column widths.

```tsx
type ColumnWidths = ReadonlyMap<string, ColumnWidth>;
type ColumnWidth =
| {
readonly type: 'measured' | 'resizing' | 'resized';
readonly width: number;
}
| {
readonly type: 'autosizing';
readonly width: 'max-content';
readonly onMeasure: (width: number) => void;
};

interface ColumnWidth {
readonly type: 'resized' | 'measured';
readonly width: number;
}
type ColumnWidths = ReadonlyMap<string, ColumnWidth>;
```

Used with `columnWidths` and `onColumnWidthsChange` props to control column widths externally.
Expand Down
109 changes: 37 additions & 72 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useImperativeHandle, useLayoutEffect, useMemo, useState } from 'react';
import { useImperativeHandle, useLayoutEffect, useMemo, useState } from 'react';
import type { Key, KeyboardEvent } from 'react';
import { flushSync } from 'react-dom';

Expand Down Expand Up @@ -308,53 +308,40 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
*/
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [columnWidthsInternal, setColumnWidthsInternal] = useState(
(): ColumnWidths => columnWidthsRaw ?? new Map()
);
const [isColumnResizing, setIsColumnResizing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [draggedOverRowIdx, setDraggedOverRowIdx] = useState<number | undefined>(undefined);
const [scrollToPosition, setScrollToPosition] = useState<PartialPosition | null>(null);
const [shouldFocusPosition, setShouldFocusPosition] = useState(false);
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 [gridRef, gridWidth, gridHeight, isResizingWidth] = useGridDimensions();

const getColumnWidth = useCallback(
(column: CalculatedColumn<R, SR>) => {
return columnWidths.get(column.key)?.width ?? column.width;
},
[columnWidths]
);
const { columns, colSpanColumns, lastFrozenColumnIndex, headerRowsCount } = useCalculatedColumns({
rawColumns,
defaultColumnOptions
});

const [gridRef, gridWidth, gridHeight] = useGridDimensions();
const {
columns,
colSpanColumns,
lastFrozenColumnIndex,
headerRowsCount,
colOverscanStartIdx,
colOverscanEndIdx,
templateColumns,
totalFrozenColumnWidth,
layoutCssVars,
totalFrozenColumnWidth
} = useCalculatedColumns({
rawColumns,
defaultColumnOptions,
getColumnWidth,
columnMetrics,
observeMeasuringCellRef,
handleColumnResizeLatest,
handleColumnResizeEndLatest
} = useColumnWidths(
gridRef,
columns,
lastFrozenColumnIndex,
gridWidth,
scrollLeft,
viewportWidth: gridWidth,
enableVirtualization
});
isResizingWidth,
enableVirtualization,
columnWidthsRaw,
onColumnResize,
onColumnWidthsChangeRaw
);

/**
* computed values
Expand Down Expand Up @@ -466,23 +453,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 @@ -695,21 +668,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(() => {
setActivePosition(({ idx, rowIdx }) => ({
idx,
rowIdx,
mode: 'EDIT',
row,
originalRow: row
}));
});
}
}

Expand Down Expand Up @@ -947,8 +916,9 @@ 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, lastFrozenColumnIndex, { 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 @@ -1089,10 +1059,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 @@ -1124,7 +1090,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
scrollPaddingInlineStart: totalFrozenColumnWidth,
scrollPaddingBlockStart: headerRowsHeight + topSummaryRowsCount * summaryRowHeight,
scrollPaddingBlockEnd: bottomSummaryRowsCount * summaryRowHeight,
gridTemplateColumns,
gridTemplateRows: templateRows,
'--rdg-header-row-height': `${headerRowHeight}px`,
...layoutCssVars
Expand Down Expand Up @@ -1279,7 +1244,7 @@ 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, observeMeasuringCellRef)}

{scrollToPosition !== null && (
<ScrollToCell
Expand Down
19 changes: 18 additions & 1 deletion src/HeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default function HeaderCell<R, SR>({
setDraggedColumnKey
}: HeaderCellProps<R, SR>) {
const [isOver, setIsOver] = useState(false);
const resizingRef = useRef<boolean>(false);
const dragImageRef = useRef<HTMLDivElement>(null);
const isDragging = draggedColumnKey === column.key;
const rowSpan = getHeaderCellRowSpan(column, rowIdx);
Expand Down Expand Up @@ -195,6 +196,7 @@ 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;
Expand All @@ -205,6 +207,13 @@ export default function HeaderCell<R, SR>({
}
}

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

function onDragStart(event: React.DragEvent<HTMLDivElement>) {
// need flushSync to make sure the drag image is rendered before the drag starts
flushSync(() => {
Expand Down Expand Up @@ -299,6 +308,7 @@ export default function HeaderCell<R, SR>({
onFocus={handleFocus}
onClick={onClick}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
{...dragTargetProps}
{...dropTargetProps}
>
Expand Down Expand Up @@ -328,6 +338,7 @@ function ResizeHandle<R, SR>({
onColumnResize,
onColumnResizeEnd
}: ResizeHandleProps<R, SR>) {
const resizingRef = useRef<boolean>(false);
const resizingOffsetRef = useRef<number>(undefined);
const isRtl = direction === 'rtl';

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

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
Loading