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;