Last active
February 7, 2023 16:24
-
-
Save rawnly/d6c3d7a99f4fb9a7fec5b43fefcb8c36 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ArrowNarrowDownIcon, ArrowNarrowUpIcon } from '@smartfish/icons'; | |
import { Column, flexRender, type Row, type Table } from '@tanstack/react-table'; | |
import clsx from 'clsx'; | |
import { useMemo } from 'react'; | |
import { forwardRef } from 'react'; | |
interface ICondensedTableProps<T> { | |
table: Table<T>; | |
onRowClick?: (row: Row<T>) => void; | |
stickyHeader?: boolean; | |
verticalLines?: boolean; | |
condensed?: boolean; | |
uppercaseHeadings?: boolean; | |
variant?: 'striped' | 'plain'; | |
emptyMessage?: string; | |
loading?: boolean; | |
stickyFooter?: boolean; | |
hasFooter?: boolean; | |
} | |
function Table<T>( | |
{ table, onRowClick, loading: isLoading = false, ...options }: ICondensedTableProps<T>, | |
ref: React.ForwardedRef<HTMLTableElement> | |
) { | |
const { | |
variant = 'striped', | |
condensed = false, | |
verticalLines = false, | |
stickyHeader = false, | |
uppercaseHeadings = false, | |
emptyMessage = 'No data', | |
stickyFooter = false, | |
hasFooter = false, | |
} = options; | |
const rows = useMemo(() => table.getRowModel().rows, [table]); | |
const headerGroups = useMemo(() => table.getHeaderGroups(), [table]); | |
const footerGroups = useMemo(() => table.getFooterGroups(), [table]); | |
const visibleFlatColumns = useMemo(() => table.getVisibleFlatColumns(), [table]); | |
return ( | |
<> | |
<table | |
ref={ref} | |
className={clsx('min-w-full', { | |
'divide-y rx-divide-neutral-6': !stickyHeader, | |
})} | |
> | |
<thead | |
className={clsx({ | |
'rx-bg-neutral-3': !stickyHeader, | |
'rx-backdrop-blur bg-neutral-3/50 dark:bg-neutralDark-3/50': stickyHeader, // style | |
'sticky top-0 z-50': stickyHeader, // positioning | |
})} | |
> | |
{headerGroups.map(group => ( | |
<tr | |
key={group.id} | |
className={clsx({ | |
'divide-x rx-divide-neutral-6': verticalLines, | |
})} | |
> | |
{group.headers.map(header => ( | |
<th | |
scope="col" | |
key={header.id} | |
onClick={header.column.getToggleSortingHandler()} | |
colSpan={header.column.columnDef.meta?.colSpan ?? header.colSpan} | |
className={clsx('whitespace-nowrap text-left text-sm font-semibold rx-text-neutral-11', { | |
'py-3.5 px-2': header.index > 0 && header.index < group.headers.length - 1 && condensed, | |
'py-3.5 px-4': header.index > 0 && header.index < group.headers.length - 1 && !condensed, | |
'sticky top-0 z-10': stickyHeader, | |
'relative hover:cursor-pointer hover:opacity-80': header.column.getCanSort(), | |
uppercase: uppercaseHeadings, | |
'py-3.5 pl-4 pr-3 sm:pl-6': header.index === 0, | |
'pr-4 pl-3 sm:pr-6': header.index === group.headers.length - 1, | |
'sticky inset-x-0 z-50 border-x rx-border-neutral-6': header.column.getIsPinned(), | |
'rx-bg-neutral-3': !stickyHeader, | |
})} | |
> | |
<div> | |
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} | |
<div | |
className={clsx('absolute top-4', { | |
'-left-4': header.index !== 0, | |
'-left-0': header.index === 0, | |
})} | |
> | |
{ | |
{ | |
desc: <ArrowNarrowUpIcon className="h-4" />, | |
asc: <ArrowNarrowDownIcon className="h-4" />, | |
}[header.column.getIsSorted().toString()] | |
} | |
</div> | |
</div> | |
</th> | |
))} | |
</tr> | |
))} | |
</thead> | |
<tbody className="divide-y overflow-y-scroll rx-divide-neutral-3"> | |
{isLoading && ( | |
<> | |
<tr | |
className={clsx('skeleton-no-border border-0', { | |
'divide-x rx-divide-neutral-6': verticalLines, | |
})} | |
> | |
{visibleFlatColumns.map(col => ( | |
<TableCell condensed={condensed} key={col.id} column={col} /> | |
))} | |
</tr> | |
<tr | |
className={clsx('skeleton-no-border border-0', { | |
'divide-x rx-divide-neutral-6': verticalLines, | |
'rx-bg-neutral-2': variant === 'striped', | |
})} | |
> | |
{visibleFlatColumns.map(col => ( | |
<TableCell condensed={condensed} key={col.id} column={col} /> | |
))} | |
</tr> | |
<tr | |
className={clsx('skeleton-no-border border-0', { | |
'divide-x rx-divide-neutral-6': verticalLines, | |
})} | |
> | |
{visibleFlatColumns.map(col => ( | |
<TableCell condensed={condensed} key={col.id} column={col} /> | |
))} | |
</tr> | |
</> | |
)} | |
{!isLoading && | |
rows.map((row, idx) => ( | |
<tr | |
key={row.id} | |
data-row-id={row.id} | |
onClick={onRowClick ? () => onRowClick(row) : row.getToggleSelectedHandler()} | |
className={clsx('transition-colors duration-150 rx-bg-neutral-1', { | |
'rx-bg-neutral-2': idx % 2 === 0 && variant === 'striped' && rows.length !== 1, | |
'rx-bg-neutral-3': row.getIsSelected(), | |
'hover:cursor-pointer hover:rx-bg-neutral-2': row.getCanSelect() || !!onRowClick, | |
'divide-x rx-divide-neutral-6': verticalLines, | |
})} | |
> | |
{row.getVisibleCells().map((cell, idx) => ( | |
<TableCell | |
key={cell.id ?? idx} | |
isFirst={idx === 0} | |
isLast={idx === row.getVisibleCells().length - 1} | |
condensed={condensed} | |
column={cell.column} | |
> | |
{(typeof cell.column.columnDef.cell === 'function' | |
? cell.column.columnDef.cell(cell.getContext()) | |
: cell.column.columnDef.cell) || <span className="font-mono tabular-nums opacity-50">--</span>} | |
</TableCell> | |
))} | |
</tr> | |
))} | |
</tbody> | |
{!isLoading && hasFooter && rows.length > 0 && ( | |
<tfoot | |
className={clsx({ | |
'rx-bg-neutral-3': !stickyFooter, | |
'rx-backdrop-blur bg-neutral-3/50 dark:bg-neutralDark-3/50': stickyFooter, // style | |
'sticky bottom-0 z-50': stickyFooter, // positioning | |
})} | |
> | |
{footerGroups.map(group => ( | |
<tr key={group.id}> | |
{group.headers.map((header, idx) => { | |
const value = header.isPlaceholder | |
? '' | |
: flexRender(header.column.columnDef.footer, header.getContext()); | |
return ( | |
<TableCell | |
column={header.column} | |
isFirst={idx === 0} | |
isLast={group.headers.length - 1 === idx} | |
condensed={condensed} | |
key={header.id} | |
> | |
{value ? <>{value}</> : <span className="font-mono tabular-nums opacity-50">--</span>} | |
</TableCell> | |
); | |
})} | |
</tr> | |
))} | |
</tfoot> | |
)} | |
</table> | |
{rows.length === 0 && !isLoading && ( | |
<div data-row-id={'empty'} key="empty-row" className="flex w-full items-center justify-center p-4 text-center"> | |
<p className="opacity-50">{emptyMessage}</p> | |
</div> | |
)} | |
</> | |
); | |
} | |
const TableCell = ({ | |
isLast, | |
isFirst, | |
condensed = false, | |
children, | |
column, | |
}: React.PropsWithChildren<{ | |
condensed?: boolean; | |
column: Column<any, unknown>; | |
isLast?: boolean; | |
isFirst?: boolean; | |
}>) => { | |
return ( | |
<td | |
className={clsx('whitespace-nowrap text-sm rx-text-neutral-11', { | |
// padding condensed | |
'px-2 py-3': !isLast && !isFirst && condensed, | |
// padding regular | |
'py-3.5 px-4': !isLast && !isFirst && !condensed, | |
// padding regular first & last | |
// 'py-3.5 px-4': (isFirst || isLast) && !condensed, | |
'px-4': (isLast || isFirst) && condensed, | |
// sempre | |
'sm:pl-6': isFirst, | |
'sm:pr-6': isLast, | |
'font-mono tabular-nums': column.columnDef.meta?.dataType === 'accounting', | |
'sticky left-0 border-r rx-bg-neutral-3 rx-border-neutral-6': column.getIsPinned(), | |
})} | |
> | |
{children ?? 'n.d'} | |
</td> | |
); | |
}; | |
export default forwardRef(Table); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment