in src/data-table/data-table.tsx [649:1074]
export function DataTable({
selectable,
batchActions,
columns,
filters,
emptyMessage,
loading,
loadingMessage,
onIncludedRowsChange,
onRowHighlightChange,
onSelectMany,
onSelectNone,
onSelectOne,
onSort,
resizableColumnWidths = false,
rows: allRows,
rowActions = [],
rowHeight = 36,
rowHighlightIndex: rowHighlightIndexControlled,
selectedRowIds,
sortIndex,
sortDirection,
textQuery = '',
controlRef,
}: DataTableProps) {
const [, theme] = useStyletron();
const locale = React.useContext(LocaleContext);
const rowHeightAtIndex = React.useCallback(
(index: number) => {
if (index === 0) {
return HEADER_ROW_HEIGHT;
}
return rowHeight;
},
[rowHeight]
);
// We use state for our ref, to allow hooks to update when the ref changes.
const [gridRef, setGridRef] = React.useState<VariableSizeGrid | undefined | null>(null);
const [measuredWidths, setMeasuredWidths] = React.useState(columns.map(() => 0));
const [resizeDeltas, setResizeDeltas] = React.useState(columns.map(() => 0));
React.useEffect(() => {
setMeasuredWidths((prev) => {
return columns.map((v, index) => prev[index] || 0);
});
setResizeDeltas((prev) => {
return columns.map((v, index) => prev[index] || 0);
});
}, [columns]);
const resetAfterColumnIndex = React.useCallback(
(columnIndex: number) => {
if (gridRef) {
// trigger react-window to layout the elements again
gridRef.resetAfterColumnIndex(columnIndex, true);
}
},
[gridRef]
);
const handleWidthsChange = React.useCallback(
(nextWidths: number[]) => {
setMeasuredWidths(nextWidths);
resetAfterColumnIndex(0);
},
[setMeasuredWidths, resetAfterColumnIndex]
);
const handleColumnResize = React.useCallback(
(columnIndex: number, delta: number) => {
setResizeDeltas((prev) => {
prev[columnIndex] = Math.max(prev[columnIndex] + delta, 0);
return [...prev];
});
resetAfterColumnIndex(columnIndex);
},
[setResizeDeltas, resetAfterColumnIndex]
);
const [scrollLeft, setScrollLeft] = React.useState(0);
const [isScrollingX, setIsScrollingX] = React.useState(false);
const [recentlyScrolledX, setRecentlyScrolledX] = React.useState(false);
React.useLayoutEffect(() => {
if (recentlyScrolledX !== isScrollingX) {
setIsScrollingX(recentlyScrolledX);
}
if (recentlyScrolledX) {
const timeout = setTimeout(() => {
setRecentlyScrolledX(false);
}, 200);
return () => clearTimeout(timeout);
}
}, [recentlyScrolledX]);
const handleScroll = React.useCallback(
(params: { scrollLeft: number }) => {
setScrollLeft(params.scrollLeft);
if (params.scrollLeft !== scrollLeft) {
setRecentlyScrolledX(true);
}
},
[scrollLeft, setScrollLeft, setRecentlyScrolledX]
);
const sortedIndices = React.useMemo(() => {
let toSort = allRows.map((r, i): [DataTableProps['rows'][number], number] => [r, i]);
const index = sortIndex;
if (index !== null && index !== undefined && index !== -1 && columns[index]) {
const sortFn = columns[index].sortFn;
// @ts-ignore
const getValue = (row) => columns[index].mapDataToValue(row.data);
if (sortDirection === SORT_DIRECTIONS.ASC) {
toSort.sort((a, b) => sortFn(getValue(a[0]), getValue(b[0])));
} else if (sortDirection === SORT_DIRECTIONS.DESC) {
toSort.sort((a, b) => sortFn(getValue(b[0]), getValue(a[0])));
}
}
return toSort.map((el) => el[1]);
}, [sortIndex, sortDirection, columns, allRows]);
const filteredIndices = React.useMemo(() => {
const set = new Set(allRows.map((_, idx) => idx));
// @ts-ignore
Array.from(filters || new Set(), (f) => f).forEach(([title, filter]) => {
const columnIndex = columns.findIndex((c) => c.title === title);
const column = columns[columnIndex];
if (!column) {
return;
}
const filterFn = column.buildFilter(filter);
Array.from(set).forEach((idx) => {
if (!filterFn(column.mapDataToValue(allRows[idx].data))) {
set.delete(idx);
}
});
});
if (textQuery) {
// @ts-ignore
const stringishColumnIndices = [];
for (let i = 0; i < columns.length; i++) {
if (columns[i].textQueryFilter) {
// @ts-ignore
stringishColumnIndices.push(i);
}
}
Array.from(set).forEach((idx) => {
// @ts-ignore
const matches = stringishColumnIndices.some((cdx) => {
const column = columns[cdx];
const textQueryFilter = column.textQueryFilter;
if (textQueryFilter) {
return textQueryFilter(textQuery, column.mapDataToValue(allRows[idx].data));
}
return false;
});
if (!matches) {
set.delete(idx);
}
});
}
return set;
}, [filters, textQuery, columns, allRows]);
const rows = React.useMemo(() => {
const result = sortedIndices
.filter((idx) => filteredIndices.has(idx))
.map((idx) => allRows[idx]);
if (onIncludedRowsChange) {
onIncludedRowsChange(result);
}
return result;
}, [sortedIndices, filteredIndices, onIncludedRowsChange, allRows]);
const [browserScrollbarWidth, setBrowserScrollbarWidth] = React.useState(0);
const normalizedWidths = React.useMemo(() => {
const resizedWidths = measuredWidths.map((w, i) => Math.floor(w) + Math.floor(resizeDeltas[i]));
if (gridRef) {
const gridProps = gridRef.props;
let isContentTallerThanContainer = false;
let visibleRowHeight = 0;
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
visibleRowHeight += rowHeightAtIndex(rowIndex);
if (visibleRowHeight >= gridProps.height) {
isContentTallerThanContainer = true;
break;
}
}
const scrollbarWidth = isContentTallerThanContainer ? browserScrollbarWidth : 0;
const remainder = gridProps.width - sum(resizedWidths) - scrollbarWidth;
const padding = Math.floor(
remainder / columns.filter((c) => (c ? c.fillWidth : true)).length
);
if (padding > 0) {
const result = [];
// -1 so that we loop over all but the last item
for (let i = 0; i < resizedWidths.length - 1; i++) {
if (columns[i] && columns[i].fillWidth) {
// @ts-ignore
result.push(resizedWidths[i] + padding);
} else {
// @ts-ignore
result.push(resizedWidths[i]);
}
}
// @ts-ignore
result.push(gridProps.width - sum(result) - scrollbarWidth);
return result;
}
}
return resizedWidths;
}, [gridRef, measuredWidths, resizeDeltas, browserScrollbarWidth, rows.length, columns]);
React.useEffect(() => {
resetAfterColumnIndex(0);
}, [normalizedWidths]);
const isSelectable = (batchActions ? !!batchActions.length : false) || !!selectable;
const isSelectedAll = React.useMemo(() => {
if (!selectedRowIds) {
return false;
}
return !!rows.length && selectedRowIds.size >= rows.length;
}, [selectedRowIds, rows.length]);
const isSelectedIndeterminate = React.useMemo(() => {
if (!selectedRowIds) {
return false;
}
return !!selectedRowIds.size && selectedRowIds.size < rows.length;
}, [selectedRowIds, rows.length]);
const isRowSelected = React.useCallback(
(id: string | number) => {
if (selectedRowIds) {
return selectedRowIds.has(id);
}
return false;
},
[selectedRowIds]
);
const handleSelectMany = React.useCallback(() => {
if (onSelectMany) {
onSelectMany(rows);
}
}, [rows, onSelectMany]);
const handleSelectNone = React.useCallback(() => {
if (onSelectNone) {
onSelectNone();
}
}, [onSelectNone]);
const handleSelectOne = React.useCallback(
(row: Row) => {
if (onSelectOne) {
onSelectOne(row);
}
},
[onSelectOne]
);
const handleSort = React.useCallback(
(columnIndex: number) => {
if (onSort) {
onSort(columnIndex);
}
},
[onSort]
);
React.useImperativeHandle(
controlRef,
() => ({
clearSelection: handleSelectNone,
getRows: () => rows,
}),
[handleSelectNone, rows]
);
const [columnHighlightIndex, setColumnHighlightIndex] = React.useState(-1);
const [rowHighlightIndex, setRowHighlightIndex] = React.useState(-1);
// @ts-ignore
function handleRowHighlightIndexChange(nextIndex) {
setRowHighlightIndex(nextIndex);
if (gridRef) {
if (nextIndex >= 0) {
gridRef.scrollToItem({ rowIndex: nextIndex });
}
if (onRowHighlightChange) {
onRowHighlightChange(nextIndex, rows[nextIndex - 1]);
}
}
}
const handleRowMouseEnter = React.useCallback(
(nextIndex: number) => {
setColumnHighlightIndex(-1);
if (nextIndex !== rowHighlightIndex) {
handleRowHighlightIndexChange(nextIndex);
}
},
[rowHighlightIndex]
);
// @ts-ignore
function handleColumnHeaderMouseEnter(columnIndex) {
setColumnHighlightIndex(columnIndex);
handleRowHighlightIndexChange(-1);
}
function handleColumnHeaderMouseLeave() {
setColumnHighlightIndex(-1);
}
React.useEffect(() => {
if (typeof rowHighlightIndexControlled === 'number') {
handleRowHighlightIndexChange(rowHighlightIndexControlled);
}
}, [rowHighlightIndexControlled]);
const itemData = React.useMemo(() => {
return {
columnHighlightIndex,
rowHighlightIndex,
isRowSelected,
isSelectable,
onRowMouseEnter: handleRowMouseEnter,
onSelectOne: handleSelectOne,
columns: columns,
rows,
textQuery,
};
}, [
handleRowMouseEnter,
columnHighlightIndex,
isRowSelected,
isSelectable,
rowHighlightIndex,
rows,
columns,
handleSelectOne,
textQuery,
]);
return (
<React.Fragment>
<MeasureColumnWidths
columns={columns}
rows={rows}
widths={measuredWidths}
isSelectable={isSelectable}
onWidthsChange={handleWidthsChange}
/>
{/* @ts-ignore */}
<MeasureScrollbarWidth
// @ts-ignore
onWidthChange={(w) => setBrowserScrollbarWidth(w)}
/>
<AutoSizer>
{/* @ts-ignore */}
{({ height, width }) => (
<HeaderContext.Provider
value={{
columns: columns,
columnHighlightIndex,
emptyMessage: emptyMessage || locale.datatable.emptyState,
filters: filters,
loading: Boolean(loading),
loadingMessage: loadingMessage || locale.datatable.loadingState,
isScrollingX,
isSelectable,
isSelectedAll,
isSelectedIndeterminate,
measuredWidths,
onMouseEnter: handleColumnHeaderMouseEnter,
onMouseLeave: handleColumnHeaderMouseLeave,
onResize: handleColumnResize,
onSelectMany: handleSelectMany,
onSelectNone: handleSelectNone,
onSort: handleSort,
resizableColumnWidths,
rowActions,
rowHeight,
rowHighlightIndex,
rows,
scrollLeft,
// @ts-ignore
sortDirection: sortDirection || null,
sortIndex: typeof sortIndex === 'number' ? sortIndex : -1,
tableHeight: height,
widths: normalizedWidths,
}}
>
<VariableSizeGrid
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={(node) => setGridRef(node)}
overscanRowCount={10}
overscanColumnCount={5}
innerElementType={InnerTableElement}
columnCount={columns.length}
columnWidth={(columnIndex) => normalizedWidths[columnIndex]}
height={height - 2}
// plus one to account for additional header row
rowCount={rows.length + 1}
rowHeight={rowHeightAtIndex}
width={width - 2}
itemData={itemData}
onScroll={handleScroll}
style={{
...theme.borders.border200,
borderColor: theme.colors.borderOpaque,
}}
direction={theme.direction === 'rtl' ? 'rtl' : 'ltr'}
>
{CellPlacementMemo}
</VariableSizeGrid>
</HeaderContext.Provider>
)}
</AutoSizer>
</React.Fragment>
);
}