Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,9 @@ See the [`PositionChangeArgs`](#positionchangeargstrow-tsummaryrow) type in the

###### `onFill?: Maybe<(event: FillEvent<R>) => R>`

###### `onScroll?: Maybe<(event: React.UIEvent<HTMLDivElement>) => void>`
###### `onScroll?: React.UIEventHandler<HTMLDivElement> | undefined`

Callback triggered when the grid is scrolled.
Native DOM `onScroll` prop.

###### `onColumnResize?: Maybe<(column: CalculatedColumn<R, SR>, width: number) => void>`

Expand Down
107 changes: 33 additions & 74 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 { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import type { Key, KeyboardEvent } from 'react';
import { flushSync } from 'react-dom';

Expand All @@ -11,17 +11,22 @@ import {
useColumnWidths,
useGridDimensions,
useLatestFunc,
useScrollState,
useScrollToPosition,
useViewportColumns,
useViewportRows,
type HeaderRowSelectionContextValue
type ActivePosition,
type HeaderRowSelectionContextValue,
type PartialPosition
} from './hooks';
import {
abs,
assertIsValidKeyGetter,
canExitGrid,
classnames,
createCellEvent,
focusCell,
getCellStyle,
getCellToScroll,
getColSpan,
getLeftRightKey,
getNextActivePosition,
Expand Down Expand Up @@ -66,8 +71,6 @@ import EditCell from './EditCell';
import GroupedColumnHeaderRow from './GroupedColumnHeaderRow';
import HeaderRow from './HeaderRow';
import { defaultRenderRow } from './Row';
import type { PartialPosition } from './ScrollToCell';
import ScrollToCell from './ScrollToCell';
import { default as defaultRenderSortStatus } from './sortStatus';
import { cellDragHandleClassname, cellDragHandleFrozenClassname } from './style/cell';
import {
Expand Down Expand Up @@ -106,6 +109,7 @@ type SharedDivProps = Pick<
| 'aria-rowcount'
| 'className'
| 'style'
| 'onScroll'
>;

export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends SharedDivProps {
Expand Down Expand Up @@ -190,8 +194,6 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
>;
/** Function called whenever the active position is changed */
onActivePositionChange?: Maybe<(args: PositionChangeArgs<NoInfer<R>, NoInfer<SR>>) => void>;
/** Callback triggered when the grid is scrolled */
onScroll?: Maybe<(event: React.UIEvent<HTMLDivElement>) => void>;
/** Callback triggered when column is resized */
onColumnResize?: Maybe<(column: CalculatedColumn<R, SR>, width: number) => void>;
/** Callback triggered when columns are reordered */
Expand Down Expand Up @@ -303,19 +305,22 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const enableVirtualization = rawEnableVirtualization ?? true;
const direction = rawDirection ?? 'ltr';

/**
* ref
*/
const gridRef = useRef<HTMLDivElement>(null);

/**
* states
*/
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const { scrollTop, scrollLeft } = useScrollState(gridRef);
const [gridWidth, gridHeight] = useGridDimensions({ gridRef });
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 =
Expand All @@ -336,7 +341,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
[columnWidths]
);

const [gridRef, gridWidth, gridHeight] = useGridDimensions();
const {
columns,
colSpanColumns,
Expand Down Expand Up @@ -383,6 +387,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const {
activePosition,
setActivePosition,
setPositionToFocus,
activePositionIsInActiveBounds,
activePositionIsInViewport,
activePositionIsRow,
Expand All @@ -391,15 +396,16 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
getActiveColumn,
getActiveRow
} = useActivePosition<R, SR>({
gridRef,
columns,
rows,
isTreeGrid,
maxColIdx,
minRowIdx,
maxRowIdx,
setDraggedOverRowIdx,
setShouldFocusPosition
setDraggedOverRowIdx
});
const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({ gridRef });

const defaultGridComponents = useMemo(
() => ({
Expand Down Expand Up @@ -496,19 +502,8 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const selectHeaderCellLatest = useLatestFunc(selectHeaderCell);

/**
* effects
* Misc hooks
*/
useLayoutEffect(() => {
if (shouldFocusPosition) {
if (activePositionIsRow) {
focusRow(gridRef.current!);
} else {
focusCell(gridRef.current!);
}
setShouldFocusPosition(false);
}
}, [shouldFocusPosition, activePositionIsRow, gridRef]);

useImperativeHandle(
ref,
(): DataGridHandle => ({
Expand Down Expand Up @@ -634,16 +629,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
}
}

function handleScroll(event: React.UIEvent<HTMLDivElement>) {
const { scrollTop, scrollLeft } = event.currentTarget;
flushSync(() => {
setScrollTop(scrollTop);
// scrollLeft is nagative when direction is rtl
setScrollLeft(abs(scrollLeft));
});
onScroll?.(event);
}

function updateRow(column: CalculatedColumn<R, SR>, rowIdx: number, row: R) {
if (typeof onRowsChange !== 'function') return;
if (row === rows[rowIdx]) return;
Expand Down Expand Up @@ -808,8 +793,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
// Avoid re-renders if the selected cell state is the same
scrollIntoView(getCellToScroll(gridRef.current!));
} else {
setShouldFocusPosition(options?.shouldFocus === true);
setActivePosition({ ...position, mode: 'ACTIVE' });
const newPosition: ActivePosition = { ...position, mode: 'ACTIVE' };
setActivePosition(newPosition);
if (options?.shouldFocus) {
setPositionToFocus(newPosition);
}
}

if (onActivePositionChange && !samePosition) {
Expand Down Expand Up @@ -992,8 +980,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row });

function closeEditor(shouldFocus: boolean) {
setShouldFocusPosition(shouldFocus);
setActivePosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'ACTIVE' }));
const newPosition: ActivePosition = { idx: activePosition.idx, rowIdx, mode: 'ACTIVE' };
setActivePosition(newPosition);
if (shouldFocus) {
setPositionToFocus(newPosition);
}
}

function onRowChange(row: R, commitChanges: boolean, shouldFocus: boolean) {
Expand Down Expand Up @@ -1131,7 +1122,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
}}
dir={direction}
ref={gridRef}
onScroll={handleScroll}
onScroll={onScroll}
onKeyDown={handleKeyDown}
onCopy={handleCellCopy}
onPaste={handleCellPaste}
Expand Down Expand Up @@ -1281,43 +1272,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
{/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */}
{renderMeasuringCells(viewportColumns)}

{scrollToPosition !== null && (
<ScrollToCell
scrollToPosition={scrollToPosition}
setScrollToCellPosition={setScrollToPosition}
gridRef={gridRef}
/>
)}
{scrollToPositionElement}
</div>
);
}

function getRowToScroll(gridEl: HTMLDivElement) {
return gridEl.querySelector<HTMLDivElement>(':scope > [role="row"][tabindex="0"]');
}

function getCellToScroll(gridEl: HTMLDivElement) {
return gridEl.querySelector<HTMLDivElement>(':scope > [role="row"] > [tabindex="0"]');
}

function isSamePosition(p1: Position, p2: Position) {
return p1.idx === p2.idx && p1.rowIdx === p2.rowIdx;
}

function focusElement(element: HTMLDivElement | null, shouldScroll: boolean) {
if (element === null) return;

if (shouldScroll) {
scrollIntoView(element);
}

element.focus({ preventScroll: true });
}

function focusRow(gridEl: HTMLDivElement) {
focusElement(getRowToScroll(gridEl), true);
}

function focusCell(gridEl: HTMLDivElement, shouldScroll = true) {
focusElement(getCellToScroll(gridEl), shouldScroll);
}
43 changes: 0 additions & 43 deletions src/ScrollToCell.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export * from './useGridDimensions';
export * from './useLatestFunc';
export * from './useRovingTabIndex';
export * from './useRowSelection';
export * from './useScrollState';
export * from './useScrollToPosition';
export * from './useViewportColumns';
export * from './useViewportRows';
27 changes: 21 additions & 6 deletions src/hooks/useActivePosition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { useLayoutEffect, useState } from 'react';

import { focusCell, focusRow } from '../utils';
import type { CalculatedColumn, Position, StateSetter } from '../types';

interface ActivePosition extends Position {
export interface ActivePosition extends Position {
readonly mode: 'ACTIVE';
}

Expand All @@ -20,27 +21,30 @@ const initialActivePosition: ActivePosition = {
};

export function useActivePosition<R, SR>({
gridRef,
columns,
rows,
isTreeGrid,
maxColIdx,
minRowIdx,
maxRowIdx,
setDraggedOverRowIdx,
setShouldFocusPosition
setDraggedOverRowIdx
}: {
gridRef: React.RefObject<HTMLDivElement | null>;
columns: readonly CalculatedColumn<R, SR>[];
rows: readonly R[];
isTreeGrid: boolean;
maxColIdx: number;
minRowIdx: number;
maxRowIdx: number;
setDraggedOverRowIdx: StateSetter<number | undefined>;
setShouldFocusPosition: StateSetter<boolean>;
}) {
const [activePosition, setActivePosition] = useState<ActivePosition | EditPosition<R>>(
initialActivePosition
);
const [positionToFocus, setPositionToFocus] = useState<ActivePosition | EditPosition<R> | null>(
null
);

/**
* Returns whether the given position represents a valid cell or row position in the grid.
Expand Down Expand Up @@ -123,14 +127,25 @@ export function useActivePosition<R, SR>({
mode: 'ACTIVE'
};
setActivePosition(newPosition);
setShouldFocusPosition(false);
setPositionToFocus(null);
({ resolvedActivePosition, validatedPosition } = getResolvedValues(newPosition));
}
}

useLayoutEffect(() => {
if (positionToFocus !== null) {
if (positionToFocus.idx === -1) {
focusRow(gridRef.current!);
} else {
focusCell(gridRef.current!);
}
}
}, [positionToFocus, gridRef]);

return {
activePosition: resolvedActivePosition,
setActivePosition,
setPositionToFocus,
activePositionIsInActiveBounds: validatedPosition.isPositionInActiveBounds,
activePositionIsInViewport: validatedPosition.isPositionInViewport,
activePositionIsRow: validatedPosition.isRowInActiveBounds,
Expand Down
Loading
Loading