diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 0ab6cac52..392cf472a 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -2,12 +2,13 @@ import { flexRender } from '@tanstack/react-table'; import React, { useRef } from 'react'; import isEqual from 'react-fast-compare'; -import type { TableProps } from '.'; -import { TBody, TH, TD, THead, TR, TTitle } from './components'; +import { TBody, TD, TH, THead, TR, TTitle, type TableProps } from '.'; import useTable from './hooks/useTable'; import { tableContainer, tableStyles } from './Table.style'; const Table = ({ + type = 'read-only', + rowsConfig, data, columns, rowSize = 'sm', @@ -16,14 +17,11 @@ const Table = ({ hasStickyHeader = false, sx, }: TableProps) => { - const { columnVisibility, setColumnVisibility } = columnsConfig ?? {}; - - const hasColumnVisibilityConfig = Boolean(columnVisibility && setColumnVisibility); - /** If true, the scrollbar of tbody is visible */ const [hasScrollbar, setHasScrollbar] = React.useState(false); const tBodyRef = useRef(); + const containerRef = useRef(null); React.useEffect(() => { if (tBodyRef?.current) { @@ -32,27 +30,35 @@ const Table = ({ }, [tBodyRef.current]); const table = useTable({ + type, data, columns, - /** Column Visibility */ - ...(hasColumnVisibilityConfig && { - state: { - columnVisibility, - }, - onColumnVisibilityChange: setColumnVisibility, - }), sorting, + rowsConfig, + columnsConfig, }); + const hasTitle = Boolean(columnsConfig || rowsConfig); + return ( -
- {hasColumnVisibilityConfig && } +
+ {hasTitle && ( + + )} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {row.getVisibleCells().map((cell) => { return ( - ); diff --git a/src/components/Table/components/TD/TD.style.ts b/src/components/Table/components/TD/TD.style.ts index 9f51c9663..6c4eebf9d 100644 --- a/src/components/Table/components/TD/TD.style.ts +++ b/src/components/Table/components/TD/TD.style.ts @@ -20,10 +20,20 @@ export const tdContainer = ({ rowSize, width, + isCheckbox, sx, - }: Pick, 'rowSize'> & { width?: number; isLastCell?: boolean; sx?: CSSObject }) => + }: Pick, 'rowSize'> & { + isCheckbox?: boolean; + width?: number; + isLastCell?: boolean; + sx?: CSSObject; + }) => (theme: Theme): SerializedStyles => { const getWidth = () => { + if (isCheckbox) { + return '52px'; + } + if (width) { return `${width}%`; } diff --git a/src/components/Table/components/TD/TD.tsx b/src/components/Table/components/TD/TD.tsx index 5819ccdaa..79eb21a65 100644 --- a/src/components/Table/components/TD/TD.tsx +++ b/src/components/Table/components/TD/TD.tsx @@ -14,11 +14,23 @@ type Props = { width?: number; /** Style overrides */ sx?: CSSObject; + /** Column Id */ + columnId?: string; }; -const TD: React.FCC = ({ colSpan, rowSize = 'sm', width, sx, children, ...rest }) => { +const TD: React.FCC = ({ + colSpan, + rowSize = 'sm', + width, + sx, + children, + columnId, + ...rest +}) => { + const isCheckbox = columnId === 'checkbox_select'; + return ( - ); diff --git a/src/components/Table/components/TH/TH.style.ts b/src/components/Table/components/TH/TH.style.ts index 9d4e50405..9d933e1ec 100644 --- a/src/components/Table/components/TH/TH.style.ts +++ b/src/components/Table/components/TH/TH.style.ts @@ -9,18 +9,20 @@ import { generateStylesFromTokens } from 'components/Typography/utils'; export const thContainer = ({ + isCheckbox, rowSize, width, hasVisibleOptions, sx, }: Pick, 'rowSize'> & { + isCheckbox?: boolean; width?: number; hasVisibleOptions?: boolean; sx?: CSSObject; }) => (theme: Theme): SerializedStyles => { return css` - width: ${width ? `${width}%` : '100%'}; + width: ${isCheckbox ? '52px' : width ? `${width}%` : '100%'}; height: ${getMinHeight(rowSize)(theme)}; align-content: center; text-align: left; diff --git a/src/components/Table/components/TH/TH.tsx b/src/components/Table/components/TH/TH.tsx index 74987f0a2..833656678 100644 --- a/src/components/Table/components/TH/TH.tsx +++ b/src/components/Table/components/TH/TH.tsx @@ -30,7 +30,7 @@ type Props = { sx?: CSSObject; }; -const TH: React.FCC> = ({ +const TH: React.FCC> = ({ width, rowSize = 'sm', children, @@ -39,9 +39,11 @@ const TH: React.FCC> = ({ colSortingState, resetSorting, sx, + id, ...rest }) => { const isSortable = Boolean(onSort); + const isCheckbox = id === 'checkbox_select'; const [hasVisibleOptions, setHasVisibleOptions] = React.useState(false); @@ -80,6 +82,7 @@ const TH: React.FCC> = ({ return (
({
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
{children}
(theme: Theme): SerializedStyles => { @@ -9,5 +11,7 @@ export const tTitleContainer = border-bottom: 1px solid ${theme.tokens.colors.get('borderColor.decorative.default')}; display: flex; justify-content: space-between; + height: 44px; + box-sizing: border-box; `; }; diff --git a/src/components/Table/components/TTitle/TTitle.tsx b/src/components/Table/components/TTitle/TTitle.tsx index 317def568..17b028b0d 100644 --- a/src/components/Table/components/TTitle/TTitle.tsx +++ b/src/components/Table/components/TTitle/TTitle.tsx @@ -1,17 +1,72 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import isEqual from 'react-fast-compare'; import ColumnChooser from './components/ColumnChooser'; import { tTitleContainer } from './TTitle.style'; import type { TableProps } from 'components/Table/types'; +import Typography from 'components/Typography'; -type Props = Pick, 'columnsConfig' | 'columns'>; +type Props = Pick, 'columnsConfig' | 'columns' | 'rowsConfig' | 'type'> & { + /** Element that that serves as the positioning boundary of the ColumnChooser Menu */ + containerRef: React.MutableRefObject; + /** Number of rows */ + rowsCount?: number; +}; + +const TTitle: React.FCC = ({ + type = 'read-only', + columns, + columnsConfig, + rowsConfig, + rowsCount, + containerRef, +}) => { + const { hasRowsCount, rowSelection, bulkActions, defaultAction } = rowsConfig ?? {}; + + const isSelectable = type === 'interactive' && rowSelection; + + const itemsToDisplay = isSelectable + ? Object.keys(rowSelection).length + : hasRowsCount + ? rowsCount + : 0; + + const hasTitle = isSelectable || hasRowsCount; + const hasVisibleActions = isSelectable && itemsToDisplay > 0; + + const title = useMemo( + () => ( +
+ + {itemsToDisplay} + + items {isSelectable ? 'selected' : ''} +
+ ), + [isSelectable, itemsToDisplay] + ); -const TTitle: React.FCC = ({ columns, columnsConfig, children }) => { return (
-
{children}
- +
+ {hasTitle && ( + <> + {title} + {hasVisibleActions && bulkActions} + + )} +
+ +
+ {hasVisibleActions && defaultAction} + {columnsConfig && ( + + )} +
); }; diff --git a/src/components/Table/components/TTitle/components/ColumnChooser/ColumnChooser.tsx b/src/components/Table/components/TTitle/components/ColumnChooser/ColumnChooser.tsx index d5de1807c..8ffd236cb 100644 --- a/src/components/Table/components/TTitle/components/ColumnChooser/ColumnChooser.tsx +++ b/src/components/Table/components/TTitle/components/ColumnChooser/ColumnChooser.tsx @@ -11,11 +11,14 @@ import { MenuItemWrapper, MenuWrapper, popoverStyle } from 'components/Menu/Menu import MenuItemDivider from 'components/Menu/MenuItemDivider'; import type { TableProps } from 'components/Table/types'; -type Props = Pick, 'columns' | 'columnsConfig'>; +type Props = Pick, 'columns' | 'columnsConfig'> & { + /** Element that that serves as the positioning boundary of the ColumnChooser Menu */ + containerRef: React.MutableRefObject; +}; /** @TODO create a generic Popover component */ -const ColumnChooser: React.FC = ({ columns, columnsConfig }) => { +const ColumnChooser: React.FC = ({ columns, columnsConfig, containerRef }) => { const [isBtnOpen, setBtnOpen] = React.useState(false); const options = flattenColumns(columns); @@ -64,7 +67,9 @@ const ColumnChooser: React.FC = ({ columns, columnsConfig }) => { aria-controls={isBtnOpen ? 'basic-menu' : undefined} aria-haspopup="true" aria-expanded={isBtnOpen ? 'true' : undefined} - iconLeftName="columnChooser" + /** @TODO add iconLeft functionality to compact Button */ + // iconLeftName="columnChooser" + size="compact" type="secondary" > Edit Columns @@ -75,6 +80,9 @@ const ColumnChooser: React.FC = ({ columns, columnsConfig }) => { isOpen={isBtnOpen} onOpenChange={() => setBtnOpen((isOpen) => !isOpen)} shouldCloseOnInteractOutside={() => true} + boundaryElement={containerRef.current} + /** @TODO adjust this when compact Button with iconLeft is implemented */ + crossOffset={-31} > = { getHeaderGroups: () => HeaderGroup[]; getRowModel: () => RowModel; + getIsAllRowsSelected: () => boolean; + getIsSomeRowsSelected: () => boolean; + getToggleAllRowsSelectedHandler: () => (event: unknown) => void; + toggleAllRowsSelected: (value: boolean) => void; }; -const getColumns = (columns: any[]) => { +const getColumns = (columns: any[], hasCheckboxes: boolean) => { const columnHelper = createColumnHelper(); + const base = hasCheckboxes + ? [ + { + id: 'checkbox_select', + header: ({ table }) => { + return ( + { + const selected = table.getIsAllRowsSelected(); // get selected status of current row. + table.toggleAllRowsSelected(!selected); + }} + /> + ); + }, + cell: ({ row }) => { + return ( +
+ { + const selected = row.getIsSelected(); // get selected status of current row. + row.toggleSelected(!selected); // reverse selected status of current row. + }} + /> +
+ ); + }, + }, + ] + : []; + return columns.reduce((tColumns, column) => { if ('columns' in column) { const groupConfig = { id: column.id, header: column.header, - columns: getColumns(column.columns), + columns: getColumns(column.columns, false), }; tColumns.push(columnHelper.group(groupConfig)); } else { @@ -33,38 +73,76 @@ const getColumns = (columns: any[]) => { } return tColumns; - }, []); + }, base); }; const useTable = ({ + type = 'read-only', data, columns, sorting, + rowsConfig, + columnsConfig, ...rest }: UseTableProps): ReturnValue => { - const tColumns = React.useMemo(() => getColumns(columns), [columns]); - const tData = React.useMemo(() => data.map((row) => row.cells), []); + const isTableInteractive = type === 'interactive'; + + const { rowSelection, setRowSelection } = rowsConfig ?? {}; + + const hasCheckboxes = Boolean(rowSelection && isTableInteractive); + + const tColumns = getColumns(columns, hasCheckboxes); + + const tData = React.useMemo(() => data.map((row) => row.cells), [data]); + + const state = React.useMemo(() => { + return { + ...(sorting && { sorting: sorting.sortingColumn }), + ...(rowSelection && isTableInteractive && { rowSelection }), + ...(columnsConfig && { columnVisibility: columnsConfig.columnVisibility }), + }; + }, [columnsConfig, isTableInteractive, rowSelection, sorting]); const table = useReactTable({ /** Basic Functionality */ data: tData, columns: tColumns, getCoreRowModel: getCoreRowModel(), + + /** States */ + state, + + /** States callbacks and extra config */ + /** Sorting */ ...(sorting && { manualSorting: true, - state: { - sorting: sorting.sortingColumn, - }, onSortingChange: sorting.handleSorting, enableMultiSort: sorting.isMultiSortable ?? false, }), + + /** Row Selection */ + ...(setRowSelection && + isTableInteractive && { + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + }), + + /** Column Visibility */ + ...(columnsConfig && { + onColumnVisibilityChange: columnsConfig.setColumnVisibility, + }), + ...rest, }); return { getHeaderGroups: table.getHeaderGroups, getRowModel: table.getRowModel, + getIsAllRowsSelected: table.getIsAllRowsSelected, + getIsSomeRowsSelected: table.getIsSomeRowsSelected, + getToggleAllRowsSelectedHandler: table.getToggleAllRowsSelectedHandler, + toggleAllRowsSelected: table.toggleAllRowsSelected, }; }; diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index c1fc48bdd..1335a23b5 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -1,7 +1,9 @@ import type { CSSObject } from '@emotion/react'; -import type { SortingState, OnChangeFn } from '@tanstack/react-table'; +import type { SortingState, OnChangeFn, RowSelectionState } from '@tanstack/react-table'; export type TableProps = { + /** If table is interactive, rows are selectable with actions */ + type?: 'interactive' | 'read-only'; /** The Columns configuration of the Table */ // Columns Type for Group Headers: columns: (DisplayColumn | GroupColumn)[]; columns: DisplayColumn[]; @@ -11,6 +13,8 @@ export type TableProps = { rowSize?: RowSize; /** Columns Configuration */ columnsConfig?: ColumnsConfig; + /** Rows Configuration */ + rowsConfig?: RowsConfig; /** Sorting Configuration */ sorting?: SortingConfig; /** Whether the table has a sticky header and scrollable tbody */ @@ -26,7 +30,10 @@ export type TableProps = { }; }; -export type UseTableProps = Pick, 'columns' | 'data' | 'sorting'> & +export type UseTableProps = Pick< + TableProps, + 'columns' | 'data' | 'sorting' | 'rowsConfig' | 'columnsConfig' +> & Partial, 'columns' | 'data'>>; /** Columns */ @@ -71,6 +78,19 @@ export type GroupColumn = { /** Rows & Cells */ +export type RowsConfig = { + /** Whether a rows counter should be displayed, regardless of row selection functionality */ + hasRowsCount?: boolean; + /** State which indicated which rows are selected */ + rowSelection?: RowSelectionState; + /** Callback for row selection state change */ + setRowSelection?: (state: RowSelectionState) => void; + /** Default action for rows */ + defaultAction?: JSX.Element; + /** Bulk actions for rows */ + bulkActions?: JSX.Element; +}; + export type TableData = { cells: TData; }[];