Using a Tooltip component within Top Sticky thead > th's and Left Sticky tbody> td's

609 Views Asked by At

I have a table using @tanstack/react-table and styled with ShadCN Table Component with a few adjustments for the sticky functionality and ShadCN Tooltip Component

I have all of the Thead (th) components within a TableHeader(thead) sticky to the top. So when a user scrolls a table vertically, the Thead(th) sticks to the top.

I have the first columns TableCell's (td) sticky to the left. So when a user scrolls the table horizontally, that column's data sticks to the left.

The problem is, that to make this work, I need to add a z-index's across various elements, so Tooltips either get stuck behind Thead's or other TableCell's. So, not really sure how to have both the sticky Thead & TableCell along with Tooltips's

Here is a StackBlitz Environment to recreate the issue if you want to play around with it?

Screenshot 2023-12-09 213945 Screenshot 2023-12-09 213911

Tanstack Column

const Header: React.FC<{ title: string }> = ({ title }) => (
  <div className="font-bold min-w-[300px] bg-slate-300 h-16 flex justify-center items-center">
    {title}
  </div>
);

//////
{
  accessorFn: (row) => `${row.firstName} ${row.lastName}`,
  id: 'fullName',
  header: () => <Header title="Name" />,
  cell: (info) => {
    return (
      <div className="font-bold min-w-[300px]">
        <TooltipProvider>
          <Tooltip defaultOpen={info.row.index === 0}>
            <TooltipTrigger asChild>
              <span>{info.getValue()}</span>
            </TooltipTrigger>
            <TooltipContent className="max-w-[200px] w-full min-w-[150px]">
              <p>This</p>
              <p>Is</p>
              <p>A</p>
              <p>Tooltip</p>
              <p>Test</p>
            </TooltipContent>
          </Tooltip>
        </TooltipProvider>
      </div>
    );
  },
  footer: (props) => props.column.id,
  meta: {
    sticky: true,
    stickyLeft: true,
  },
},

table.tsx

import { cn } from '@/lib/utils';
import { type ColumnMeta } from '@tanstack/react-table';
import * as React from 'react';

export interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
  tableClassName?: string;
}

const Table = React.forwardRef<HTMLTableElement, TableProps>(({ className, tableClassName, ...props }, ref) => (
  <div className={cn('w-full overflow-x-auto data-table-container border rounded-md', className)}>
    <table ref={ref} className={cn('w-full caption-bottom text-sm relative data-table', tableClassName)} {...props} />
  </div>
));
Table.displayName = 'Table';

const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
  <thead ref={ref} className={cn(className)} {...props} />
));
TableHeader.displayName = 'TableHeader';

const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
  <tbody ref={ref} className={cn(className)} {...props} />
));
TableBody.displayName = 'TableBody';

const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
  <tfoot ref={ref} className={cn('bg-primary font-medium text-primary-foreground', className)} {...props} />
));
TableFooter.displayName = 'TableFooter';

export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
  isLoading?: boolean;
  noHover?: boolean;
}

const TableRow = React.forwardRef<HTMLTableRowElement, TableRowProps>(({ className, isLoading, noHover, ...props }, ref) => (
  <tr
    ref={ref}
    className={cn('transition-colors', !isLoading && '[&>*]:data-[state=selected]:bg-muted', !noHover && !isLoading && '[&>*]:hover:bg-muted', className)}
    {...props}
  />
));
TableRow.displayName = 'TableRow';

export interface TableHeadProps<TData, TValue> extends React.ThHTMLAttributes<HTMLTableCellElement>, ColumnMeta<TData, TValue> {
  isLoading?: boolean;
  last?: boolean;
}

const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps<any, any>>(
  ({ className, sticky, stickyLeft, stickyRight, isLoading, last, ...props }, ref) => {
    return (
      <th
        ref={ref}
        className={cn(
          'h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] relative',
          sticky && 'sticky -top-[1px] z-[7] bg-background',
          stickyLeft && 'sticky -left-[1px] z-[9] bg-background',
          stickyRight && 'sticky -right-[1px] z-[9] bg-background',
          stickyRight && last && 'z-[8]',
          isLoading && 'min-w-[100px]',
          className,
        )}
        {...props}
      />
    );
  },
);
TableHead.displayName = 'TableHead';

export interface TableCellProps<TData, TValue> extends React.TdHTMLAttributes<HTMLTableCellElement>, ColumnMeta<TData, TValue> {}

const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps<any, any>>(({ className, sticky, stickyLeft, stickyRight, ...props }, ref) => (
  <td
    ref={ref}
    className={cn(
      'table-cell px-4 py-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] relative',
      sticky && 'sticky -right-[1px] z-[5] bg-background',
      stickyLeft && 'sticky -left-[1px] z-[6] bg-background',
      stickyRight && 'sticky -right-[1px] z-[5] bg-background',
      className,
    )}
    {...props}
  />
));
TableCell.displayName = 'TableCell';

const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
  <caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

Any help would be greatly appreciated!

2

There are 2 best solutions below

0
On BEST ANSWER

In case anyone comes across a similar issue, while using shadcn-ui/radix-ui/react-tooltip.

You can wrap a the tooltip Content in a Portal

import * as TooltipPrimitive from '@radix-ui/react-tooltip';

<TooltipPrimitive.Portal>
  <TooltipPrimitive.Content
    ref={ref}
    sideOffset={sideOffset}
    {...props}
  />
</TooltipPrimitive.Portal>

StackBlitz Fork

1
On

You could look at increasing the z-index of the cell when it is hovered, since this is presumably when the tooltip shows:

<TableCell
  …
  hasTooltip={cell.getContext().column.columnDef.id === 'fullName'}
>
const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps<any, any>>(({ …, hasTooltip, … }, ref) => (
  <td
    ref={ref}
    className={cn(
      …
      hasTooltip && 'hover:z-10',
      …
    )}
    {...props}
  />
));

Though you will still get some janky behavior such as when the cell being hovered is partially behind a different cell that should be on top:

enter image description here

See a Stackblitz fork of this solution

Otherwise, you could consider using Portals to render the tooltip element outside the table such that it would be easier to organize elements in the z-stack, though I'm not sure how compatible the Shadcn/Radix Tooltip component would be with this approach.