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 @@ -627,9 +627,9 @@ Arguments:
- `args.row`: `R | undefined` - row object of the currently selected cell
- `args.column`: `CalculatedColumn<TRow, TSummaryRow>` - column object of the currently selected cell

###### `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
93 changes: 23 additions & 70 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 @@ -10,17 +10,22 @@ import {
useColumnWidths,
useGridDimensions,
useLatestFunc,
useScrollState,
useScrollToPosition,
useShouldFocusPosition,
useViewportColumns,
useViewportRows,
type HeaderRowSelectionContextValue
type HeaderRowSelectionContextValue,
type PartialPosition
} from './hooks';
import {
abs,
assertIsValidKeyGetter,
canExitGrid,
classnames,
createCellEvent,
focusCell,
getCellStyle,
getCellToScroll,
getColSpan,
getLeftRightKey,
getNextSelectedCellPosition,
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 { rootClassname, viewportDraggingClassname } from './style/core';
Expand Down Expand Up @@ -110,6 +113,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 @@ -195,8 +199,6 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
>;
/** Function called whenever cell selection is changed */
onSelectedCellChange?: Maybe<(args: CellSelectArgs<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 @@ -307,19 +309,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 [shouldFocusCell, setShouldFocusCell] = useState(false);
const [previousRowIdx, setPreviousRowIdx] = useState(-1);

const isColumnWidthsControlled =
Expand All @@ -340,7 +345,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 @@ -372,6 +376,8 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const [selectedPosition, setSelectedPosition] = useState(
(): SelectCellState | EditCellState<R> => ({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' })
);
const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({ gridRef });
const { shouldFocusPositionRef } = useShouldFocusPosition({ gridRef, selectedPosition });

/**
* computed values
Expand Down Expand Up @@ -480,19 +486,8 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const selectHeaderCellLatest = useLatestFunc(selectHeaderCell);

/**
* effects
* Misc hooks
*/
useLayoutEffect(() => {
if (shouldFocusCell) {
if (selectedPosition.idx === -1) {
focusRow(gridRef.current!);
} else {
focusCell(gridRef.current!);
}
setShouldFocusCell(false);
}
}, [shouldFocusCell, selectedPosition.idx, gridRef]);

useImperativeHandle(
ref,
(): DataGridHandle => ({
Expand Down Expand Up @@ -621,16 +616,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 @@ -814,7 +799,7 @@ 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 {
setShouldFocusCell(options?.shouldFocusCell === true);
shouldFocusPositionRef.current = options?.shouldFocusCell === true;
setSelectedPosition({ ...position, mode: 'SELECT' });
}

Expand Down Expand Up @@ -1004,7 +989,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const closeOnExternalRowChange = column.editorOptions?.closeOnExternalRowChange ?? true;

const closeEditor = (shouldFocusCell: boolean) => {
setShouldFocusCell(shouldFocusCell);
shouldFocusPositionRef.current = shouldFocusCell;
setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' }));
};

Expand Down Expand Up @@ -1191,7 +1176,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 @@ -1303,43 +1288,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);
}
42 changes: 0 additions & 42 deletions src/ScrollToCell.tsx

This file was deleted.

3 changes: 3 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export * from './useGridDimensions';
export * from './useLatestFunc';
export * from './useRovingTabIndex';
export * from './useRowSelection';
export * from './useScrollState';
export * from './useScrollToPosition';
export * from './useShouldFocusPosition';
export * from './useViewportColumns';
export * from './useViewportRows';
13 changes: 8 additions & 5 deletions src/hooks/useGridDimensions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useLayoutEffect, useRef, useState } from 'react';
import { useLayoutEffect, useState } from 'react';
import { flushSync } from 'react-dom';

export function useGridDimensions() {
const gridRef = useRef<HTMLDivElement>(null);
export function useGridDimensions({
gridRef
}: {
gridRef: React.RefObject<HTMLDivElement | null>;
}) {
const [inlineSize, setInlineSize] = useState(1);
const [blockSize, setBlockSize] = useState(1);

Expand Down Expand Up @@ -32,7 +35,7 @@ export function useGridDimensions() {
return () => {
resizeObserver.disconnect();
};
}, []);
}, [gridRef]);

return [gridRef, inlineSize, blockSize] as const;
return [inlineSize, blockSize] as const;
}
66 changes: 66 additions & 0 deletions src/hooks/useScrollState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useSyncExternalStore } from 'react';

import { abs } from '../utils';

interface ScrollState {
readonly scrollTop: number;
readonly scrollLeft: number;
}

const initialScrollState: ScrollState = {
scrollTop: 0,
scrollLeft: 0
};

function getServerSnapshot() {
return initialScrollState;
}

const scrollStateMap = new WeakMap<React.RefObject<HTMLDivElement | null>, ScrollState>();

export function useScrollState(gridRef: React.RefObject<HTMLDivElement | null>): ScrollState {
const subscribe = useCallback(
(onStoreChange: () => void) => {
if (gridRef.current === null) return () => {};

const el = gridRef.current;

// prime the scroll state map with the initial values
setScrollState();

function setScrollState() {
const { scrollTop } = el;
// scrollLeft is negative when direction is rtl
const scrollLeft = abs(el.scrollLeft);

const prev = scrollStateMap.get(gridRef) ?? initialScrollState;
if (prev.scrollTop === scrollTop && prev.scrollLeft === scrollLeft) {
return false;
}

scrollStateMap.set(gridRef, { scrollTop, scrollLeft });
return true;
}

function onScroll() {
if (setScrollState()) {
onStoreChange();
}
}

el.addEventListener('scroll', onScroll);

return () => el.removeEventListener('scroll', onScroll);
},
[gridRef]
);

const getSnapshot = useCallback((): ScrollState => {
// gridRef.current is null during initial render, suspending, or <Activity mode="hidden">
// to avoid returning a different state in those cases,
// we key the ref object instead of the element itself
return scrollStateMap.get(gridRef) ?? initialScrollState;
}, [gridRef]);

return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Scroll state now uses useSyncExternalStore instead of 2xuseState+flushSync

}
Loading