import React, { useState } from 'react'; import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import Table, { TableProps } from '@material-ui/core/Table'; import TableBody, { TableBodyProps } from '@material-ui/core/TableBody'; import TableCell, { TableCellProps } from '@material-ui/core/TableCell'; import TableHead, { TableHeadProps } from '@material-ui/core/TableHead'; import TablePagination, { TablePaginationProps, } from '@material-ui/core/TablePagination'; import TableRow, { TableRowProps } from '@material-ui/core/TableRow'; import TableSortLabel from '@material-ui/core/TableSortLabel'; import { getHeaders, getColumns, merge } from './utils'; type ResolvePropType = | T | ((obj: { rowData: TData; column: ColumnDef }) => T | void); function resolveProp( prop: ResolvePropType | undefined, args: any ) { return prop instanceof Function ? prop(args) : prop; } export const useStyles = makeStyles(theme => ({ container: {}, tableWrapper: {}, cellSelected: { backgroundColor: theme.palette.grey[100], }, cellHovered: { backgroundColor: theme.palette.grey[200], }, })); export type ColumnDef = { name: string; header?: string | React.ReactNode; cell?: (data: TData, index: number) => React.ReactNode; cellProps?: ResolvePropType; onClick?: TableCellProps['onClick']; onHeaderClick?: boolean | ((obj: { column: ColumnDef }) => void); headerCellProps?: TableCellProps; bodyCellProps?: TableCellProps; orderBy?: string | boolean | ((a: any, b: any) => number); columns?: ColumnDef[]; }; export type MuiTableProps = { data: TData[] | null; columns: ColumnDef[]; bodyProps?: TableBodyProps; containerProps?: any; includeHeaders?: boolean; headerProps?: TableHeadProps; rowProps?: ResolvePropType; headerRowProps?: ResolvePropType; headerCellProps?: ResolvePropType; bodyRowProps?: ResolvePropType; bodyCellProps?: ResolvePropType; cellProps?: ResolvePropType; onHeaderClick?: (obj: { column: ColumnDef }) => void; onCellClick?: (obj: { rowData: TData; column: ColumnDef }) => void; orderBy?: string; orderDirection?: 'asc' | 'desc'; pagination?: TablePaginationProps; addPlaceholderRows?: boolean; tableWrapperProps?: React.HTMLAttributes; isCellHovered?: (obj: { column: ColumnDef; rowData: TData | null; hoveredColumn: ColumnDef | null; hoveredRowData: TData | null; }) => boolean; isCellSelected?: (obj: { column: ColumnDef; rowData: TData | null; }) => boolean; classes?: { container?: string; tableWrapper?: string; cellHovered?: string; cellSelected?: string; }; } & TableProps; function MuiTable(props: MuiTableProps) { const { data, columns, containerProps, tableWrapperProps, headerProps, bodyProps, rowProps, headerRowProps, bodyRowProps, cellProps: defaultCellProps, headerCellProps: defaultHeaderCellProps, bodyCellProps: defaultBodyCellProps, includeHeaders, onHeaderClick, onCellClick, isCellHovered, isCellSelected, pagination, addPlaceholderRows, orderBy, orderDirection, ...tableProps } = props; const [state, setState] = useState<{ hoveredColumn: ColumnDef | null; hoveredRowData: T | null; }>({ hoveredColumn: null, hoveredRowData: null, }); const classes = useStyles(); const { hoveredColumn, hoveredRowData } = state; return (
{includeHeaders && ( {getHeaders(columns).map((headerRow, headerRowIndex) => ( {headerRow && headerRow.map((column, _columnIndex) => { const contents = column.header || column.name; const isHovered = hoveredColumn && hoveredRowData && isCellHovered && isCellHovered({ column, rowData: null, hoveredColumn, hoveredRowData, }); const isSelected = isCellSelected && isCellSelected({ column, rowData: null }); const className = clsx({ [classes.cellHovered]: isHovered, [classes.cellSelected]: isSelected, }); const cellProps = merge( {}, { className }, resolveProp(defaultCellProps, { column, rowData: null, hoveredColumn, hoveredRowData, }), resolveProp(column.cellProps, { column, rowData: null, hoveredColumn, hoveredRowData, }), resolveProp(defaultHeaderCellProps, { column, rowData: null, hoveredColumn, hoveredRowData, }), resolveProp(column.headerCellProps, { column, rowData: null, hoveredColumn, hoveredRowData, }) ); return ( { setState({ hoveredColumn: column, hoveredRowData: null, }); }, onMouseLeave: () => setState({ hoveredColumn: null, hoveredRowData: null, }), })} key={`header-cell-${column.name}`} colSpan={column.colSpan} rowSpan={column.rowSpan} {...cellProps} > {column.orderBy !== false && column.onHeaderClick !== false && (column.onHeaderClick || onHeaderClick) ? ( typeof column.onHeaderClick === 'function' ? column.onHeaderClick({ column }) : onHeaderClick?.({ column }) } > {contents} ) : ( contents )} ); })} ))} )} {data && data.map((rowData, rowIndex) => { return ( {getColumns(columns).map((column, _columnIndex) => { const contents = column.cell ? column.cell(rowData, rowIndex) : // @ts-ignore: add index signature rowData[column.name]; const isHovered = hoveredColumn && hoveredRowData && isCellHovered && isCellHovered({ column, rowData, hoveredColumn, hoveredRowData, }); const isSelected = isCellSelected && isCellSelected({ column, rowData }); const className = clsx({ [classes.cellHovered]: isHovered, [classes.cellSelected]: isSelected, }); const cellProps = merge( {}, { className }, resolveProp(defaultCellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(column.cellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(defaultBodyCellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(column.bodyCellProps, { column, rowData, hoveredColumn, hoveredRowData, }) ); return ( { setState({ hoveredColumn: column, hoveredRowData: rowData, }); }, onMouseLeave: () => setState({ hoveredColumn: null, hoveredRowData: null, }), })} {...(onCellClick && { onClick: () => { if (window.getSelection()?.toString() === '') { onCellClick({ column, rowData }); // Can be overridden by cellProps.onClick on column definition } }, })} key={`body-cell-${rowIndex}-${column.name}`} {...cellProps} > {contents} ); })} ); })} {/* Fill remaining space to keep pagination controls in consistent location */} {pagination && addPlaceholderRows && pagination.rowsPerPage > (data ? data.length : 0) && Array.from({ length: pagination.rowsPerPage - (data ? data.length : 0), }).map((rowData, rowIndex) => ( {columns.map(column => { const cellProps: any = merge( {}, resolveProp(defaultCellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(column.cellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(defaultBodyCellProps, { column, rowData, hoveredColumn, hoveredRowData, }), resolveProp(column.bodyCellProps, { column, rowData, hoveredColumn, hoveredRowData, }) ); cellProps.style = { ...cellProps.style, visibility: 'hidden', }; return (   ); })} ))}
{pagination && ( // @ts-ignore - `component` explicitly omitted from props but allowed (https://github.com/mui-org/material-ui/commit/8dc12394addced6c1ae34e0a05a3a799efe4ca6c#diff-33ba99dbf7e0ca4f0d9c07e27849f8d3R47) )}
); } export { getHeaders, getColumns }; // export default MuiTable; // Workaround: Generic Props lost with React memo - https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-542793243 // export default React.memo(MuiTable); const typedMemo: (c: T) => T = React.memo; export default typedMemo(MuiTable);