import React, { type ReactNode, useMemo, useRef, useState } from 'react'; import { useLocale } from '../GrapesProvider'; import { classNames } from '../../utils'; import { getEnabledRowIds, tableSortFunction } from './utils'; import type { NextSortDirection, SortState, TableColumn, TableSelectionProps, TableVariant, } from './types'; import { TableEmptyState } from './TableEmptyState'; import { TableHeaderCell } from './TableHeaderCell'; import { TableHeaderCheckbox } from './TableHeaderCheckbox'; import { TableFooter } from './TableFooter'; import { TableProvider } from './TableProvider'; import { TableRow } from './TableRow'; import { TableRowCheckbox } from './TableRowCheckbox'; import { TableMini } from './TableMini'; import { groupByFn } from '../ListBox/utils'; import styles from './Table.module.css'; import tableStyles from '../../theme/table.module.css'; const NEXT_SORT_DIRECTIONS: NextSortDirection = { // when you click on an unsorted column, it goes to Descending order none: 'descending', descending: 'ascending', ascending: 'none', }; export type TableProps = { /** * className for the element. */ className?: string; /** * The columns to display. */ columns: TableColumn[]; /** * The data to display. */ data: T[]; /** * The footer of the Table. */ footer?: ReactNode; /** * The maximum height of the Table (will allow the rows to be scrollable). */ maxHeight?: number | string; /** * The size of the rows. * @default normal */ rowHeight?: 'normal' | 'compact'; /** * Function to get a row's unique identifier. */ getRowId?: (row: T) => string; /** * Function to get whether a row is active. */ getIsRowActive?(row: T): boolean; /** * Function to get whether a row is disabled. */ getIsRowDisabled?(row: T): boolean; /** * Get whether a given row should be checkable. * By default, all options can be checked. */ getIsRowCheckable?: (option: T) => boolean; /** * Function to get the row's visual style. */ getRowVariant?(row: T): TableVariant | undefined; /** * Handler that is called when a row is clicked. */ onRowClick?(row: T): void; /** * Defines the default sort state (direction and column id). * Sorting capability is disabled when groupBy is provided. */ defaultSortState?: SortState; /** * Defines the direction following each sort direction. * Sorting capability is disabled when groupBy is provided. */ nextSortDirection?: NextSortDirection; /** * The content to display if the data is empty. */ emptyState?: { title: string; subtitle?: ReactNode; }; /** * Function to group row together. * Disable sorting capability. */ groupBy?: (row: T) => string; /** * Render function for grouped option. * If not provided, the key returned by groupBy will be displayed */ renderGroupedRowHeader?: (value: string, aggregatedRows: T[]) => ReactNode; } & TableSelectionProps; function Table({ className, columns, data, footer, maxHeight, rowHeight = 'normal', selectedRowIds, getIsRowActive, getIsRowDisabled, getIsRowCheckable, getRowVariant, getRowId, groupBy, renderGroupedRowHeader, onRowClick, onRowSelectionChange, onAllRowsSelectionChange, defaultSortState = { direction: 'none', columnId: null, }, nextSortDirection = NEXT_SORT_DIRECTIONS, emptyState, ...rest }: TableProps) { const theadRef = useRef(null); const tbodyRef = useRef(null); const [sortState, setSortState] = useState(defaultSortState); const locale = useLocale(); const Collator = useMemo(() => Intl.Collator(locale), [locale]); const isSelectable = getRowId && selectedRowIds && onRowSelectionChange; const memoizedData = useMemo(() => { return tableSortFunction(data, sortState, columns, Collator); }, [data, sortState, columns, Collator]); const groupedData = groupByFn(data, groupBy); const rowIds = getRowId ? getEnabledRowIds( memoizedData, getRowId, getIsRowDisabled, getIsRowCheckable, ) : // TODO: remove this condition once getRowId is mandatory. []; const memoizedColumns = useMemo(() => { if (isSelectable) { const selectableColumn: TableColumn = { id: 'checkboxes', className: styles.tableCheckboxCell, width: 48, renderCell: (row) => , header: , }; return [selectableColumn, ...columns]; } return columns; }, [columns, isSelectable]); return ( {memoizedColumns.map((column) => ( ))} {groupBy ? Object.entries(groupedData).map( ([key, aggregatedRows]: [string, T[]]) => { return [ , aggregatedRows.map((row, rowIndex) => ( )), ]; }, ) : memoizedData.map((row, rowIndex) => ( ))} {data.length === 0 && ( )} {footer && ( {footer} )}
{renderGroupedRowHeader ? renderGroupedRowHeader(key, aggregatedRows) : key}
); } Table.Mini = TableMini; export { Table };