React-window: One flick scrolls forever (to end) on Safari and all iOS browsers with scroll snap enabled

Created on 13 Jul 2019  Â·  5Comments  Â·  Source: bvaughn/react-window

In Safari and iOS Safari/Chrome (but not desktop Chrome), if CSS scroll snapping is enabled, then a fast scroll ("flick" on mobile) will scroll to the end or beginning of the react-window list, even if the list is thousands of items long.

The problem is caused by a difference in how Blink (desktop Chrome) vs. WebKit (Safari, iOS) handles snap-scrolling past the last scroll snap position. Blink will refuse to scroll past the last scroll snap position so that react-window has time to populate new items in front of where the user is scrolling. WebKit, however, if the scroll is fast enough, will immediately scroll to the very end of the container before react-window has a chance to place new items in front of the user's scroll position.

Repro

  • Open https://codesandbox.io/s/happy-architecture-m7pn5 in Chrome on Mac
  • Scroll all three lists (each of which is 1000 items wide) horizontally any way you want: fast/slow, right/left, etc. Everything works as expected. Note that the bottom list is sparse: it only has the first 10 items and Chrome refuses to scroll past #10.
  • Now open the page with Safari on Mac
  • Scroll the middle ("HTML full") list quickly. It works normally.
  • Scroll the bottom ("HTML sparse") list forward sloooowly. It works normally, until you scroll the last item (#10) out of view. At that point, the entire list fast-forwards to the end of the list, which I guess makes sense given that there are no items left before the end of the container.
  • Scroll the top list (react-window) slowly. It works normally.
  • Now scroll the top list quickly. You don't need to scroll very far-- the speed is what matters.

Expected: normal scroll
Actual: scrolls all the way to the end/beginning of the 1000-item list! Note that this is the same behavior seen in the "HTML sparse" list when scrolling past the last item in the list.

I'm continuing to investigate this, but if anyone has seen this problem before or has insights into how to fix or work around it, let me know!

I also filed ~https://github.com/w3c/csswg-drafts/issues/4111~ _(UPDATE: discussion moved to https://github.com/w3c/csswg-drafts/issues/4037)_ to ask about whether Chrome's or WebKit's behavior is correct according to the standard. While I was there I asked for suggestions for how we can get the Chrome behavior on Safari.

I've skimmed the open WebKit bugs related to scroll snapping. I didn't see any obvious bugs that suggest this is a bug as opposed to expected behavior for WebKit.

Most helpful comment

I had the same problem with horizontal scrolling in iOS devices and found out something that might save some time for you:
apparently, the snap scrolling only work on direction: ltr and not on "rtl" (in iOS devices),
and also the scrolling stop working altogether when the parent has flex-direction: row-reverse.

hope it helps someone.

All 5 comments

After a long day of trying to find a workaround, I didn't make much progress. I found that setting scroll-snap-type: x proximity instead of scroll-snap-type: x mandatory fixes the problem on desktop Safari, but iOS browsers are still broken. (And using proximity makes desktop Chrome worse.) So that's not really a viable fix.

Unless anyone has other suggestions, I think the next step will be to give up on CSS snapping in Safari and iOS and use a polyfill like https://github.com/PureCarsLabs/css-scroll-snap-polyfill, or to roll my own solution. I'm leaning towards the latter.

If I end up rolling my own fix, it'd probably go something like this:

  1. Add an onScroll handler whose job will be to debounce calls to the handler, and when the user stops scrolling then...
  2. Calculate a desired (snapped) scroll position
  3. If that desired position is different (accounting for rounding per #228) from the current scroll position, then call scrollTo
  4. Prevent recursion, because calling scrollTo will trigger more onScroll events and (per #228) there's not a reliable way in current react-window to know if an onScroll was user-initiated or programmatic.

@bvaughn - I see that react-window already tracks isScrolling in component state. Seems wasteful (in complexity and perf) to add another scroll handler and debouncer to duplicate what react-window is already doing internally. Would you be open to a PR that adds an onScrollEnd event to the four react-window components? This would be helpful in use-cases beyond scroll snapping, like when you want to update another component after a list is done scrolling. I can't promise that I'll have time to make a PR for this, but if I do have time then I wanted to know if you think it'd be helpful.

I was trying a similar configuration as @justingrant and run into the same problem. Too bad because react-window with scroll-snap would be great – but it is too buggy on Safari.

It doesn't seem that easy to create a polyfill either...

I found adding -webkit-overflow-scrolling: touch; fixed a similar issue for me with scroll-snap-type: x mandatory might help here?

I had the same problem with horizontal scrolling in iOS devices and found out something that might save some time for you:
apparently, the snap scrolling only work on direction: ltr and not on "rtl" (in iOS devices),
and also the scrolling stop working altogether when the parent has flex-direction: row-reverse.

hope it helps someone.

Just ran into that myself on the RTL issue and landed here. The webkit bug for that appears to be https://bugs.webkit.org/show_bug.cgi?id=193671.

Was this page helpful?
0 / 5 - 0 ratings