UNPKG

@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
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 };