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

[cssom-view][proposal] beforescroll event #4172

Open
hdodov opened this issue Aug 5, 2019 · 9 comments
Open

[cssom-view][proposal] beforescroll event #4172

hdodov opened this issue Aug 5, 2019 · 9 comments

Comments

@hdodov
Copy link

hdodov commented Aug 5, 2019

My first issue here. I'll be making a suggestion for CSSOM View Module Events.

Problem

We have solid control over various input events. For keyboard, we have keydown, keyup, keypress (deprecated). For mouse, we have click, dblclick, mouseup,mousedown... We don't have great control over scroll, though.

The scroll event is useful for implementing functionality that responds to scrolling, but is too limited for more advanced use cases. It's emitted after the actual scrolling has taken place and therefore doesn't provide much control. You can't preventDefault() and prevent scrolling and if you use document.scrollingElement.scrollTop for scroll-based animations, for example, they can be glitchy since they would lag one frame.

Touch events and the wheel event might be used as an alternative. They fire before scrolling has taken place, but they don't fire continuously. They can only be used to know when scrolling is expected to start, which is not very helpful.

Solution

A middle ground between the scroll and wheel events is what's needed. My suggestion is to have the beforescroll event which:

  • is fired right before the element is actually scrolled
  • has deltaY/deltaX properties, similar to wheel
  • has defaultPrevented which prevents scroll

The delta properties specify with how much pixels the element will be scrolled in the current event loop. Or, in other words, element.scrollTop + event.deltaY in the beforescroll event should be equal to element.scrollTop in the scroll event. This is useful because:

  • it allows you to predict the next scroll position of the target element, fixing the lag issue with scroll mentioned above
  • you can easily know the difference in scroll between frames, accounting for the various animations/easings of UAs, without having to compare scrollTop between the scroll event emissions (which also lag one frame)

You can call preventDefault() to prevent scrolling, but not any following beforescroll events. In Chrome, this code:

var delta = null
window.addEventListener('scroll', function (e) {
  var st = document.documentElement.scrollTop
  console.log(st - (delta || st))
  delta = st
})

...would log similar to the following after a single wheel event with deltaY of 100:

0
5
7
10
13
14
15
12
10
8
4
1

Here's a fiddle. With beforescroll, you should be able to do this:

window.addEventListener('beforescroll', function (e) {
  console.log(e.deltaY)

  // animate some element with deltaX/deltaY
  e.preventDefault()
})

...and the scroll target should not be scrolled, while the logs should read:

5
7
10
13
14
15
12
10
8
4
1

Since the actual scroll was prevented, scroll events should not fire after the beforescroll handler.

Why

Having this event would allow developers to implement advanced behaviors based on scrolling accurately. It would also give a reliable way to prevent scrolling, which is currently not easily achievable. You could prevent scrolling with the mouse wheel by calling preventDefault() on the wheel event, but you can still scroll with the scrollbar, arrow keys, scrollTo() or even clicking a link with a hash.

Use cases:

  1. If the site visitor scrolls to a section with horizontal overflow, the developer could use beforescroll to prevent vertical scrolling and use the delta properties to scroll that section horizontally. When the section is fully scrolled, vertical scroll is no longer prevented and the user can continue scrolling the site. This is a UX pattern for carousel-like content.
  2. Better "scroll hijacking." Even though that's considered bad practice, beforescroll could allow for better hijacking which, in the right hands, can improve UX. For example, while the site visitor scrolls through a section, the developer could use beforescroll to prevent scrolling, optionally animate something inside the section with deltaY, and use document.scrollingElement.scrollTop += Math.ceil(event.deltaY / 2). This would still scroll the page vertially, with the UA's expected scroll easing, only at half the speed. This way, the developer can easily implement dynamic visuals for a content section and then emphasize them by directing the user's attention with slower scrolling.
  3. Similar to the previous use case, the developer could speed up (instead of slowing down) the scrolling of a page that has more whitespace as a part of it's design.
  4. For use cases where you don't want to disturb the natural page scrolling, you can still use beforescroll to implement scroll-triggered animations via the delta properties. This would be easier and more accurate compared to manually measuring delta with a scroll handler.

Edit: This event would also open possibilities for carousel libraries on mobile. They could use overflow:auto and control the behavior with beforescroll, instead of setting overflow:hidden and manually scrolling the target by monitoring touch events.

@emilio
Copy link
Collaborator

emilio commented Aug 5, 2019

I'm not sure this is something people would want given having such an event will necessarily disable all accelerated scrolling (at least if the page uses non-passive listeners for that).

@hdodov
Copy link
Author

hdodov commented Aug 5, 2019

@emilio just so we're on the same page - by accelerated scrolling you mean how Chrome for example increases deltaY on the wheel event from 100 to 200, 300 or more, depending on how rapidly the user is scrolling?

If yes, then there might have been a misunderstanding. My idea was to be able to interrupt scrolling right before the UA changes scrollTop. I mean, the UA should apply acceleration and other similar calculations. It should act as if the element is actually scrolling, except the scroll position doesn't change. If I scroll very rapidly, I should get larger deltas in the beforescroll event but the element's scrollTop should simply stay the same if defaultPrevented is true.

@emilio
Copy link
Collaborator

emilio commented Aug 5, 2019

No, with accelerated scrolling I mean scrolling without going back to the main thread (async scrolling, etc).

Right now when you use your trackpad, or when you use the wheel and there are no wheel event listeners, and keyboard (at least Firefox allows in some circumstances to use accelerated scrolling when you use the keyboard, in some situations), etc, scrolling is pretty fast, because it doesn't need to go through the main thread and it's a compositor-only effect.

Your event would mean that the UA cannot really scroll without dispatching an event and then knowing if that event is default-prevented.

@hdodov
Copy link
Author

hdodov commented Aug 5, 2019

From my understanding, this:

Right now when you use your trackpad, or when you use the wheel and there are no wheel event listeners [...]

Needs to be changed so that it checks if there are beforescroll event listeners too, aside from wheel listeners. If nothing is listening for beforescroll, nothing can prevent scrolling and therefore, the browser can use accelerated scrolling?

You could also flag the beforescroll listener as passive, just as you can do with scroll and still benefit from accelerated scrolling?

@emilio
Copy link
Collaborator

emilio commented Aug 5, 2019

If there's a slow script running on the page, right now you can scroll. If the browser needs to wait for that script to finish in order to trigger the event (and run more script) in order to figure out if it can even start scrolling, it defeats the point of accelerated scrolling.

@emilio
Copy link
Collaborator

emilio commented Aug 5, 2019

Making them passive would indeed work, but that prevents you from being able to preventDefault() them.

@hdodov
Copy link
Author

hdodov commented Aug 5, 2019

From what I understand, having a non-passive wheel event listener means the browser no longer uses accelerated scrolling (since it needs to wait for the event handler to see if it should further scroll).

Well, can't it be the same for the beforescroll event too? By adding a non-passive beforescroll listener, you opt-out of accelerated scrolling in exchange for better scroll control. It's a decision that the developer makes. The UA should simply give that choice.

If I have a passive wheel listener and no beforescroll listeners, I should benefit from accelerated scrolling since there's still nothing to prevent the scroll from happening. It should act as the same way as if my proposed event was not implemented.

I think the passive option of events was designed to solve that very problem you're talking about. You won't have better scrolling performance with a non-passive beforescroll the same way you won't have it with a non-passive wheel listener. By specifying that the event is passive, you tell the browser that it can assume nothing will prevent scrolling so it can optimize it. Why can't beforescroll function the same way?

@emilio
Copy link
Collaborator

emilio commented Aug 5, 2019

Sure, it can be a passive-by-default event or what not, and behave like you describe, I'm just unsure if people are going to be keen to add a potential performance footgun like that.

@hdodov
Copy link
Author

hdodov commented Aug 5, 2019

I think that people would very much prefer sacrificing this performance benefit over using hacks, workarounds, overflow, and whatnot to "prevent" the browser from scrolling or using position:fixed so that content can appear like it's not scrolling.

Accelerated scrolling is a performance benefit after all. If you use beforescroll wisely, it shouldn't be such a problem. Besides, beforescroll is useful for passive functionality as well, thanks to its delta properties and the fact that whatever you do with it won't lag behind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants