Closed jedwards1211 closed 7 years ago
This feature request makes sense but I have no plans to create the wrap-around functionality you describe at the moment. I think it would be pretty hard to get right, when you consider things like momentum scrolling.
I think I'd rather spend time doing performance tuning on the functionality that exists currently than create new things like that. (Mostly just acknowledging my own limits.)
Closing this now since I don't plan to act on it but please feel free to share more ideas and stuff on this issue. Maybe in the future we'll revisit. 😄
@bvaughn yes, momentum scrolling would be tricky -- now that I think about it though, if I implement it for myself, I may just handle wheel events directly instead of making the DOM element scrollable and doing some kind of wrapping. I've handled wheel events in things like zoomable plots and I know that macOS at least manages scroll momentum at a low enough level that it automatically dispatches decelerating wheel events after I flicked and released with two fingers.
@bvaughn just for future reference, I played with this old JSFiddle demo: http://jsfiddle.net/27s5y/
Good news! When scrolling with either my MacBook touchpad, or flicking in chrome on my android phone, momentum persists through wrap-arounds.
Nice! Looks very smooth in Chrome, Safari, and Edge!
In Firefox the momentum totally stops when the scrolling wraps around top or bottom though. I suspect there might be a way to make Firefox happy if the other 3 are.
Oh, that sucks...
I got a basic two-way vertical infinite scroll wrapper for Grid
working in Chrome! Notably, I had to call the Grid
's _resetStyleCache
whenever scrolling wraps around to make the cells render the proper contents before scrolling stops.
Here's the code:
import React from 'react'
import ReactDOM from 'react-dom'
import {Grid} from 'react-virtualized'
export default class InfiniteGrid extends React.Component {
state = {
scrollTop: 0,
yOffset: 0,
}
onScroll = ({clientHeight, scrollHeight, scrollTop}) => {
if (scrollTop >= scrollHeight - clientHeight) {
const newScrollTop = scrollTop % this.props.rowHeight + 1
this.setState({
scrollTop: newScrollTop,
yOffset: this.state.yOffset + scrollHeight - clientHeight - newScrollTop,
})
if (this.grid) this.grid._resetStyleCache()
} else if (scrollTop == 0) {
this.setState({
scrollTop: scrollHeight - clientHeight - 1,
yOffset: this.state.yOffset - scrollHeight + clientHeight + 1,
})
if (this.grid) this.grid._resetStyleCache()
} else {
this.setState({scrollTop})
}
}
cellRenderer = (props) => {
const {cellRenderer, rowHeight} = this.props
const {yOffset} = this.state
const rowIndex = props.rowIndex + Math.floor(yOffset / rowHeight)
return cellRenderer({...props, rowIndex})
}
render(): React.Element<any> {
const {height, rowHeight} = this.props
const {scrollTop, yOffset} = this.state
const rowCount = Math.ceil(height / rowHeight) + 10
return (
<Grid
{...this.props}
ref={c => this.grid = c}
yOffset={yOffset}
rowCount={rowCount}
scrollTop={scrollTop}
counter={scrollTop}
onScroll={this.onScroll}
cellRenderer={this.cellRenderer}
/>
)
}
}
ReactDOM.render(
<InfiniteGrid
columnCount={2}
width={600}
height={600}
columnWidth={300}
rowHeight={30}
cellRenderer={({rowIndex, columnIndex, key, style}) =>
<div key={key} style={style}>
{rowIndex}, {columnIndex}
</div>
}
/>,
document.getElementById('root')
)
Nice! Thanks for sharing! Hopefully that will be helpful to someone else in the future 😁 (maybe even me)
PS Didn't mean to be a buzzkill with the Firefox comment! Just wanted to make sure you knew~ I was actually surprised that both Edge and Safari seemed happy
@bvaughn no worries, yeah, it's important to know. I was kind of surprised too. Even though the momentum dies in Firefox it's still usable, so I think I'll run with this for a calendar view I'm building 😄
@bvaughn another problem with my code example is if you flick too fast it gets stuck at the top or bottom somehow...it doesn't wrap around and there's no more room to scroll up so scroll events don't get dispatched until you scroll the other direction at least 1 pixel and then back in the direction you were trying to go. Maybe I can work around that with a wheel event listener.
Yeah, I think this sort of thing is easier to do on mobile/native 😄 but this looks promising!
@bvaughn glad you like it! If I figure out anything else I'll let you know. I suppose if I wanted to really go the extra mile I could simulate momentum in Firefox...
You could but that seems...maybe like a path I wouldn't want to go down 😁 Best to let the browser manage that sort of thing if possible. If you did it, it would be in the UI thread and might feel clunky- plus you'd be stuck trying to mimic/estimate browser behavior.
Oh I know.
I guess if I have time I'll also try coding up something from scratch that just responds to wheel events in a DOM element without scrollbars and does the virtualized rendering by itself. That may be immune to momentum issues.
That's what some windowing libs (like fixed-data-table) do. The downside is that- any custom scrolling solution is stuck in the main/UI thread, so if renders are slow, the scrolling will feel laggy or unresponsive. Eh...trade-offs 😁 Gotta decide which have the fewest cons. Good luck either way!
@jedwards1211 For what it's worth, you could check out what the Angular Material team came up with for inspiration, sounds quite similar to what you're describing. If I recall correctly, it works by listening to mousewheel events and translating a container with translate3d
, and can "scroll" infinitely in both directions. Check out this infinite scrolling datepicker for reference: https://material.angularjs.org/1.1.2/demo/datepicker
It doesn't feel quite as perfect as native scrolling, but it's pretty close. The differences are mostly noticeable when scrolling really fast.
@clauderic yeah, that's exactly what I was thinking, they did a nice job. Just a shame it's not a React windowing lib yet 😄 There's this but I'm not sold, I'm skeptical of any windowing library where callbacks to load cells are part of the API, because what if I don't need to load anything?
If I ever get around to making a library for this I'm thinking I might call it react-omnidirectional-infinite-scroll
.
And I'll probably just use this calendar component for my work, even though it doesn't have infinite scroll, because a lot goes into making a nice event calendar...
@clauderic you didn't even shill your own react-infinite-calendar
? Looks awesome! Don't be so humble 😉
@clauderic how do you handle scrolling back in time with react-tiny-virtual-list
? Does it support negative scroll offset?
Haha, thanks for the kind words man 😊
react-infinite-calendar
needs a min
and max
date to render, so it isn't truly "infinite" the way you described. This was a conscious decision on my part when building it out, as I felt that there weren't that many use-cases for using a scrolling datepicker to go many millennia in the past or future, and that for most people, being able to scroll a hundred years or two in the past and a few hundred years in the future would be plenty sufficient. From a UX perspective, I think a scrolling datepicker might not be the best approach if you have to scroll too far into the past or future anyway, as it might take a really long time to get there. For those cases, a conventional calendar might be better for UX.
The upside of this decision was that I was well within the browser limits of what can be natively rendered, so virtualization libraries were a good fit, and there was no need to try and emulate native scroll behavior.
Having said that though, I do believe there could be value in a separate omnidirectional virtualization library that would potentially truly be infinite.
Regarding react-tiny-virtual-list
, it essentially behaves the same way as react-virtualized
's <List/>
component (and is quite heavily inspired by it, might I add ❤️), so it doesn't support negative scroll offsets.
@clauderic ah I see, yeah, that's a good point that it's usually no problem to restrict the min date. Well it's certainly possible to add a way to jump to a given year and month without sacrificing the infinite scrolling in a datepicker. My initial inspiration for this came from the macOS calendar, which I think is pretty nice. (Interestingly, it treats slow scrolling and fast flicks as separate gestures, for the latter it aligns to the prev/next month.)
There are several things I'd like to make infinitely scrollable:
In both of these cases I want to be able to scroll as far into the past or future as possible, but since
<Grid>
isn't really designed to scroll up above row 0, it would take some kind of hacks to enable that.For use cases like this, I think it's probably better for the scroll container to wrap around when it reaches the bottom or top, but maintain a "virtual" scroll position. Then the scroll container could pass its virtual scroll bounds (top/bottom/left/right) to a function I provide that decides what to render for those bounds.
But maybe that should just be a separate package...what do you think?