From e5ded5897253243b1fbffa17956401e9d53fcfd2 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 9 Nov 2020 15:46:48 -0800 Subject: [PATCH] [fix] ScrollView doesn't respond to descendant scroll events Workaround for React DOM's non-standard bubbling of scroll events. Fix #1800 --- .../src/exports/ScrollView/ScrollViewBase.js | 38 ++++++++++-------- .../ScrollView/__tests__/index-test.js | 40 ++++++++++++++++++- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js b/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js index cb2b9f4c..d774f0c9 100644 --- a/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js +++ b/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js @@ -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, 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, 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, forwardedRef) => { onTouchMove={createPreventableScrollHandler(onTouchMove)} onWheel={createPreventableScrollHandler(onWheel)} pointerEvents={pointerEvents} - ref={forwardedRef} + ref={useMergeRefs(scrollRef, forwardedRef)} style={[ style, !scrollEnabled && styles.scrollDisabled, diff --git a/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js b/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js index 99eecfd2..8a93d818 100644 --- a/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js @@ -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(); + }); + 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( + +
+ + ); + }); + const target = createEventTarget(ref.current); + act(() => { + target.scroll(); + }); + expect(onScroll).not.toBeCalled(); + }); + }); });