diff --git a/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js b/packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js index ba5224c0c..e40321c11 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 View from '../View'; import ViewPropTypes from '../ViewPropTypes'; import React, { Component } from 'react'; import { bool, func, number } from 'prop-types'; +import findNodeHandle from '../findNodeHandle'; const normalizeScrollEvent = e => ({ nativeEvent: { @@ -44,6 +45,37 @@ const normalizeScrollEvent = e => ({ timeStamp: Date.now() }); +const normalizeWindowScrollEvent = e => ({ + nativeEvent: { + contentOffset: { + get x() { + return window.scrollX; + }, + get y() { + return window.scrollY; + } + }, + contentSize: { + get height() { + return window.innerHeight; + }, + get width() { + return window.innerWidth; + } + }, + layoutMeasurement: { + get height() { + // outer dimensions do not apply for windows + return window.innerHeight; + }, + get width() { + return window.innerWidth; + } + } + }, + timeStamp: Date.now() +}); + /** * Encapsulates the Web-specific scroll throttling and disabling logic */ @@ -63,16 +95,19 @@ export default class ScrollViewBase extends Component<*> { scrollEnabled: bool, scrollEventThrottle: number, showsHorizontalScrollIndicator: bool, - showsVerticalScrollIndicator: bool + showsVerticalScrollIndicator: bool, + useWindowScrolling: bool }; static defaultProps = { scrollEnabled: true, - scrollEventThrottle: 0 + scrollEventThrottle: 0, + useWindowScrolling: false }; _debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100); _state = { isScrolling: false, scrollLastTick: 0 }; + _windowResizeObserver: any | null = null; setNativeProps(props: Object) { if (this._viewRef) { @@ -80,6 +115,95 @@ export default class ScrollViewBase extends Component<*> { } } + _handleWindowLayout = () => { + const { onLayout } = this.props; + + if (typeof onLayout === 'function') { + const layout = { + x: 0, + y: 0, + get width() { + return window.innerWidth; + }, + get height() { + return window.innerHeight; + } + }; + + const nativeEvent = { + layout + }; + + // $FlowFixMe + Object.defineProperty(nativeEvent, 'target', { + enumerable: true, + get: () => findNodeHandle(this) + }); + + onLayout({ + nativeEvent, + timeStamp: Date.now() + }); + } + }; + + registerWindowHandlers() { + window.addEventListener('scroll', this._handleScroll); + window.addEventListener('touchmove', this._handleWindowTouchMove); + window.addEventListener('wheel', this._handleWindowWheel); + window.addEventListener('resize', this._handleWindowLayout); + + if (typeof window.ResizeObserver === 'function') { + this._windowResizeObserver = new window.ResizeObserver((/*entries*/) => { + this._handleWindowLayout(); + }); + // handle changes of the window content size. + // It technically works with regular onLayout of the container, + // but this called very often if the content change based on scrolling, e.g. FlatList + this._windowResizeObserver.observe(window.document.body); + } else if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + console.warn( + '"useWindowScrolling" relies on ResizeObserver which is not supported by your browser. ' + + 'Please include a polyfill, e.g., https://github.com/que-etc/resize-observer-polyfill. ' + + 'Only handling the window.onresize event.' + ); + } + this._handleWindowLayout(); + } + + unregisterWindowHandlers() { + window.removeEventListener('scroll', this._handleScroll); + window.removeEventListener('touchmove', this._handleWindowTouchMove); + window.removeEventListener('wheel', this._handleWindowWheel); + const { _windowResizeObserver } = this; + if (_windowResizeObserver) { + _windowResizeObserver.disconnect(); + } + } + + componentDidMount() { + if (this.props.useWindowScrolling) { + this.registerWindowHandlers(); + } + } + + componentDidUpdate({ useWindowScrolling: wasUsingBodyScroll }: Object) { + const { useWindowScrolling } = this.props; + if (wasUsingBodyScroll !== useWindowScrolling) { + if (wasUsingBodyScroll) { + this.unregisterWindowHandlers(); + } else { + this.registerWindowHandlers(); + } + } + } + + componentWillUnmount() { + if (this.props.useWindowScrolling) { + this.unregisterWindowHandlers(); + } + } + render() { const { scrollEnabled, @@ -118,6 +242,7 @@ export default class ScrollViewBase extends Component<*> { snapToInterval, snapToAlignment, zoomScale, + useWindowScrolling, /* eslint-enable */ ...other } = this.props; @@ -127,9 +252,17 @@ export default class ScrollViewBase extends Component<*> { return ( { }; }; + _handleWindowTouchMove = this._createPreventableScrollHandler(() => { + const { onTouchMove } = this.props; + if (typeof onTouchMove === 'function') { + return onTouchMove(); + } + }); + + _handleWindowWheel = this._createPreventableScrollHandler(() => { + const { onWheel } = this.props; + if (typeof onWheel === 'function') { + return onWheel(); + } + }); + _handleScroll = (e: Object) => { - e.persist(); + if (typeof e.persist === 'function') { + // this is a react SyntheticEvent, but not for window scrolling + e.persist(); + } + e.stopPropagation(); const { scrollEventThrottle } = this.props; // A scroll happened, so the scroll bumps the debounce. @@ -176,18 +327,20 @@ export default class ScrollViewBase extends Component<*> { } _handleScrollTick(e: Object) { - const { onScroll } = this.props; + const { onScroll, useWindowScrolling } = this.props; this._state.scrollLastTick = Date.now(); if (onScroll) { - onScroll(normalizeScrollEvent(e)); + const transformEvent = useWindowScrolling ? normalizeWindowScrollEvent : normalizeScrollEvent; + onScroll(transformEvent(e)); } } _handleScrollEnd(e: Object) { - const { onScroll } = this.props; + const { onScroll, useWindowScrolling } = this.props; this._state.isScrolling = false; if (onScroll) { - onScroll(normalizeScrollEvent(e)); + const transformEvent = useWindowScrolling ? normalizeWindowScrollEvent : normalizeScrollEvent; + onScroll(transformEvent(e)); } } diff --git a/packages/react-native-web/src/exports/ScrollView/index.js b/packages/react-native-web/src/exports/ScrollView/index.js index 3559ed1b5..55f1a4e9d 100644 --- a/packages/react-native-web/src/exports/ScrollView/index.js +++ b/packages/react-native-web/src/exports/ScrollView/index.js @@ -66,7 +66,12 @@ const ScrollView = createReactClass({ }, getScrollableNode(): any { - return findNodeHandle(this._scrollViewRef); + // when window scrolling is enabled, the scroll node is not the div, but the full page + if (this.props.useWindowScrolling) { + return window.document.documentElement; + } else { + return findNodeHandle(this._scrollViewRef); + } }, getInnerViewNode(): any { @@ -199,7 +204,19 @@ const ScrollView = createReactClass({ /> ); - const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical; + // window scrolling should not block overflow automatically as it needs to be able to grow the page. + const { useWindowScrolling } = other; + const horizontalStyle = [ + styles.baseHorizontal, + useWindowScrolling ? null : styles.baseHorizontalOverflow + ]; + const verticalStyle = [ + styles.baseVertical, + useWindowScrolling ? null : styles.baseVerticalOverflow + ]; + + const baseStyle = horizontal ? horizontalStyle : verticalStyle; + const pagingEnabledStyle = horizontal ? styles.pagingEnabledHorizontal : styles.pagingEnabledVertical; @@ -294,13 +311,17 @@ const commonStyle = { const styles = StyleSheet.create({ baseVertical: { ...commonStyle, - flexDirection: 'column', + flexDirection: 'column' + }, + baseVerticalOverflow: { overflowX: 'hidden', overflowY: 'auto' }, baseHorizontal: { ...commonStyle, - flexDirection: 'row', + flexDirection: 'row' + }, + baseHorizontalOverflow: { overflowX: 'auto', overflowY: 'hidden' },