Skip to content

Enable window/body based scrolling for all ScrollViews #1472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 163 additions & 10 deletions packages/react-native-web/src/exports/ScrollView/ScrollViewBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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
*/
Expand All @@ -63,23 +95,115 @@ 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) {
this._viewRef.setNativeProps(props);
}
}

_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,
Expand Down Expand Up @@ -118,6 +242,7 @@ export default class ScrollViewBase extends Component<*> {
snapToInterval,
snapToAlignment,
zoomScale,
useWindowScrolling,
/* eslint-enable */
...other
} = this.props;
Expand All @@ -127,9 +252,17 @@ export default class ScrollViewBase extends Component<*> {
return (
<View
{...other}
onScroll={this._handleScroll}
onTouchMove={this._createPreventableScrollHandler(this.props.onTouchMove)}
onWheel={this._createPreventableScrollHandler(this.props.onWheel)}
onLayout={useWindowScrolling ? undefined : other.onLayout}
// disable regular scroll handling if window scrolling is used
onScroll={useWindowScrolling ? undefined : this._handleScroll}
onTouchMove={
useWindowScrolling
? undefined
: this._createPreventableScrollHandler(this.props.onTouchMove)
}
onWheel={
useWindowScrolling ? undefined : this._createPreventableScrollHandler(this.props.onWheel)
}
ref={this._setViewRef}
style={[
style,
Expand All @@ -153,8 +286,26 @@ export default class ScrollViewBase extends Component<*> {
};
};

_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.
Expand All @@ -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));
}
}

Expand Down
29 changes: 25 additions & 4 deletions packages/react-native-web/src/exports/ScrollView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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'
},
Expand Down