@equinor/eds-data-grid-react
Version:
A feature-rich data-grid written in React, implementing the Equinor Design System
468 lines (456 loc) • 15.9 kB
JavaScript
import { Typography, useEds, Table, Pagination } from '@equinor/eds-core-react';
import { getExpandedRowModel, getCoreRowModel, getSortedRowModel, getFacetedRowModel, getFacetedUniqueValues, getFacetedMinMaxValues, getFilteredRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { forwardRef, useState, useEffect, useMemo, useRef, useCallback } from 'react';
import styled from 'styled-components';
import { TableProvider } from './EdsDataGridContext.js';
import { TableHeaderRow } from './components/TableHeaderRow.js';
import { TableFooterRow } from './components/TableFooterRow.js';
import { TableRow } from './components/TableRow.js';
import { addPxSuffixIfInputHasNoPrefix, logDevelopmentWarningOfPropUse } from './utils.js';
import { mergeRefs } from '@equinor/eds-utils';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
function EdsDataGridInner({
rows,
columns,
columnResizeMode,
pageSize,
rowSelection,
enableRowSelection,
enableMultiRowSelection,
enableSubRowSelection,
selectedRows,
rowSelectionState,
enableColumnFiltering,
debug,
enablePagination,
enableSorting,
stickyHeader,
stickyFooter,
onSelectRow,
onRowSelectionChange,
caption,
enableVirtual,
virtualHeight,
columnVisibility,
columnVisibilityChange,
emptyMessage,
columnOrder,
cellClass,
cellStyle,
rowClass,
rowStyle,
headerClass,
headerStyle,
footerClass,
footerStyle,
externalPaginator,
onSortingChange,
manualSorting,
sortingState,
columnPinState,
scrollbarHorizontal,
width,
minWidth,
height,
getRowId,
rowVirtualizerInstanceRef,
tableInstanceRef,
columnSizing,
onColumnResize,
expansionState,
setExpansionState,
getSubRows,
defaultColumn,
onRowContextMenu,
onRowClick,
onRowDoubleClick,
onCellClick,
enableFooter,
enableSortingRemoval,
...rest
}, ref) {
logDevelopmentWarningOfPropUse({
virtualHeight: {
value: virtualHeight,
mitigationInfo: "Use 'height' instead."
},
rowSelection: {
value: rowSelection,
mitigationInfo: "Use 'enableRowSelection' instead."
},
onSelectRow: {
value: onSelectRow,
mitigationInfo: "Use 'onRowSelectionChange' instead."
},
selectedRows: {
value: selectedRows,
mitigationInfo: "Use 'rowSelectionState' instead."
}
});
const [sorting, setSorting] = useState(sortingState ?? []);
const [internalRowSelectionState, setInternalRowSelectionState] = useState(rowSelectionState ?? selectedRows ?? {});
const [columnPin, setColumnPin] = useState(columnPinState ?? {});
const [columnFilters, setColumnFilters] = useState([]);
const [internalColumnSize, setInternalColumnSize] = useState(columnSizing ?? {});
const [visible, setVisible] = useState(columnVisibility ?? {});
const [globalFilter, setGlobalFilter] = useState('');
const [columnOrderState, setColumnOrderState] = useState([]);
const [page, setPage] = useState({
pageIndex: 0,
pageSize: pageSize ?? 25
});
useEffect(() => {
if (virtualHeight) {
console.warn(`virtualHeight prop is deprecated and will be removed in a later version. Please update your code to use height instead.`);
}
}, [virtualHeight]);
useEffect(() => {
setVisible(columnVisibility ?? {});
}, [columnVisibility, setVisible]);
useEffect(() => {
setColumnPin(s => columnPinState ?? s);
}, [columnPinState]);
useEffect(() => {
setSorting(sortingState);
}, [sortingState]);
useEffect(() => {
setInternalRowSelectionState(rowSelectionState ?? selectedRows ?? {});
}, [rowSelectionState, selectedRows]);
/**
* By default, the filter-function accepts single-value filters. This adds multi-filter functionality out of the box.
*/
const _columns = useMemo(() => {
return columns.map(column => {
if (column.filterFn || column.enableColumnFilter === false) {
return column;
}
/* istanbul ignore next */
return {
...column,
filterFn: (row, columnId, filterValue) => {
if (debug) {
console.log('filterFn', row, columnId, filterValue);
}
if (!filterValue || (Array.isArray(filterValue) || typeof filterValue === 'string') && filterValue.length === 0) {
return true;
}
const value = row.getValue(columnId) ?? 'NULL_OR_UNDEFINED';
if (Array.isArray(filterValue)) {
const numeric = filterValue.some(v => typeof v === 'number');
if (numeric) {
const [start, end] = filterValue;
return Number(value) >= (isNaN(start) ? 0 : start) && Number(value) <= (!end || isNaN(end) ? Infinity : end);
} else {
const validFilterValue = filterValue.filter(v => !!v);
if (validFilterValue.length === 0) return true;
return filterValue.includes(value);
}
}
return value === filterValue;
}
};
});
}, [debug, columns]);
/**
* Set up default table options
*/
const options = {
data: rows,
columns: _columns,
defaultColumn: defaultColumn ?? {
size: 150,
cell: context => {
return /*#__PURE__*/jsx(Typography, {
style: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},
group: "table",
variant: "cell_text",
children: String(context.getValue() ?? '')
});
}
},
columnResizeMode: columnResizeMode,
onColumnSizingChange: change => {
if (typeof change === 'function') {
setInternalColumnSize(change(internalColumnSize));
} else {
setInternalColumnSize(change);
}
if (onColumnResize) {
onColumnResize(internalColumnSize);
}
},
state: {
sorting,
columnPinning: columnPin,
rowSelection: internalRowSelectionState,
columnOrder: columnOrderState,
columnSizing: columnSizing ?? internalColumnSize,
expanded: expansionState
},
getSubRows: getSubRows,
onExpandedChange: setExpansionState,
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: changes => {
if (onSortingChange) {
onSortingChange(changes);
}
setSorting(changes);
},
enableColumnFilters: !!enableColumnFiltering,
enableFilters: !!enableColumnFiltering,
enableSorting: enableSorting ?? false,
manualSorting: manualSorting ?? false,
enableColumnResizing: !!columnResizeMode,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: debug,
debugHeaders: debug,
debugColumns: debug,
enableRowSelection: enableRowSelection ?? rowSelection ?? false,
enableMultiRowSelection: enableMultiRowSelection ?? false,
enableSubRowSelection: enableSubRowSelection ?? false,
enableColumnPinning: true,
enablePinning: true,
getRowId,
enableSortingRemoval: enableSortingRemoval ?? true
};
useEffect(() => {
if (columnOrder && columnOrder.length > 0) {
setColumnOrderState(columnOrder ?? []);
}
}, [columnOrder]);
/**
* Set up handlers for rowSelection
*/
if (enableRowSelection ?? rowSelection ?? false) {
options.onRowSelectionChange = updaterOrValue => {
onSelectRow?.(updaterOrValue);
onRowSelectionChange?.(updaterOrValue);
setInternalRowSelectionState(updaterOrValue);
};
}
/**
* Set up config for column filtering
*/
if (enableColumnFiltering) {
options.state.columnFilters = columnFilters;
options.state.globalFilter = globalFilter;
options.onColumnFiltersChange = setColumnFilters;
options.onGlobalFilterChange = setGlobalFilter;
options.getFacetedRowModel = getFacetedRowModel();
options.getFacetedUniqueValues = getFacetedUniqueValues();
options.getFacetedMinMaxValues = getFacetedMinMaxValues();
options.getFilteredRowModel = getFilteredRowModel();
}
/**
* Set up config for pagination
*/
if (enablePagination ?? false) {
options.state.pagination = page;
options.getPaginationRowModel = getPaginationRowModel();
}
/**
* Set up config to handle column visibility controls
*/
if (columnVisibility) {
options.state.columnVisibility = visible;
options.onColumnVisibilityChange = vis => {
let updated;
if (typeof vis === 'function') {
updated = vis(visible);
} else {
updated = vis;
}
if (columnVisibilityChange) columnVisibilityChange(updated);
setVisible(updated);
};
}
useEffect(() => {
setPage(prev => ({
...prev,
pageSize: pageSize ?? 25
}));
}, [pageSize]);
const table = useReactTable(options);
if (tableInstanceRef) {
tableInstanceRef.current = table;
}
let tableWrapperStyle = {};
/**
* Style the parent container to enable virtualization.
* By not setting this, the virtual-scroll will always render every row, reducing computational overhead if turned off.
*/
if (enableVirtual) {
tableWrapperStyle = {
height: height ?? virtualHeight ?? 500,
overflow: 'auto',
position: 'relative'
};
}
const parentRef = useRef(null);
const combinedRef = useMemo(() => mergeRefs(parentRef, ref), [parentRef, ref]);
/**
* Virtualization setup
*/
const {
density
} = useEds();
const estimateSize = useCallback(() => {
return density === 'compact' ? 32 : 48;
}, [density]);
const virtualizer = useVirtualizer({
count: table.getRowModel().rows.length,
getScrollElement: () => parentRef.current,
estimateSize
});
if (rowVirtualizerInstanceRef) rowVirtualizerInstanceRef.current = virtualizer;
const virtualRows = virtualizer.getVirtualItems();
const paddingTop = virtualRows.length ? virtualRows[0].start : 0;
const paddingBottom = virtualRows.length ? virtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end : 0;
// These classes are primarily used to allow for feature-detection in the test-suite
const classList = {
'sticky-header': !!stickyHeader,
'sticky-footer': !!stickyFooter,
virtual: !!enableVirtual,
paging: !!enablePagination
};
return /*#__PURE__*/jsxs(TableProvider, {
cellClass: cellClass,
cellStyle: cellStyle,
rowClass: rowClass,
rowStyle: rowStyle,
headerClass: headerClass,
headerStyle: headerStyle,
footerClass: footerClass,
footerStyle: footerStyle,
table: table,
enableSorting: !!enableSorting,
enableColumnFiltering: !!enableColumnFiltering,
stickyHeader: !!stickyHeader,
stickyFooter: !!stickyFooter,
children: [/*#__PURE__*/jsxs(TableWrapper, {
...rest,
className: `table-wrapper ${rest.className ?? ''}`,
style: {
...rest.style,
...tableWrapperStyle
},
ref: combinedRef,
$height: height,
$width: width,
$scrollbarHorizontal: scrollbarHorizontal,
children: [/*#__PURE__*/jsxs(Table, {
className: Object.entries(classList).filter(([, k]) => k).map(([k]) => k).join(' '),
style: {
tableLayout: scrollbarHorizontal ? 'fixed' : 'auto',
width: table.getTotalSize(),
minWidth: scrollbarHorizontal ? minWidth : 'auto'
},
children: [caption && /*#__PURE__*/jsx(Table.Caption, {
children: caption
}), /*#__PURE__*/jsx(Table.Head, {
sticky: stickyHeader,
children: table.getHeaderGroups().map(headerGroup => /*#__PURE__*/jsx(TableHeaderRow, {
table: table,
headerGroup: headerGroup,
columnResizeMode: columnResizeMode,
deltaOffset: table.getState().columnSizingInfo.deltaOffset
}, headerGroup.id))
}), /*#__PURE__*/jsxs(Table.Body, {
style: {
backgroundColor: 'inherit'
},
children: [table.getRowModel().rows.length === 0 && emptyMessage && /*#__PURE__*/jsx(Table.Row, {
children: /*#__PURE__*/jsx(Table.Cell, {
colSpan: table.getFlatHeaders().length,
children: emptyMessage
})
}), enableVirtual && /*#__PURE__*/jsxs(Fragment, {
children: [paddingTop > 0 && /*#__PURE__*/jsx(Table.Row, {
"data-testid": "virtual-padding-top",
className: 'virtual-padding-top',
style: {
pointerEvents: 'none'
},
children: /*#__PURE__*/jsx(Table.Cell, {
style: {
height: `${paddingTop}px`
}
})
}), virtualRows.map(virtualItem => {
const row = table.getRowModel().rows[virtualItem.index];
return /*#__PURE__*/jsx(TableRow, {
row: row,
onContextMenu: onRowContextMenu ? event => onRowContextMenu(row, event) : undefined,
onClick: onRowClick ? event => onRowClick(row, event) : undefined,
onDoubleClick: onRowDoubleClick ? event => onRowDoubleClick(row, event) : undefined,
onCellClick: onCellClick
}, virtualItem.index);
}), paddingBottom > 0 && /*#__PURE__*/jsx(Table.Row, {
"data-testid": "virtual-padding-bottom",
className: 'virtual-padding-bottom',
style: {
pointerEvents: 'none'
},
children: /*#__PURE__*/jsx(Table.Cell, {
style: {
height: `${paddingBottom}px`
}
})
})]
}), !enableVirtual && table.getRowModel().rows.map(row => /*#__PURE__*/jsx(TableRow, {
row: row,
onContextMenu: onRowContextMenu ? event => onRowContextMenu(row, event) : undefined,
onClick: onRowClick ? event => onRowClick(row, event) : undefined,
onCellClick: onCellClick
}, row.id))]
}), enableFooter && /*#__PURE__*/jsx(Table.Foot, {
sticky: stickyFooter,
"data-testid": "eds-grid-footer",
children: table.getFooterGroups().map(footerGroup => /*#__PURE__*/jsx(TableFooterRow, {
table: table,
footerGroup: footerGroup,
columnResizeMode: columnResizeMode,
deltaOffset: table.getState().columnSizingInfo.deltaOffset
}, footerGroup.id))
})]
}), externalPaginator ? externalPaginator : enablePagination && /*#__PURE__*/jsx("div", {
style: {
maxWidth: `${table.getTotalSize()}px`
},
children: /*#__PURE__*/jsx(Pagination, {
totalItems: table.getFilteredRowModel().rows.length,
withItemIndicator: true,
itemsPerPage: page.pageSize,
onChange: (e, p) => setPage(s => ({
...s,
pageIndex: p - 1
})),
defaultPage: 1
})
})]
}), debug && enableVirtual && /*#__PURE__*/jsxs("span", {
children: ["Visible items: ", virtualizer.range.startIndex, " -", ' ', virtualizer.range.endIndex, " / ", rows.length]
})]
});
}
const TableWrapper = styled.div.withConfig({
displayName: "EdsDataGrid__TableWrapper",
componentId: "sc-82fj3f-0"
})(["height:", ";width:", ";overflow:auto;contain:", ";"], ({
$height
}) => addPxSuffixIfInputHasNoPrefix($height) ?? 'auto', ({
$scrollbarHorizontal,
$width
}) => $scrollbarHorizontal ? addPxSuffixIfInputHasNoPrefix($width) ?? '100%' : 'auto', ({
$height,
$width
}) => Boolean($height) && Boolean($width) ? 'strict' : 'unset');
const EdsDataGrid = /*#__PURE__*/forwardRef(EdsDataGridInner);
export { EdsDataGrid };