[fix] ScrollView doesn't respond to descendant scroll events

Workaround for React DOM's non-standard bubbling of scroll events.

Fix #1800
This commit is contained in:
Nicolas Gallagher
2020-11-09 15:46:48 -08:00
parent cf91d75471
commit e5ded58972
2 changed files with 59 additions and 19 deletions

View File

@@ -13,6 +13,7 @@ import * as React from 'react';
import { forwardRef, useRef } from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
import useMergeRefs from '../../modules/useMergeRefs';
type Props = {
...ViewProps,
@@ -93,6 +94,7 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
const scrollState = useRef({ isScrolling: false, scrollLastTick: 0 });
const scrollTimeout = useRef(null);
const scrollRef = useRef(null);
function createPreventableScrollHandler(handler: Function) {
return (e: Object) => {
@@ -105,29 +107,31 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
}
function handleScroll(e: Object) {
e.persist();
e.stopPropagation();
// A scroll happened, so the scroll resets the scrollend timeout.
if (scrollTimeout.current != null) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = setTimeout(() => {
handleScrollEnd(e);
}, 100);
if (scrollState.current.isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (shouldEmitScrollEvent(scrollState.current.scrollLastTick, scrollEventThrottle)) {
handleScrollTick(e);
if (e.target === scrollRef.current) {
e.persist();
// A scroll happened, so the scroll resets the scrollend timeout.
if (scrollTimeout.current != null) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = setTimeout(() => {
handleScrollEnd(e);
}, 100);
if (scrollState.current.isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (shouldEmitScrollEvent(scrollState.current.scrollLastTick, scrollEventThrottle)) {
handleScrollTick(e);
}
} else {
// Weren't scrolling, so we must have just started
handleScrollStart(e);
}
} else {
// Weren't scrolling, so we must have just started
handleScrollStart(e);
}
}
function handleScrollStart(e: Object) {
scrollState.current.isScrolling = true;
scrollState.current.scrollLastTick = Date.now();
handleScrollTick(e);
}
function handleScrollTick(e: Object) {
@@ -161,7 +165,7 @@ const ScrollViewBase = forwardRef<Props, *>((props, forwardedRef) => {
onTouchMove={createPreventableScrollHandler(onTouchMove)}
onWheel={createPreventableScrollHandler(onWheel)}
pointerEvents={pointerEvents}
ref={forwardedRef}
ref={useMergeRefs(scrollRef, forwardedRef)}
style={[
style,
!scrollEnabled && styles.scrollDisabled,

View File

@@ -1,5 +1,41 @@
/* eslint-env jasmine, jest */
import React from 'react';
import ScrollView from '../';
import { act } from 'react-dom/test-utils';
import { createEventTarget } from 'dom-event-testing-library';
import { findDOMNode } from 'react-dom';
import { render } from '@testing-library/react';
describe('components/ScrollView', () => {
test.skip('todo', () => {});
describe('prop "onScroll"', () => {
test('is called when element scrolls', () => {
const onScroll = jest.fn();
const ref = React.createRef();
act(() => {
render(<ScrollView onScroll={onScroll} ref={ref} scrollEventThrottle={16} />);
});
const target = createEventTarget(findDOMNode(ref.current));
act(() => {
target.scroll();
target.scroll();
});
expect(onScroll).toBeCalled();
});
test('is not called when descendant scrolls', () => {
const onScroll = jest.fn();
const ref = React.createRef();
act(() => {
render(
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
<div ref={ref} />
</ScrollView>
);
});
const target = createEventTarget(ref.current);
act(() => {
target.scroll();
});
expect(onScroll).not.toBeCalled();
});
});
});