0

I'm trying to develop timeline component. This is stripped version of it to demonstrate issue:

https://jsfiddle.net/mauron85/5vverasn/

image

It should render 48 hour intervals between two dates (1/30/2007 - 1/31/2007) in table header and courier routes (rendered as cascade of squares), courier working hours and striped background in the table body.

My problem is that scrolling is only possible up to last courier route (3PM). There are no other routes, but I would like to enable scrolling to very last hour interval (11PM)

It's working when there is only one component in table body, but it's not when there more of them. In this case there is only stripe background.

image

Pseudo code:

this is working perfectly:

<ScrollSync>
    <AutoSizer disableHeight>
        <Grid className={styles.HeaderGrid} /> {/* hour intervals */}
        <div className={styles.Body}>
            <Grid /> {/* table striped background */}
        </div>
    </AutoSizer>
</ScrollSync>

but scrolling is limited when multiple children in AutoSizer:

<ScrollSync>
    <AutoSizer disableHeight>
        <div>
            <Grid className={styles.HeaderGrid} /> {/* hour intervals */}
            <div className={styles.Body}>
                <Grid /> {/* table striped background */}
                <Collection/> {/* courier routes has to be collection */}
                <Collection/> {/* courier working hours has to be collection */}
            </div>
        </div>
    </AutoSizer>
</ScrollSync>

Comment out block: {/* when following commented scrolling works as expected */} in sample project to get expected scrolling behavior.

Not 100% sure where is the catch, but I believe it's because every child component Grid, Collection ... has its own ReactVirtualized__Collection__innerScrollContainer, which has different width and only very last in DOM tree is actually scrollable/visible. Playing with z-index of components confirms this.

enter image description here

mauron85
  • 1,364
  • 1
  • 14
  • 28

2 Answers2

1

Not 100% sure where is the catch, but I believe it's because every child component Grid, Collection ... has its own ReactVirtualized__Collection__innerScrollContainer, which has different width and only very last in DOM tree is actually scrollable/visible. Playing with z-index of components confirms this.

Yes. This isn't anything specific to react-virtualized. If you stack several scrollable DOM elements, only the top-most one is going to receive scroll events. You can allow events to pass through the top-most element using pointer-events: none but you'll still only have a single element receiving scroll events.

If you want to coordinate the scrolling of multiple elements, the way to do that would be to place them all within a scrollable container. That's trickier to do with a lib like react-virtualized of course, but... looking at the pictures you pasted in, I would suggest you may not actually need the two Collections. Instead you might be able to get away with using a Grid and supplying your own, custom cellRangeRenderer that can be used to render the overlay UI. (This is the sort of thing that property was exposed to handle.) This would perform better as well because Grid performs better than Collection (and 1 component typically performs better than 3).

Here's a rough outline of what I am suggesting:

import { defaultCellRangeRenderer, Grid } from 'react-virtualized'

function cellRangeRenderer (props) {
  // Use the default cellRangeRenderer to render your Grid's cells
  // No need to re-implement that
  const children = defaultCellRangeRenderer(props)

  // cellRangeRenderer is passed params representing the Grid's current offset
  // Use these to decide what Collection items you should display
  const { scrollLeft, scrollTop } = props

  // Add the additional items you were putting into the Collection
  // They were just absolutely positioned elements anyway so treat them the same
  children.push(
    <div style={...}>This could be a Collection cell</div>
  )

  // NOTE If you think your Grid will be larger than 1.5M pixels,
  // You will also need to account for compression, see:
  // https://github.com/bvaughn/react-virtualized/blob/eb50a1569c8a59ec3d296145b636dff4b24ccae8/source/Grid/defaultCellRangeRenderer.js#L77-L83

  return children
}

function YourGrid (props) {
  return (
    <Grid
      cellRangeRenderer={cellRangeRenderer}
      {...props}
    />
  )
}
bvaughn
  • 13,300
  • 45
  • 46
  • Thank you. I'll try that. In mean time I've come with own solution. I'll add it as separate answer, just for alternative implementation (but worst in performance and elegance as yours). – mauron85 Feb 21 '17 at 13:50
  • Trying to implement rendering of all components in cellRangeRender as you suggested. So there will be Grid with cellRangeRender, inside which I render additional Collection. However problem with that is that nested Collection will have own innerScrollContainer. Is there any easy way to render Collection in cellRangeRender method, without own innerScrollContainer? https://jsfiddle.net/mauron85/p562pnoe/2/ – mauron85 Feb 23 '17 at 16:09
  • I wasn't suggesting you render a nested `Collection`. That would be expensive. I was suggesting you render the items you would put in that `Collection` directly inside of your `Grid` (the `cellRangeRenderer` you provide) in addition to your `Grid` cells. – bvaughn Feb 23 '17 at 17:08
  • I've edited my answer to provide a code snippet explaining what I was suggesting. – bvaughn Feb 23 '17 at 17:13
  • Understood. My intention with putting Collection(s) inside Grid is that I would like to have benefit of partial data rendering based on scroll position, both for grid and nested collection. So render Grid with 48 cols, the Grid will represent just the zebra background. Inside cellRangeRenderer I would like to render additional Collections representing courier routes (rectangles), but because of performance I would like to not render them all at once (as you suggested), but put them into collection. – mauron85 Feb 23 '17 at 17:21
  • To achieve thatm I've overridden Collection and CollectionView to achieve that. Basically I've only removed rendering of innerScrollContainer. But there must be a better way, like just using some functions from those classes. – mauron85 Feb 23 '17 at 17:23
  • I'm not suggesting you render them all at once. Just render the ones needed for the current set of cells being displayed. You have the column start/stop indices, row stop/start indices, and scroll left/top. That should be all the info you need to decide which `Collection` cells to render. What you're doing is a pretty advanced use-case. – bvaughn Feb 23 '17 at 17:28
  • Basically I wanted to reuse as much code as possible from Collection/CollectionView to avoid deep diving into codebase. But it seems I cannot avoid it anyway. Need to figure out what indices... Anyway many thanks @brainvaughn. – mauron85 Feb 23 '17 at 17:34
  • Just for completeness. This is demo with Grid with nested Collection using overridden Collection and CollectionView classes. https://jsfiddle.net/mauron85/p562pnoe/7/ – mauron85 Feb 23 '17 at 20:09
  • One of the last comment on this topic. I've followed your advice and implemented rendering of component in cellRangeRender reusing as much of your code as possible. Demo: https://jsfiddle.net/mauron85/p562pnoe/8/ (cellRangeRender starts at line 180). Of course it needs some polish. I've found out, that in my use case, it will be nice if plugin also exports: calculateSizeAndPositionData and defaultCellGroupRenderer so I don't have to duplicate them or do custom builds. Anyway react-virtualized is fantastic. Many thanks. – mauron85 Feb 23 '17 at 22:14
0

I've come with following solution. Basically it's custom Scroller component which wraps all scrollable components and setting width and height of all scrollable components with height and width of largest one.

<Scroller
    width={width}
    height={height}
    leftOffset={100}
    totalWidth={totalWidth}
    totalHeight={totalHeight}
    scrollTop={scrollTop}
    scrollLeft={scrollLeft}
    onScroll={onScroll}
>
    <div className="Body">
        <div className="BodyPart">
            <Grid
                className="BodyGrid"
                width={totalWidth}
                height={totalHeight}
                columnWidth={columnWidth}
                columnCount={columnCount}
                rowHeight={rowHeight}
                rowCount={rowCount}
                overscanColumnCount={overscanColumnCount}
                overscanRowCount={overscanRowCount}
                cellRenderer={this.renderBodyCell}
                scrollTop={scrollTop}
                scrollLeft={scrollLeft}
            />
        </div>
        <div className="BodyPart">
            <Routes
                startTime={startDate.getTime()}
                routes={Object.values(routes)}
                courierIds={courierIds}
                height={totalHeight}
                width={totalWidth}
                rowHeight={rowHeight}
                columnWidth={columnWidth}
                scrollTop={scrollTop}
                scrollLeft={scrollLeft}
            />
        </div>
    </div>
</Scroller>

Demo: https://jsfiddle.net/mauron85/97dj3baq/

mauron85
  • 1,364
  • 1
  • 14
  • 28