uui/components/tables/DataTable.tsx (157 lines of code) (raw):

import * as React from 'react'; import { IconContainer, DataTableSelectionProvider, DataTableFocusManager, DataTableFocusProvider } from '@epam/uui-components'; import { useColumnsWithFilters } from '../../helpers'; import { ColumnsConfig, DataRowProps, useUuiContext, uuiScrollShadows, useColumnsConfig, IEditable, DataTableState, DataTableColumnsConfigOptions, DataSourceListProps, DataColumnProps, cx, TableFiltersConfig, DataTableRowProps, DataTableSelectedCellData, Overwrite, DataColumnGroupProps, } from '@epam/uui-core'; import { DataTableHeaderRow, DataTableHeaderRowProps } from './DataTableHeaderRow'; import { DataTableRow, DataTableRowProps as UuiDataTableRowProps } from './DataTableRow'; import { DataTableMods, DataTableModsOverride, DataTableRowMods } from './types'; import { ColumnsConfigurationModal, ColumnsConfigurationModalProps } from './columnsConfigurationModal'; import { VirtualList, VirtualListRenderRowsParams, VirtualListProps } from '../layout'; import { DataRowsContainer } from './DataRowsContainer'; import { ReactComponent as EmptyTableIcon } from '../../icons/pictures/empty-table.svg'; import { Text } from '../typography'; import { i18n } from '../../i18n'; import { settings } from '../../settings'; import './variables.scss'; import css from './DataTable.module.scss'; export interface DataTableProps<TItem, TId, TFilter = any> extends IEditable<DataTableState>, DataSourceListProps, DataTableColumnsConfigOptions, Pick<VirtualListProps, 'onScroll'> { /** Callback to get rows that will be rendered in table */ getRows?(): DataRowProps<TItem, TId>[]; /** Rows that should be rendered in table */ rows?: DataRowProps<TItem, TId>[]; /** Array of all possible column groups for the table */ columnGroups?: DataColumnGroupProps[]; /** Array of all possible columns for the table */ columns: DataColumnProps<TItem, TId>[]; /** Render callback for the table row. * If omitted, default DataTableRow implementation will be rendered. * */ renderRow?(props: DataTableRowProps<TItem, TId>): React.ReactNode; /** Render callback for the 'No results' block. Will be rendered when table doesn't have rows for displaying, e.g. after search applying. * If omitted, default implementation will be rendered. * */ renderNoResultsBlock?(): React.ReactNode; /** Pass true to enable the column configuration button in the last column header. On this button click will show the columns configuration modal. * Note that you need to have at least one column fixed to the right for proper display * */ showColumnsConfig?: boolean; /** Array of filters to be added to the column header. * For each filter, you need to specify the `columnKey` of the column where it will be attached. * */ filters?: TableFiltersConfig<any>[]; /** Called when cell content is copied to other cells via the DataTable cell copying mechanism. * This callback is typically used to update the state according to the changes. * To enable cell copying, provide the canCopy prop for the column. * */ onCopy?: (copyFrom: DataTableSelectedCellData<TItem, TId, TFilter>, selectedCells: DataTableSelectedCellData<TItem, TId, TFilter>[]) => void; /** Render callback for column configuration modal. * If omitted, default `ColumnsConfigurationModal` implementation will be rendered. */ renderColumnsConfigurationModal?: (props: ColumnsConfigurationModalProps<TItem, TId, TFilter>) => React.ReactNode; dataTableFocusManager?: DataTableFocusManager<TId>; /** * Enables collapse/expand all functionality. * */ showFoldAll?: boolean; } export function DataTable<TItem, TId>(props: React.PropsWithChildren<DataTableProps<TItem, TId> & Overwrite<DataTableMods, DataTableModsOverride>>) { const { uuiModals } = useUuiContext(); const headerRef = React.useRef<HTMLDivElement>(); const columnsWithFilters = useColumnsWithFilters(props.columns, props.filters); const { columns, config, defaultConfig } = useColumnsConfig(columnsWithFilters, props.value?.columnsConfig); const defaultRenderRow = React.useCallback((rowProps: DataRowProps<TItem, TId> & DataTableRowMods) => { return ( <DataTableRow key={ rowProps.rowKey } size={ props.size || settings.sizes.dataTable.body.row.default as UuiDataTableRowProps['size'] } columnsGap={ props.columnsGap } borderBottom={ props.border } { ...rowProps } cx={ css.cell } /> ); }, []); const renderRow = (row: DataRowProps<TItem, TId>) => (props.renderRow ?? defaultRenderRow)({ ...row, columns }); const rows = props.getRows?.() ?? props.rows ?? []; const renderNoResultsBlock = React.useCallback(() => { return ( <div className={ css.noResults }> {props.renderNoResultsBlock ? ( props.renderNoResultsBlock?.() ) : ( <> <IconContainer cx={ css.icon } icon={ EmptyTableIcon } /> <Text cx={ css.title } fontSize="24" lineHeight="30" color="primary" fontWeight="600"> {i18n.tables.noResultsBlock.title} </Text> <Text fontSize="16" lineHeight="24" color="primary"> {i18n.tables.noResultsBlock.message} </Text> </> )} </div> ); }, [props.renderNoResultsBlock]); const onConfigurationButtonClick = React.useCallback(() => { const configProps = { columns: props.columns, columnsConfig: { ...config }, defaultConfig }; uuiModals .show<ColumnsConfig>((modalProps) => { return ( props.renderColumnsConfigurationModal ? props.renderColumnsConfigurationModal({ ...configProps, ...modalProps }) : ( <ColumnsConfigurationModal { ...modalProps } columns={ props.columns } columnsConfig={ config } defaultConfig={ defaultConfig } /> ) ); }) .then((columnsConfig) => props.onValueChange({ ...props.value, columnsConfig })) .catch(() => null); }, [ props.columns, config, defaultConfig, props.value, props.onValueChange, props.renderColumnsConfigurationModal, ]); const renderRowsContainer = React.useCallback( ({ listContainerRef, estimatedHeight, offsetY, scrollShadows }: VirtualListRenderRowsParams) => ( <> <div className={ css.stickyHeader } ref={ headerRef }> <DataTableHeaderRow columns={ columns } columnGroups={ props.columnGroups } onConfigButtonClick={ props.showColumnsConfig && onConfigurationButtonClick } selectAll={ props.selectAll } size={ props.headerSize || settings.sizes.dataTable.header.row.default as DataTableHeaderRowProps['size'] } textCase={ props.headerTextCase } allowColumnsReordering={ props.allowColumnsReordering } allowColumnsResizing={ props.allowColumnsResizing } showFoldAll={ props.showFoldAll } value={ { ...props.value, columnsConfig: config } } onValueChange={ props.onValueChange } columnsGap={ props.columnsGap } /> <div className={ cx(uuiScrollShadows.top, { [uuiScrollShadows.topVisible]: scrollShadows.verticalTop, }) } /> </div> {props.exactRowsCount !== 0 ? ( <DataRowsContainer headerRef={ headerRef } listContainerRef={ listContainerRef } estimatedHeight={ estimatedHeight } offsetY={ offsetY } scrollShadows={ scrollShadows } renderRow={ renderRow } rows={ rows } /> ) : ( renderNoResultsBlock?.() )} </> ), [ props, columns, rows, renderNoResultsBlock, onConfigurationButtonClick, ], ); return ( <DataTableSelectionProvider onCopy={ props.onCopy } rows={ rows } columns={ columns }> <DataTableFocusProvider dataTableFocusManager={ props.dataTableFocusManager }> <VirtualList value={ props.value } onValueChange={ props.onValueChange } onScroll={ props.onScroll } rowsCount={ props.rowsCount } renderRows={ renderRowsContainer } cx={ cx(css.root, 'uui-dt-vars') } isLoading={ props.isReloading } rowsSelector="[role=row]" rawProps={ { role: 'table', 'aria-colcount': columns.length, 'aria-rowcount': props.rowsCount, } } /> </DataTableFocusProvider> </DataTableSelectionProvider> ); }