Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sticky header in a useReactTable table with useVirtualizer disappears when scrolling #640

Open
2 tasks done
wjthieme opened this issue Dec 20, 2023 · 5 comments
Open
2 tasks done

Comments

@wjthieme
Copy link

Describe the bug

When using a react-table together with vritualizer and a sticky header the sticky header disappears when scrolling. This is due to the fact that when combining the two the table should be wrapped in two divs:

  1. The container that takes the height that should be taken up. This is the div that is scrollable
  2. The div that directly wraps the container and takes the height of all the virtual items combined

Since the table element at any given time only contains the visible rows (plus overscan) the table itself has a height smaller than the wrapper div (nr. 2). This causes the sticky header to disappear when you scroll down since the sticky header cannot escape the table element.

Your minimal, reproducible example

https://codesandbox.io/p/devbox/tanstack-react-virtual-example-dynamic-mr8t3x

Steps to reproduce

  1. Add position: sticky to the thead element
  2. Scroll down the table and watch how the header dissapears

Expected behavior

The header should stay on top since it is sticky.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

Chrome

tanstack-virtual version

3.0.0-beta.68

TypeScript version

5.3.2

Additional context

I've tried getting rid of the wrapper div (nr. 2) and setting the height: ${getTotalSize()}px` directly on the table element but this causes the rows' height the grow because there are only ever enough rows to fit on the screen (plus overscan) but having the table the full height causes the rows to evenly divide the space between each other (making them a lot larger).

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
@riptusk331
Copy link

I found a way to fix this, and am linking a working example. The trick is to calculate the height difference between the table element and the scrollable div, and then append an invisible pseudo-element to the table with that calculated height. This results in the table having the same height as the scrollable div, and stops the header from vanishing since you'll no longer scroll past the "end" of table element.

Doing this is a little bit tricky since pseudo elements can't be controlled directly through React/JSX inline styling, and directly adding new styles to the DOM is super expensive and forces the browser to recalculate the layout (which will make scrolling janky). The best way I found to do this was with with CSS variables, which the browser is very efficient at handling and it doesn't force a full layout recalc. I added a virtual-table::after CSS class with a variable height, and made sure to class my <table> element with virtual-table. Then just set that height from your handlers in your React table component.

Also, since the number of table rows (<tr>) being rendered to the DOM by the virtualizer at any given time varies (and therefore the height of the table + your originally set pseudo element varies), you need to pay attention to this as you near the bottom of your scrollable area as this could result in a large divergence (meaning you'll over-scroll past the table, or the pseudo element will be too short and the header will disappear again). The way I did this was set up an event listener for the scroll that flags when we're near the bottom 5% of the list, and triggers the pseudo element height recalculation.

Sticky Table Header Example

I'm sure this can be improved and there are further efficiencies, but it works buttery smooth for me.

As for the "issue" itself, I would argue that this isn't really a react-virtual issue. I don't think there's anything the library could really do for you. This is just a quirk of virtualizing that you need to account for. Virtualizing tables is kind of a bastardization of things. That said, it would be good to include this as an example in the existing docs, as none of this is immediately obvious.

@ryanjoycemacq
Copy link

ryanjoycemacq commented Dec 30, 2023

@wjthieme i think the example you linked is just the regular virtual table example...there's nothing with sticky headers in it.

@wjthieme
Copy link
Author

wjthieme commented Jan 1, 2024

@ryanjoycemacq if you add position: sticky to the <thead> element in the example you can reproduce this.

@riptusk331 I'll check if this works!

@aronedwards
Copy link

aronedwards commented Jan 10, 2024

TEMP Workaround - issues see post below
Cleaner "hack/solution", but not super smooth
can use parentRef (ref of scrolling component) to generate your own code version of sticky
less performant, but more maintainable until bug solved

<TableRow
    key={headerGroup.id}
    className="relative z-20"
    style={{
      transform: `translateY(${
        parentRef.current?.scrollTop || 0
      }px)`,
    }} >
           

@piecyk
Copy link
Collaborator

piecyk commented Jan 10, 2024

There are few issues here, overfall tables don't quite like when rows are positions absolute, one option is to change the positioning strategy by adding before/after row that will move items when scrolling

https://codesandbox.io/p/devbox/zealous-firefly-8vm5y2?file=%2Fsrc%2Fmain.tsx%3A91%2C19

btw there is no clear answer what positioning strategy is better, it's really hard to compare performance in a significant way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants