Open1
table
export function TanstackTableVirtualizedWindowSticky() {
}
interface TanStackTableContainerProps {
// table: Table<Person>;
table: Table<any>;
}
function TanStackTableContainer({ table }: TanStackTableContainerProps) {
const tableContainerRef = React.useRef<HTMLDivElement>(null);
const scrollableColumns = table.getCenterLeafColumns();
const columnVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableCellElement>({
count: scrollableColumns.length,
estimateSize: (index) => scrollableColumns[index].getSize(),
getScrollElement: () => tableContainerRef.current,
horizontal: true,
overscan: 3,
});
const virtualColumns = columnVirtualizer.getVirtualItems();
let virtualPaddingLeft: number | undefined;
let virtualPaddingRight: number | undefined;
if (columnVirtualizer && virtualColumns?.length) {
virtualPaddingLeft = virtualColumns[0]?.start ?? 0;
virtualPaddingRight = columnVirtualizer.getTotalSize() - (virtualColumns[virtualColumns.length - 1]?.end ?? 0);
}
useEffect(() => {
columnVirtualizer.measure();
}, [table.getState(), columnVirtualizer.measure]);
/**
* Instead of calling `column.getSize()` on every render for every header
* and especially every data cell (very expensive),
* we will calculate all column sizes at once at the root table level in a useMemo
* and pass the column sizes down as CSS variables to the <table> element.
*/
const columnSizeVars = React.useMemo(() => {
const headers = table.getFlatHeaders();
const colSizes: { [key: string]: number } = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!;
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
return (
<div
className="container border-2"
ref={tableContainerRef}
style={{
overflow: 'auto',
position: 'relative',
// height: '300px',
maxHeight: '100vh',
scrollBehavior: 'smooth', // Memo: カラムリサイズ時の行のスクロール位置がリセットされる挙動を防ぐ
}}
>
<TableRoot
style={{
display: 'grid',
...columnSizeVars, //Define column sizes on the <table> element
width: table.getTotalSize(),
}}
>
<TanStackTableHead
columnVirtualizer={columnVirtualizer}
table={table}
virtualPaddingLeft={virtualPaddingLeft}
virtualPaddingRight={virtualPaddingRight}
/>
{/* When resizing any column we will render this special memoized version of our table body */}
{table.getState().columnSizingInfo.isResizingColumn ? (
<TanStackTableBodyMemo
columnVirtualizer={columnVirtualizer}
table={table}
tableContainerRef={tableContainerRef}
virtualPaddingLeft={virtualPaddingLeft}
virtualPaddingRight={virtualPaddingRight}
/>
) : (
<TanStackTableBody
columnVirtualizer={columnVirtualizer}
table={table}
tableContainerRef={tableContainerRef}
virtualPaddingLeft={virtualPaddingLeft}
virtualPaddingRight={virtualPaddingRight}
/>
)}
</TableRoot>
</div>
);
}
interface TanStackTableHeadProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
table: Table<any>;
virtualPaddingLeft: number | undefined;
virtualPaddingRight: number | undefined;
}
function TanStackTableHead({
columnVirtualizer,
table,
virtualPaddingLeft,
virtualPaddingRight,
}: TanStackTableHeadProps) {
return (
<TableHeader
style={{
display: 'grid',
position: 'sticky',
top: 0,
zIndex: 1,
}}
>
{table.getHeaderGroups().map((headerGroup) => (
<TanStackTableHeadRow
columnVirtualizer={columnVirtualizer}
headerGroup={headerGroup}
key={headerGroup.id}
virtualPaddingLeft={virtualPaddingLeft}
virtualPaddingRight={virtualPaddingRight}
table={table}
/>
))}
</TableHeader>
);
}
interface TanStackTableHeadRowProps<TData> {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
headerGroup: HeaderGroup<any>;
virtualPaddingLeft: number | undefined;
virtualPaddingRight: number | undefined;
table: Table<TData>;
}
function TanStackTableHeadRow<TData>({
columnVirtualizer,
headerGroup,
virtualPaddingLeft,
virtualPaddingRight,
table,
}: TanStackTableHeadRowProps<TData>) {
const virtualColumns = columnVirtualizer.getVirtualItems();
const { scrollableHeaders, stickyLeftHeaders, stickyRightHeaders } = useMemo(
() => ({
scrollableHeaders: table.getCenterLeafHeaders(),
stickyLeftHeaders: table.getLeftLeafHeaders(),
stickyRightHeaders: table.getRightLeafHeaders(),
}),
[table.getState()]
);
return (
<TableRow key={headerGroup.id} style={{ display: 'flex', width: '100%' }}>
{stickyLeftHeaders.map((header: Header<any, unknown>) => {
return <TanStackTableHeadCell key={header.id} header={header} />;
})}
{virtualPaddingLeft ? (
//fake empty column to the left for virtualization scroll padding
<TableHead style={{ display: 'flex', width: virtualPaddingLeft }} />
) : null}
{virtualColumns.map((virtualColumn) => {
const header = scrollableHeaders[virtualColumn.index];
return <TanStackTableHeadCell key={header.id} header={header} />;
})}
{virtualPaddingRight ? (
//fake empty column to the right for virtualization scroll padding
<TableHead style={{ display: 'flex', width: virtualPaddingRight }} />
) : null}
{stickyRightHeaders.map((header: Header<any, unknown>) => {
return <TanStackTableHeadCell key={header.id} header={header} />;
})}
</TableRow>
);
}
interface TanStackTableHeadCellProps {
header: Header<any, unknown>;
}
function TanStackTableHeadCell({ header }: TanStackTableHeadCellProps) {
return (
<TableHead
key={header.id}
colSpan={header.colSpan} //needed for nested headers
className="relative bg-gray-200/100 dark:bg-gray-900/100 "
style={{
display: 'flex',
width: `calc(var(--header-${header?.id}-size) * 1px)`,
...getCommonPinningStyles(header.column),
}}
>
<div className="w-full">{flexRender(header.column.columnDef.header, header.getContext())}</div>
<div
className="absolute right-0 top-0 h-full w-1 bg-blue-300 select-none touch-none hover:bg-blue-500 cursor-col-resize"
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
/>
</TableHead>
);
}
interface TanStackTableBodyProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
table: Table<any>;
tableContainerRef: React.RefObject<HTMLDivElement>;
virtualPaddingLeft: number | undefined;
virtualPaddingRight: number | undefined;
}
function TanStackTableBody({
columnVirtualizer,
table,
tableContainerRef,
virtualPaddingLeft,
virtualPaddingRight,
}: TanStackTableBodyProps) {
const ROW_HEIGHT = 37; // Set a fixed height for each row
const { scrollableRows, topRows, bottomRows } = useMemo(
() => ({
scrollableRows: table.getCenterRows(),
topRows: table.getTopRows(),
bottomRows: table.getBottomRows(),
}),
[table.getState()]
);
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: scrollableRows.length,
estimateSize: () => ROW_HEIGHT,
getScrollElement: () => tableContainerRef.current,
measureElement:
typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1
? (element) => element?.getBoundingClientRect().height
: undefined,
overscan: 5,
});
const virtualRows = rowVirtualizer.getVirtualItems();
return (
<>
{topRows.length > 0 && (
<TableBody className="grid sticky z-10 top-[41px] border-b shadow-md">
{topRows.map((row) => {
return (
<StickyRow
key={row.id}
row={row}
table={table}
rowHeight={ROW_HEIGHT}
columnVirtualizer={columnVirtualizer}
/>
);
})}
</TableBody>
)}
<TableBody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
position: 'relative',
}}
>
{virtualRows.map((virtualRow) => {
const row = scrollableRows[virtualRow.index] as Row<any>;
return (
<TanStackTableBodyRow
columnVirtualizer={columnVirtualizer}
key={row.id}
row={row}
rowVirtualizer={rowVirtualizer}
virtualPaddingLeft={virtualPaddingLeft}
virtualPaddingRight={virtualPaddingRight}
virtualRow={virtualRow}
/>
);
})}
</TableBody>
{bottomRows.length > 0 && (
<TableBody className="grid sticky z-10 bottom-0 border-t-2 ">
{bottomRows.map((row) => {
return (
<StickyRow
key={row.id}
row={row}
table={table}
rowHeight={ROW_HEIGHT}
columnVirtualizer={columnVirtualizer}
/>
);
})}
</TableBody>
)}
</>
);
}
//special memoized wrapper for our table body that we will use during column resizing
const TanStackTableBodyMemo = React.memo(
TanStackTableBody,
(prev, next) => prev.table.options.data === next.table.options.data
) as typeof TanStackTableBody;
function StickyRow({
row,
table,
rowHeight,
columnVirtualizer,
}: {
row: Row<any>;
table: Table<any>;
rowHeight: number;
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
}) {
return (
<TableRow
className="group/sticky-row"
style={{
position: 'sticky',
top: row.getIsPinned() === 'top' ? `${row.getPinnedIndex() * rowHeight + 41}px` : undefined,
bottom:
row.getIsPinned() === 'bottom'
? `${(table.getBottomRows().length - 1 - row.getPinnedIndex()) * rowHeight}px`
: undefined,
display: 'flex',
height: `${rowHeight}px`,
}}
>
{row.getVisibleCells().map((cell) => {
return <TanStackTableBodyCellMemo key={cell.id} cell={cell} row={row} columnVirtualizer={columnVirtualizer} />;
})}
</TableRow>
);
}
interface TanStackTableBodyRowProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
row: Row<any>;
rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>;
virtualPaddingLeft: number | undefined;
virtualPaddingRight: number | undefined;
virtualRow: VirtualItem;
}
function TanStackTableBodyRow({
columnVirtualizer,
row,
rowVirtualizer,
virtualPaddingLeft,
virtualPaddingRight,
virtualRow,
}: TanStackTableBodyRowProps) {
const virtualColumns = columnVirtualizer.getVirtualItems();
const { scrollableCells, stickyLeftCells, stickyRightCells } = useMemo(
() => ({
scrollableCells: row.getCenterVisibleCells(),
stickyLeftCells: row.getLeftVisibleCells(),
stickyRightCells: row.getRightVisibleCells(),
}),
[row.getRightVisibleCells(), row.getLeftVisibleCells(), row.getCenterVisibleCells()]
);
const rowStyle = useMemo(
() => ({
display: 'flex',
position: 'absolute' as const,
transform: `translateY(${virtualRow.start}px)`,
width: '100%',
}),
[virtualRow.start]
);
return (
<TableRow
data-index={virtualRow.index}
ref={(node) => rowVirtualizer.measureElement(node)}
key={row.id}
className="group/scrollable-row"
style={rowStyle}
>
{stickyLeftCells.map((cell: Cell<any, unknown>) => {
return <TanStackTableBodyCellMemo key={cell.id} cell={cell} row={row} columnVirtualizer={columnVirtualizer} />;
})}
{virtualPaddingLeft ? (
//fake empty column to the left for virtualization scroll padding
<TableCell style={{ display: 'flex', width: virtualPaddingLeft }} />
) : null}
{virtualColumns.map((vc) => {
const cell = scrollableCells[vc.index];
return <TanStackTableBodyCellMemo key={cell.id} cell={cell} row={row} columnVirtualizer={columnVirtualizer} />;
})}
{virtualPaddingRight ? (
//fake empty column to the right for virtualization scroll padding
<TableCell style={{ display: 'flex', width: virtualPaddingRight }} />
) : null}
{stickyRightCells.map((cell: Cell<any, unknown>) => {
return <TanStackTableBodyCellMemo key={cell.id} cell={cell} row={row} columnVirtualizer={columnVirtualizer} />;
})}
</TableRow>
);
}
// test out when rows don't re-render at all (future TanStack Virtual release can make this unnecessary)
// const TanStackTableBodyRowMemo = React.memo(TanStackTableBodyRow, (_prev, next) => {
// // return next.rowVirtualizer.isScrolling;
// // return _prev.row === next.row;
// return (
// next.rowVirtualizer.isScrolling || next.columnVirtualizer.isScrolling === false // 横スクロール時は再描画する
// );
// }) as typeof TanStackTableBodyRow;
interface TanStackTableBodyCellProps {
className?: string;
cell: Cell<any, unknown>;
row: Row<any>;
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>;
}
function TanStackTableBodyCell({ cell, row }: TanStackTableBodyCellProps) {
const bgColor = row.getIsPinned() ? 'bg-slate-50 dark:bg-gray-900' : 'bg-white/100 dark:bg-gray-950/100';
return (
<TableCell
key={cell.id}
className={cn('group-hover/sticky-row:bg-muted/100 group-hover/scrollable-row:bg-muted/100', bgColor)}
style={{
display: 'flex',
...getCommonPinningStyles(cell.column),
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
}}
>
{cell.getIsGrouped() ? (
// If it's a grouped cell, add an expander and row count
<>
<button
className="flex"
{...{
onClick: row.getToggleExpandedHandler(),
style: {
cursor: row.getCanExpand() ? 'pointer' : 'normal',
},
}}
>
{row.getIsExpanded() ? '👇' : '👉'} {flexRender(cell.column.columnDef.cell, cell.getContext())} (
{row.subRows.length})
</button>
</>
) : cell.getIsAggregated() ? (
// If the cell is aggregated, use the Aggregated
// renderer for cell
flexRender(cell.column.columnDef.aggregatedCell ?? cell.column.columnDef.cell, cell.getContext())
) : cell.getIsPlaceholder() ? null : ( // For cells with repeated values, render null
// Otherwise, just render the regular cell
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
{/* {flexRender(cell.column.columnDef.cell, cell.getContext())} */}
</TableCell>
);
}
export const TanStackTableBodyCellMemo = React.memo(
TanStackTableBodyCell,
// (prev, next) => next.cell === prev.cell
(_prev, next) => {
return next.columnVirtualizer.isScrolling;
}
) as typeof TanStackTableBodyCell;