diff --git a/src/components/table/index.ts b/src/components/table/index.ts index 49f1841c..51ab06fa 100644 --- a/src/components/table/index.ts +++ b/src/components/table/index.ts @@ -14,5 +14,10 @@ declare module '@tanstack/react-table' { * Merged into the `style` prop of the default-rendered `` element. */ thStyle?: CSSProperties; + + /** + * Merged into the `style` prop of the default-rendered `` element. + */ + tdStyle?: CSSProperties; } } diff --git a/src/components/table/table_body.tsx b/src/components/table/table_body.tsx index b67aea30..4335dd35 100644 --- a/src/components/table/table_body.tsx +++ b/src/components/table/table_body.tsx @@ -1,3 +1,5 @@ +import { Icon } from '@blueprintjs/core'; +import styled from '@emotion/styled'; import type { Row, RowData } from '@tanstack/react-table'; import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'; import { notUndefined } from '@tanstack/react-virtual'; @@ -23,6 +25,14 @@ interface TableBodyProps { virtualizer: Virtualizer; isReorderingEnabled: boolean; renderRowPreview?: TableRowPreviewRenderer; + + emptyContent: ReactNode; + emptyIcon: ReactNode; + + /** + * Bound to `EmptyRow`, it is needed for td colSpan. + */ + columns: number; } export function TableBody(props: TableBodyProps) { @@ -36,6 +46,9 @@ export function TableBody(props: TableBodyProps) { ) as TableRowTrRenderer, virtualizer, virtualizeRows, + emptyContent, + emptyIcon, + columns, } = props; if (virtualizeRows) { @@ -72,6 +85,13 @@ export function TableBody(props: TableBodyProps) { }} /> ))} + {virtualItems.length === 0 && ( + + )} {after > 0 && ( @@ -91,10 +111,50 @@ export function TableBody(props: TableBodyProps) { } /> ))} + {rows.length === 0 && ( + + )} ); } +interface EmptyRowProps { + emptyContent: ReactNode; + emptyIcon: ReactNode; + columns: number; +} + +function EmptyRow(props: EmptyRowProps) { + const { + emptyIcon = , + emptyContent = 'No data', + columns, + } = props; + + return ( + + + + {emptyIcon} + {emptyContent} + + + + ); +} + +const EmptyState = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0.25em; + gap: 0.5em; +`; + type TableRowRenderer = (row: Row) => ReactNode; function TableRow({ diff --git a/src/components/table/table_root.tsx b/src/components/table/table_root.tsx index d515dbf0..27c46847 100644 --- a/src/components/table/table_root.tsx +++ b/src/components/table/table_root.tsx @@ -12,7 +12,12 @@ import { useReactTable, } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; -import type { CSSProperties, RefObject, TableHTMLAttributes } from 'react'; +import type { + CSSProperties, + ReactNode, + RefObject, + TableHTMLAttributes, +} from 'react'; import { useEffect, useMemo, useRef } from 'react'; import { match } from 'ts-pattern'; @@ -185,6 +190,18 @@ interface TableBaseProps { * Ignored when using custom row rendering with `renderRowTr`. */ renderRowPreview?: TableRowPreviewRenderer; + + /** + * Icon to display when the table is empty. + * @default `` + */ + emptyIcon?: ReactNode; + + /** + * Content to display when the table is empty. + * @default `'No data'` + */ + emptyContent?: ReactNode; } interface RegularTableProps< @@ -244,6 +261,9 @@ export function Table(props: TableProps) { renderRowPreview, scrollToRowRef, + + emptyIcon, + emptyContent, } = props; const isReorderingEnabled = !!onRowOrderChanged; const virtualScrollElementRef = useRef(null); @@ -294,13 +314,16 @@ export function Table(props: TableProps) { }), [className, getTdProps, renderRowTr, columns, compact], ); + + const rows = table.getRowModel().rows; + return ( } > { onRowOrderChanged?.(items.map((item) => item.original)); }} @@ -341,7 +364,7 @@ export function Table(props: TableProps) { /> )} (props: TableProps) { virtualizeRows={virtualizeRows} renderRowPreview={renderRowPreview} isReorderingEnabled={isReorderingEnabled} + emptyIcon={emptyIcon} + emptyContent={emptyContent} + columns={table.getAllColumns().length} /> diff --git a/src/components/table/table_row_cell.tsx b/src/components/table/table_row_cell.tsx index 8b6f80a4..e580b3fb 100644 --- a/src/components/table/table_row_cell.tsx +++ b/src/components/table/table_row_cell.tsx @@ -15,8 +15,9 @@ export function TableRowCell( ) { const { cell, tdStyle, getTdProps } = props; + const tdStyleMeta = cell.column.columnDef.meta?.tdStyle; const tdProps = getTdProps?.(cell); - const style = { ...tdStyle, ...tdProps?.style }; + const style = { ...tdStyleMeta, ...tdStyle, ...tdProps?.style }; return ( diff --git a/stories/components/table.stories.tsx b/stories/components/table.stories.tsx index 904e0d10..f58b4b9a 100644 --- a/stories/components/table.stories.tsx +++ b/stories/components/table.stories.tsx @@ -6,7 +6,7 @@ import type { GetTdProps, TableProps } from '../../src/components/index.js'; import { Table, TableRowTr } from '../../src/components/index.js'; import { table } from '../data/data.js'; -import { columns } from './table_columns.js'; +import { columns, columnsNativeMeta } from './table_columns.js'; type TableRecord = (typeof table)[number]; @@ -111,3 +111,18 @@ export const WithTdStyle = { export const Virtualized: Story = { args: { virtualizeRows: true, estimatedRowHeight: () => 172 }, } satisfies Story; + +export const WithMetaThAndTdStyle: Story = { + args: { + data: table.slice(0, 1), + columns: columnsNativeMeta, + }, +}; + +export const EmptyTable: Story = { + args: { + data: [], + emptyContent: 'No molecules', + emptyIcon: , + }, +}; diff --git a/stories/components/table_columns.tsx b/stories/components/table_columns.tsx index c132f2a5..89476f69 100644 --- a/stories/components/table_columns.tsx +++ b/stories/components/table_columns.tsx @@ -65,3 +65,23 @@ export const columns = [ }, }), ]; + +export const columnsNativeMeta = [ + columnHelper.accessor('ocl.idCode', { + header: 'Molecule', + cell: ({ getValue }) => , + }), + columnHelper.accessor('name', { + header: 'Name', + meta: { + thStyle: { + backgroundColor: 'black', + color: '#FFF903', + }, + tdStyle: { + backgroundColor: '#DB261D', + color: '#FFF903', + }, + }, + }), +];