feat(FE-284): Refactor table component support for nesting header

This commit is contained in:
randy-ar
2025-12-09 17:57:46 +07:00
parent 3569955e7f
commit f9dfe7b27f
5 changed files with 1446 additions and 351 deletions
+103 -88
View File
@@ -14,6 +14,7 @@ import {
SortingState,
OnChangeFn,
Row,
HeaderContext,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react';
@@ -57,8 +58,6 @@ export interface TableProps<TData extends object> {
setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
renderFooter?: boolean;
footerContent?: ReactNode;
footerData?: TData[];
withCheckbox?: boolean;
rowOptions?: number[];
}
@@ -73,22 +72,22 @@ const emptyContentDefaultValue = (
</div>
);
const TABLE_DEFAULT_STYLING = {
export const TABLE_DEFAULT_STYLING = {
containerClassName: 'w-full mb-20',
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: 'px-4 py-3 text-base-content/50',
headerColumnClassName:
'px-4 py-3 border-base-content/10 text-base-content/50',
tableBodyClassName: '',
bodyRowClassName: 'border-t border-t-base-content/10',
bodyRowClassName: 'border-t border-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content',
paginationClassName: '',
tableFooterClassName: '',
footerRowClassName: '',
footerColumnClassName: '',
tableFooterClassName: 'font-semibold border-base-content/10',
footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
};
const Table = <TData extends object>({
@@ -111,8 +110,6 @@ const Table = <TData extends object>({
setRowSelection,
enableRowSelection,
renderFooter = false,
footerContent,
footerData = [],
withCheckbox = false,
rowOptions = [10, 20, 50, 100],
}: TableProps<TData>) => {
@@ -187,14 +184,6 @@ const Table = <TData extends object>({
const table = useReactTable(tableOptions);
const { setPageSize } = table;
const footerTableOptions: TableOptions<TData> = {
columns,
data: footerData,
getCoreRowModel: getCoreRowModel(),
};
const footerTable = useReactTable(footerTableOptions);
const prevPageClickHandler = () => {
table.previousPage();
@@ -235,58 +224,82 @@ const Table = <TData extends object>({
key={headerGroup.id}
className={tableClassNames.headerRowClassName}
>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
{
'first:w-9 first:pr-0': withCheckbox,
},
tableClassNames.headerColumnClassName
)}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
{headerGroup.headers.map((header) => {
const columnRelativeDepth =
header.depth - header.column.depth;
if (
!header.isPlaceholder &&
columnRelativeDepth > 1 &&
header.id === header.column.id
) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = leafs[leafs.length - 1].depth - header.depth;
}
return (
<th
key={header.id}
colSpan={header.colSpan}
rowSpan={rowSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
{
'first:w-9 first:pr-0': withCheckbox,
},
{
'border-b': header.colSpan > 1,
},
tableClassNames.headerColumnClassName
)}
>
<div
className={cn('flex items-center gap-1 min-h-full', {
'justify-center': header.colSpan > 1,
})}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<div className='w-4 h-4 relative flex flex-col items-center'>
<Icon
icon='heroicons:chevron-up-16-solid'
width={18}
height={18}
className={cn(
'absolute -top-1',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='heroicons:chevron-down-16-solid'
width={18}
height={18}
className={cn(
'absolute -bottom-1.5',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
))}
{header.column.getCanSort() && (
<div className='w-4 h-4 relative flex flex-col items-center'>
<Icon
icon='heroicons:chevron-up-16-solid'
width={18}
height={18}
className={cn(
'absolute -top-1',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='heroicons:chevron-down-16-solid'
width={18}
height={18}
className={cn(
'absolute -bottom-1.5',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
);
})}
</tr>
))}
</thead>
@@ -311,25 +324,27 @@ const Table = <TData extends object>({
</tr>
))}
</tbody>
<tfoot className={cn(className.tableFooterClassName)}>
{renderFooter &&
(footerData && footerData.length > 0
? footerTable.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.footerRowClassName}>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className={className.footerColumnClassName}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))
: footerContent)}
<tfoot className={cn(tableClassNames.tableFooterClassName)}>
{renderFooter && (
<tr className={cn(tableClassNames.footerRowClassName)}>
{table.getAllLeafColumns().map((column) => (
<td
key={column.id}
className={cn(
{ 'first:w-9 first:pr-0': withCheckbox },
tableClassNames.footerColumnClassName
)}
>
{column.columnDef.footer &&
flexRender(column.columnDef.footer, {
column,
header: column.columnDef,
table,
} as HeaderContext<TData, unknown>)}
</td>
))}
</tr>
)}
</tfoot>
</table>
</div>