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

Fixed grid headers with CSS position sticky #166

Closed
apazzolini opened this issue Mar 8, 2019 · 14 comments
Closed

Fixed grid headers with CSS position sticky #166

apazzolini opened this issue Mar 8, 2019 · 14 comments

Comments

@apazzolini
Copy link

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.

@bvaughn
Copy link
Owner

bvaughn commented Mar 8, 2019

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?

@bvaughn
Copy link
Owner

bvaughn commented Mar 8, 2019

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!

@bvaughn bvaughn closed this as completed Mar 8, 2019
@MarkLeMerise
Copy link

MarkLeMerise commented Mar 22, 2019

@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!

@apazzolini
Copy link
Author

@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.

@sonofjack
Copy link

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

position: -webkit-sticky;

Thank you for this !

@bvaughn
Copy link
Owner

bvaughn commented Apr 2, 2019

Thanks! I'll update the FAQ sandbox

@thielium
Copy link

thielium commented Apr 3, 2019

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.

@john-osullivan
Copy link

john-osullivan commented Jun 13, 2019

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.

@apazzolini
Copy link
Author

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.

@lindsve
Copy link

lindsve commented Jan 31, 2020

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?

@astorchous
Copy link

@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.

@bvaughn
Copy link
Owner

bvaughn commented Mar 4, 2020

Looks nice in my Android+Chrome 👍

@znarkd
Copy link

znarkd commented Mar 6, 2020

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

@znarkd
Copy link

znarkd commented Mar 12, 2020

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.

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

9 participants