Skip to content

Conversation

@sebmarkbage
Copy link
Collaborator

We added support for onScrollEnd in #26789 but it only works in Chrome and Firefox. Safari still doesn't support scrollend and there's no indication that they will anytime soon so this polyfills it.

While I don't particularly love our synthetic event system this tries to stay within the realm of how our other polyfills work. This implements all onScrollEnd events as a plugin.

The basic principle is to first feature detect the onscrollend DOM property to see if there's native support and otherwise just use the native event.

Then we listen to scroll events and set a timeout. If we don't get any more scroll events before the timeout we fire onScrollEnd. Basically debouncing it. If we're currently pressing down on touch or a mouse then we wait until it is lifted such as if you're scrolling with a finger or using the scrollbars on desktop but isn't currently moving.

If we do get any native events even though we're in polyfilling mode, we use that as an indication to fire the onScrollEnd early.

Part of the motivation is that this becomes extra useful pair for #32422. We also probably need these events to coincide with other gesture related internals so you're better off using our polyfill so they're synced.

@react-sizebot
Copy link

react-sizebot commented Feb 19, 2025

Comparing: 3607f48...399aa22

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.01% 518.24 kB 518.32 kB +0.02% 92.43 kB 92.45 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.47% 570.57 kB 573.25 kB +0.51% 101.56 kB 102.08 kB
facebook-www/ReactDOM-prod.classic.js +0.01% 638.06 kB 638.14 kB +0.03% 112.28 kB 112.31 kB
facebook-www/ReactDOM-prod.modern.js +0.01% 628.38 kB 628.46 kB +0.03% 110.70 kB 110.73 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-react-native/react-dom/cjs/ReactDOMClient-prod.js +0.49% 543.28 kB 545.95 kB +0.54% 96.42 kB 96.95 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-prod.js +0.49% 548.78 kB 551.46 kB +0.53% 97.50 kB 98.02 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-profiling.js +0.47% 568.14 kB 570.81 kB +0.51% 100.18 kB 100.70 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.47% 570.57 kB 573.25 kB +0.51% 101.56 kB 102.08 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-profiling.js +0.47% 574.08 kB 576.75 kB +0.51% 101.34 kB 101.85 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.46% 585.30 kB 587.98 kB +0.50% 105.12 kB 105.65 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +0.43% 626.05 kB 628.72 kB +0.47% 110.30 kB 110.82 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-dev.js +0.32% 993.54 kB 996.71 kB +0.33% 166.85 kB 167.40 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-dev.js +0.31% 1,009.87 kB 1,013.04 kB +0.34% 169.68 kB 170.26 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +0.30% 1,045.95 kB 1,049.12 kB +0.33% 175.18 kB 175.76 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +0.30% 1,062.35 kB 1,065.52 kB +0.32% 178.03 kB 178.61 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.30% 1,062.87 kB 1,066.04 kB +0.32% 178.90 kB 179.47 kB

Generated by 🚫 dangerJS against dead6ae

@sebmarkbage
Copy link
Collaborator Author

Note that at one point in the past the scroll event wasn't firing during momentum scroll in Safari. Then you needed to use a requestAnimationFrame loop to track when scroll position stopped updating. This doesn't appear to be the case any more, using debounced scroll seems enough.

Enabled in native and test-renderer since they shouldn't affect them anyway.
Makes it easier to see that it's safe to ship.
@sebmarkbage
Copy link
Collaborator Author

There's some movement in Safari. It's part of the roadmap for 2025. https://webkit.org/blog/16458/announcing-interop-2025/#scrollend-event

But we still need to polyfill for now and for older versions.


if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticUIEvent(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec says it's a generic Event not a UIEvent. I also get a generic Event in Chrome.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebmarkbage sebmarkbage merged commit 605a880 into facebook:main Mar 3, 2025
194 checks passed
github-actions bot pushed a commit that referenced this pull request Mar 3, 2025
We added support for `onScrollEnd` in #26789 but it only works in Chrome
and Firefox. Safari still doesn't support `scrollend` and there's no
indication that they will anytime soon so this polyfills it.

While I don't particularly love our synthetic event system this tries to
stay within the realm of how our other polyfills work. This implements
all `onScrollEnd` events as a plugin.

The basic principle is to first feature detect the `onscrollend` DOM
property to see if there's native support and otherwise just use the
native event.

Then we listen to `scroll` events and set a timeout. If we don't get any
more scroll events before the timeout we fire `onScrollEnd`. Basically
debouncing it. If we're currently pressing down on touch or a mouse then
we wait until it is lifted such as if you're scrolling with a finger or
using the scrollbars on desktop but isn't currently moving.

If we do get any native events even though we're in polyfilling mode, we
use that as an indication to fire the `onScrollEnd` early.

Part of the motivation is that this becomes extra useful pair for
#32422. We also probably need
these events to coincide with other gesture related internals so you're
better off using our polyfill so they're synced.

DiffTrain build for [605a880](605a880)
github-actions bot pushed a commit that referenced this pull request Mar 3, 2025
We added support for `onScrollEnd` in #26789 but it only works in Chrome
and Firefox. Safari still doesn't support `scrollend` and there's no
indication that they will anytime soon so this polyfills it.

While I don't particularly love our synthetic event system this tries to
stay within the realm of how our other polyfills work. This implements
all `onScrollEnd` events as a plugin.

The basic principle is to first feature detect the `onscrollend` DOM
property to see if there's native support and otherwise just use the
native event.

Then we listen to `scroll` events and set a timeout. If we don't get any
more scroll events before the timeout we fire `onScrollEnd`. Basically
debouncing it. If we're currently pressing down on touch or a mouse then
we wait until it is lifted such as if you're scrolling with a finger or
using the scrollbars on desktop but isn't currently moving.

If we do get any native events even though we're in polyfilling mode, we
use that as an indication to fire the `onScrollEnd` early.

Part of the motivation is that this becomes extra useful pair for
#32422. We also probably need
these events to coincide with other gesture related internals so you're
better off using our polyfill so they're synced.

DiffTrain build for [605a880](605a880)
Comment on lines +147 to +157
case 'touchstart': {
isTouchStarted = true;
break;
}
case 'touchcancel':
case 'touchend': {
// Note we cannot use pointer events for this because they get
// cancelled when native scrolling takes control.
isTouchStarted = false;
break;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the post-merge drive-by, but not accounting for multi-touch is an old pet peeve of mine. 😜

Suggested change
case 'touchstart': {
isTouchStarted = true;
break;
}
case 'touchcancel':
case 'touchend': {
// Note we cannot use pointer events for this because they get
// cancelled when native scrolling takes control.
isTouchStarted = false;
break;
}
case 'touchstart':
case 'touchcancel':
case 'touchend': {
// Note we cannot use pointer events for this because they get
// cancelled when native scrolling takes control.
isTouchStarted = ((nativeEvent: any): TouchEvent).touches.length > 0;
break;
}

Comment on lines +158 to +165
case 'mousedown': {
isMouseDown = true;
break;
}
case 'mouseup': {
isMouseDown = false;
break;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also shouldn't assume all mouse events are for left clicks, and mouseup won't fire after a drag starts.

Suggested change
case 'mousedown': {
isMouseDown = true;
break;
}
case 'mouseup': {
isMouseDown = false;
break;
}
case 'mousedown': {
if (((nativeEvent: any): MouseEvent).button === 0) {
isMouseDown = true;
}
break;
}
case 'mouseup':
case 'dragend': {
if (((nativeEvent: any): MouseEvent).button === 0) {
isMouseDown = false;
}
break;
}

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

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants