React-window: Fixed grid headers with CSS position sticky

Created on 8 Mar 2019  路  14Comments  路  Source: bvaughn/react-window

Hi @bvaughn,

I'm sure you're really tired of this request, but per your comment here I think I might have a lightweight-enough solution for you to consider.

This solution keeps browser-native scrolling, doesn't require any ScrollSync-esque behavior, and only requires a few changes to the createGridComponent render method. However, IE11 is not supported. I think this is OK as long as it's explicitly called out in the documentation.

You can see a demo here: https://codesandbox.io/s/xjzrzmxv8q

This is what I have locally as a prototype - if you're interested in a PR for this, there's definitely some more work to be done as several things are hardcoded. I can do the work here, but would appreciate some guidance.

I totally understand if you don't want to add this - in the worst case, this issue can at least document the approach for others.

An overview of my changes:

First, when generating items, we want to skip row == 0 and column == 0 (the Math.max() calls). We also push into leftStickyItems and topStickyItems for the visible range. Those are the only changes here.

const items = [];
const topStickyItems = [];
const leftStickyItems = [];

if (columnCount > 0 && rowCount) {
  for (
    let columnIndex = Math.max(1, columnStartIndex); // Skip column 0
    columnIndex <= columnStopIndex;
    columnIndex++
  ) {
    topStickyItems.push(
      createElement(children, {
        columnIndex,
        data: itemData,
        isScrolling: useIsScrolling ? isScrolling : undefined,
        key: itemKey({ columnIndex, data: itemData, rowIndex: 0 }),
        rowIndex: 0,
        style: this._getItemStyle(0, columnIndex),
      })
    );
  }

  for (
    let rowIndex = Math.max(1, rowStartIndex); // Skip row 0
    rowIndex <= rowStopIndex;
    rowIndex++
  ) {
    // I'm leveraging this already existing loop, but this is probably
    // better as its own loop for clarity.
    leftStickyItems.push(
      createElement(children, {
        columnIndex: 0,
        data: itemData,
        isScrolling: useIsScrolling ? isScrolling : undefined,
        key: itemKey({ columnIndex: 0, data: itemData, rowIndex }),
        rowIndex,
        style: this._getItemStyle(rowIndex, 0),
      })
    );

    for (
      let columnIndex = Math.max(1, columnStartIndex); // Skip column 0
      columnIndex <= columnStopIndex;
      columnIndex++
    ) {
      items.push(
        createElement(children, {
          columnIndex,
          data: itemData,
          isScrolling: useIsScrolling ? isScrolling : undefined,
          key: itemKey({ columnIndex, data: itemData, rowIndex }),
          rowIndex,
          style: this._getItemStyle(rowIndex, columnIndex),
        })
      );
    }
  }
}

Once we've generated those items, before the return call, we add the sticky containers to the items array.

const topLeftStyle = this._getItemStyle(0, 0);

items.unshift(
  createElement('div', {
    children: leftStickyItems,
    key: 'left-sticky',
    className: 'left-sticky',
    style: {
      height: estimatedTotalHeight,
      width: topLeftStyle.width,
      position: 'sticky',
      left: 0,
      zIndex: 1,
      transform: `translateY(-${topLeftStyle.height}px)`,
    },
  })
);

items.unshift(
  createElement('div', {
    children: topStickyItems,
    key: 'top-sticky',
    className: 'top-sticky',
    style: {
      height: topLeftStyle.height,
      width: estimatedTotalWidth,
      position: 'sticky',
      top: 0,
      zIndex: 1,
    },
  })
);

There's one unsolved part - the (0, 0) position. I've gotten around this by rendering it outside of the grid (the grey box in the top left). This is fine for my use case but would merit a better solution if you want a PR for this.

Most helpful comment

@bvaughn First of all man I would like to thank you for this awesome lib. Provided abstraction level allows to resolve almost whatever you want.
Now back to the topic. My production case is pretty complicated: multiple grouping against rows and columns which should be sticky. So I tried to simplify it as it was possible and made this sandbox based on sticky list implementation: https://codesandbox.io/s/react-window-sticky-grid-liwsd.

All 14 comments

You're welcome to submit a PR if you're interested. I am willing to take a look and give it consideration!

I'll warn up front that I'm reluctant to add complexity or scope to grid or list for a feature that I don't think is either essentially for accessibility _or_ needed by a significant percentage of users.

Perhaps it's something we could manage with an add-on or a separate entry point though?

Okay. Here's what I think!

I don't plan on taking any personal action on this issue, so I'm going to close it. (I do this to help myself manage the projects I maintain.)

I would be happy to look at a PR and discuss pros and cons for this, given what I said above. So let's talk more if you're interested in that!

@apazzolini I just wanted to weigh in here and say that I've been playing with your branch all day and it works really well! My particular use case has to render a 5000-cell (100 rows x 50 columns) table and my first attempt with scroll-syncing was okay, but noticeably laggy. This native CSS solution is really smooth.

I understand this probably wouldn't (and shouldn't) be a core library feature, but hopefully it'll find more exposure for other devs who need stick/frozen columns and rows.

Thanks for the great work, @apazzolini!

@MarkLeMerise glad it's working for you! FYI I'm still planning on submitting a PR for this, but we had to delay migrating from react-virtualized to this strategy for another month-ish, so it'll be a bit.

demo in safari was not working. https://codesandbox.io/s/xjzrzmxv8q
needed to add:

position: -webkit-sticky;

Thank you for this !

Thanks! I'll update the FAQ sandbox

Not sure if this is the right place to "+1" (I don't want to be a part of +1 spamming), but stickiness (or a mechanism for manipulating the children's rendering in general) would be helpful (critical?) for my use case. Right now I'm having to do some slightly gross things to ensure virtualized, frozen rows and columns. I do appreciate that features like this can raise some hackles for maintenance, though, and having used cellRangeRenderer in the past, I know it can get really complex very quickly.

Didn't see a PR for this yet, so it seemed like my comment would best go here. Maybe I'm in a minority, but given that I'm using react-window to display a massive dataset, a sticky header row is a pretty key piece of the user experience for large grids.

That said, I'm a big fan of how this lib reduces footprint and ups speed, so if you'd rather not build the solution in directly, I see where you're coming from. Thank you @apazzolini for putting together the Code Sandbox as an example!

@bvaughn Do you think this approach is clean enough to link in the main README? The List version is useful, but it wasn't immediately obvious to me how I'd adapt it to the Grid -- surfacing this CodeSandbox would be nice.

Update: I just realized that the CodeSandbox requires this forked version, my bad! I'll close the PR in the meantime.

Hey - you're right, the demo requires a fork to always render the sticky header and left column, regardless of the scroll position. I think a better approach for a PR here would be to allow customizing some ancillary ranges to render in addition to the cells based on scroll position.

That said, I ended up writing my own virtualization component to directly support this and some other custom logic I needed, so it's unlikely I'll get around to submitting anything here. Sorry about that.

Hey - you're right, the demo requires a fork to always render the sticky header and left column, regardless of the scroll position. I think a better approach for a PR here would be to allow customizing some ancillary ranges to render in addition to the cells based on scroll position.

That said, I ended up writing my own virtualization component to directly support this and some other custom logic I needed, so it's unlikely I'll get around to submitting anything here. Sorry about that.

I'm currently in the works of trying to display large datasets with multiple columns, and struggle a bit to solve both sticky left column and sticky headers. Could you provide a link to how you implemented your virtualization component?

@bvaughn First of all man I would like to thank you for this awesome lib. Provided abstraction level allows to resolve almost whatever you want.
Now back to the topic. My production case is pretty complicated: multiple grouping against rows and columns which should be sticky. So I tried to simplify it as it was possible and made this sandbox based on sticky list implementation: https://codesandbox.io/s/react-window-sticky-grid-liwsd.

Looks nice in my Android+Chrome 馃憤

@astorchous Thank you for your sticky header/column grid example.

I expanded on the example by @astorchous to work with VariableSizeGrid. My implementation can be found here: https://codesandbox.io/s/sticky-variable-grid-example-0cnwb.

Sorry, my example doesn't use jsx. Also for some reason, the grid doesn't always display right away. You may need to reload the output window to see it.

Was this page helpful?
0 / 5 - 0 ratings