Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,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
108 changes: 33 additions & 75 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 @@ -65,8 +70,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 @@ -105,6 +108,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 @@ -189,8 +193,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>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

/** 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 @@ -302,19 +304,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 @@ -335,7 +340,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 @@ -382,6 +386,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const {
activePosition,
setActivePosition,
setPositionToFocus,
activePositionIsInActiveBounds,
activePositionIsInViewport,
activePositionIsRow,
Expand All @@ -390,15 +395,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 @@ -495,20 +501,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!);
}
// eslint-disable-next-line react-hooks/set-state-in-effect
setShouldFocusPosition(false);
}
}, [shouldFocusPosition, activePositionIsRow, gridRef]);

useImperativeHandle(
ref,
(): DataGridHandle => ({
Expand Down Expand Up @@ -636,16 +630,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 @@ -810,8 +794,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 @@ -994,8 +981,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 @@ -1133,7 +1123,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 @@ -1283,43 +1273,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>('& > [role="row"][tabindex="0"]');
}

function getCellToScroll(gridEl: HTMLDivElement) {
return gridEl.querySelector<HTMLDivElement>('& > [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';
36 changes: 30 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, useRef, 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,31 @@ 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
);
const positionToFocusRef = useRef<ActivePosition | EditPosition<R>>(null);

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

useLayoutEffect(() => {
// Layout effects clean up when the component is replaced by a suspense fallback,
// or when under <Activity mode="hidden">, then re-mounts when the suspense boundary cleans,
// or when Activity switches back to `mode="visible"`.
// So we use a ref to:
// 1. avoid re-focusing after the effect re-mounts
// 2. avoid re-rendering by not re-setting the state
if (positionToFocus !== null && positionToFocus !== positionToFocusRef.current) {
positionToFocusRef.current = positionToFocus;

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