Skip to content

Commit e5ded58

Browse files
committed
[fix] ScrollView doesn't respond to descendant scroll events
Workaround for React DOM's non-standard bubbling of scroll events. Fix #1800
1 parent cf91d75 commit e5ded58

File tree

2 files changed

+59
-19
lines changed

2 files changed

+59
-19
lines changed

packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as React from 'react';
1313
import { forwardRef, useRef } from 'react';
1414
import StyleSheet from '../StyleSheet';
1515
import View from '../View';
16+
import useMergeRefs from '../../modules/useMergeRefs';
1617

1718
type Props = {
1819
...ViewProps,
@@ -93,6 +94,7 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
9394

9495
const scrollState = useRef({ isScrolling: false, scrollLastTick: 0 });
9596
const scrollTimeout = useRef(null);
97+
const scrollRef = useRef(null);
9698

9799
function createPreventableScrollHandler(handler: Function) {
98100
return (e: Object) => {
@@ -105,29 +107,31 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
105107
}
106108

107109
function handleScroll(e: Object) {
108-
e.persist();
109110
e.stopPropagation();
110-
// A scroll happened, so the scroll resets the scrollend timeout.
111-
if (scrollTimeout.current != null) {
112-
clearTimeout(scrollTimeout.current);
113-
}
114-
scrollTimeout.current = setTimeout(() => {
115-
handleScrollEnd(e);
116-
}, 100);
117-
if (scrollState.current.isScrolling) {
118-
// Scroll last tick may have changed, check if we need to notify
119-
if (shouldEmitScrollEvent(scrollState.current.scrollLastTick, scrollEventThrottle)) {
120-
handleScrollTick(e);
111+
if (e.target === scrollRef.current) {
112+
e.persist();
113+
// A scroll happened, so the scroll resets the scrollend timeout.
114+
if (scrollTimeout.current != null) {
115+
clearTimeout(scrollTimeout.current);
116+
}
117+
scrollTimeout.current = setTimeout(() => {
118+
handleScrollEnd(e);
119+
}, 100);
120+
if (scrollState.current.isScrolling) {
121+
// Scroll last tick may have changed, check if we need to notify
122+
if (shouldEmitScrollEvent(scrollState.current.scrollLastTick, scrollEventThrottle)) {
123+
handleScrollTick(e);
124+
}
125+
} else {
126+
// Weren't scrolling, so we must have just started
127+
handleScrollStart(e);
121128
}
122-
} else {
123-
// Weren't scrolling, so we must have just started
124-
handleScrollStart(e);
125129
}
126130
}
127131

128132
function handleScrollStart(e: Object) {
129133
scrollState.current.isScrolling = true;
130-
scrollState.current.scrollLastTick = Date.now();
134+
handleScrollTick(e);
131135
}
132136

133137
function handleScrollTick(e: Object) {
@@ -161,7 +165,7 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
161165
onTouchMove={createPreventableScrollHandler(onTouchMove)}
162166
onWheel={createPreventableScrollHandler(onWheel)}
163167
pointerEvents={pointerEvents}
164-
ref={forwardedRef}
168+
ref={useMergeRefs(scrollRef, forwardedRef)}
165169
style={[
166170
style,
167171
!scrollEnabled && styles.scrollDisabled,
Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
1-
/* eslint-env jasmine, jest */
1+
import React from 'react';
2+
import ScrollView from '../';
3+
import { act } from 'react-dom/test-utils';
4+
import { createEventTarget } from 'dom-event-testing-library';
5+
import { findDOMNode } from 'react-dom';
6+
import { render } from '@testing-library/react';
27

38
describe('components/ScrollView', () => {
4-
test.skip('todo', () => {});
9+
describe('prop "onScroll"', () => {
10+
test('is called when element scrolls', () => {
11+
const onScroll = jest.fn();
12+
const ref = React.createRef();
13+
act(() => {
14+
render(<ScrollView onScroll={onScroll} ref={ref} scrollEventThrottle={16} />);
15+
});
16+
const target = createEventTarget(findDOMNode(ref.current));
17+
act(() => {
18+
target.scroll();
19+
target.scroll();
20+
});
21+
expect(onScroll).toBeCalled();
22+
});
23+
24+
test('is not called when descendant scrolls', () => {
25+
const onScroll = jest.fn();
26+
const ref = React.createRef();
27+
act(() => {
28+
render(
29+
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
30+
<div ref={ref} />
31+
</ScrollView>
32+
);
33+
});
34+
const target = createEventTarget(ref.current);
35+
act(() => {
36+
target.scroll();
37+
});
38+
expect(onScroll).not.toBeCalled();
39+
});
40+
});
541
});

0 commit comments

Comments
 (0)