Skip to content

Commit 17c2225

Browse files
committed
implement a version of window/body based scrolling for lists
1 parent 501ae4c commit 17c2225

File tree

2 files changed

+182
-13
lines changed

2 files changed

+182
-13
lines changed

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

+163-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import View from '../View';
1313
import ViewPropTypes from '../ViewPropTypes';
1414
import React, { Component } from 'react';
1515
import { bool, func, number } from 'prop-types';
16+
import findNodeHandle from '../findNodeHandle';
1617

1718
const normalizeScrollEvent = e => ({
1819
nativeEvent: {
@@ -44,6 +45,37 @@ const normalizeScrollEvent = e => ({
4445
timeStamp: Date.now()
4546
});
4647

48+
const normalizeWindowScrollEvent = e => ({
49+
nativeEvent: {
50+
contentOffset: {
51+
get x() {
52+
return window.scrollX;
53+
},
54+
get y() {
55+
return window.scrollY;
56+
}
57+
},
58+
contentSize: {
59+
get height() {
60+
return window.innerHeight;
61+
},
62+
get width() {
63+
return window.innerWidth;
64+
}
65+
},
66+
layoutMeasurement: {
67+
get height() {
68+
// outer dimensions do not apply for windows
69+
return window.innerHeight;
70+
},
71+
get width() {
72+
return window.innerWidth;
73+
}
74+
}
75+
},
76+
timeStamp: Date.now()
77+
});
78+
4779
/**
4880
* Encapsulates the Web-specific scroll throttling and disabling logic
4981
*/
@@ -63,23 +95,115 @@ export default class ScrollViewBase extends Component<*> {
6395
scrollEnabled: bool,
6496
scrollEventThrottle: number,
6597
showsHorizontalScrollIndicator: bool,
66-
showsVerticalScrollIndicator: bool
98+
showsVerticalScrollIndicator: bool,
99+
useWindowScrolling: bool
67100
};
68101

69102
static defaultProps = {
70103
scrollEnabled: true,
71-
scrollEventThrottle: 0
104+
scrollEventThrottle: 0,
105+
useWindowScrolling: false
72106
};
73107

74108
_debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100);
75109
_state = { isScrolling: false, scrollLastTick: 0 };
110+
_windowResizeObserver: any | null = null;
76111

77112
setNativeProps(props: Object) {
78113
if (this._viewRef) {
79114
this._viewRef.setNativeProps(props);
80115
}
81116
}
82117

118+
_handleWindowLayout = () => {
119+
const { onLayout } = this.props;
120+
121+
if (typeof onLayout === 'function') {
122+
const layout = {
123+
x: 0,
124+
y: 0,
125+
get width() {
126+
return window.innerWidth;
127+
},
128+
get height() {
129+
return window.innerHeight;
130+
}
131+
};
132+
133+
const nativeEvent = {
134+
layout
135+
};
136+
137+
// $FlowFixMe
138+
Object.defineProperty(nativeEvent, 'target', {
139+
enumerable: true,
140+
get: () => findNodeHandle(this)
141+
});
142+
143+
onLayout({
144+
nativeEvent,
145+
timeStamp: Date.now()
146+
});
147+
}
148+
};
149+
150+
registerWindowHandlers() {
151+
window.addEventListener('scroll', this._handleScroll);
152+
window.addEventListener('touchmove', this._handleWindowTouchMove);
153+
window.addEventListener('wheel', this._handleWindowWheel);
154+
window.addEventListener('resize', this._handleWindowLayout);
155+
156+
if (typeof window.ResizeObserver === 'function') {
157+
this._windowResizeObserver = new window.ResizeObserver((/*entries*/) => {
158+
this._handleWindowLayout();
159+
});
160+
// handle changes of the window content size.
161+
// It technically works with regular onLayout of the container,
162+
// but this called very often if the content change based on scrolling, e.g. FlatList
163+
this._windowResizeObserver.observe(window.document.body);
164+
} else if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
165+
console.warn(
166+
'"useWindowScrolling" relies on ResizeObserver which is not supported by your browser. ' +
167+
'Please include a polyfill, e.g., https://github.com/que-etc/resize-observer-polyfill. ' +
168+
'Only handling the window.onresize event.'
169+
);
170+
}
171+
this._handleWindowLayout();
172+
}
173+
174+
unregisterWindowHandlers() {
175+
window.removeEventListener('scroll', this._handleScroll);
176+
window.removeEventListener('touchmove', this._handleWindowTouchMove);
177+
window.removeEventListener('wheel', this._handleWindowWheel);
178+
const { _windowResizeObserver } = this;
179+
if (_windowResizeObserver) {
180+
_windowResizeObserver.disconnect();
181+
}
182+
}
183+
184+
componentDidMount() {
185+
if (this.props.useWindowScrolling) {
186+
this.registerWindowHandlers();
187+
}
188+
}
189+
190+
componentDidUpdate({ useWindowScrolling: wasUsingBodyScroll }: Object) {
191+
const { useWindowScrolling } = this.props;
192+
if (wasUsingBodyScroll !== useWindowScrolling) {
193+
if (wasUsingBodyScroll) {
194+
this.unregisterWindowHandlers();
195+
} else {
196+
this.registerWindowHandlers();
197+
}
198+
}
199+
}
200+
201+
componentWillUnmount() {
202+
if (this.props.useWindowScrolling) {
203+
this.unregisterWindowHandlers();
204+
}
205+
}
206+
83207
render() {
84208
const {
85209
scrollEnabled,
@@ -118,6 +242,7 @@ export default class ScrollViewBase extends Component<*> {
118242
snapToInterval,
119243
snapToAlignment,
120244
zoomScale,
245+
useWindowScrolling,
121246
/* eslint-enable */
122247
...other
123248
} = this.props;
@@ -127,9 +252,17 @@ export default class ScrollViewBase extends Component<*> {
127252
return (
128253
<View
129254
{...other}
130-
onScroll={this._handleScroll}
131-
onTouchMove={this._createPreventableScrollHandler(this.props.onTouchMove)}
132-
onWheel={this._createPreventableScrollHandler(this.props.onWheel)}
255+
onLayout={useWindowScrolling ? undefined : other.onLayout}
256+
// disable regular scroll handling if window scrolling is used
257+
onScroll={useWindowScrolling ? undefined : this._handleScroll}
258+
onTouchMove={
259+
useWindowScrolling
260+
? undefined
261+
: this._createPreventableScrollHandler(this.props.onTouchMove)
262+
}
263+
onWheel={
264+
useWindowScrolling ? undefined : this._createPreventableScrollHandler(this.props.onWheel)
265+
}
133266
ref={this._setViewRef}
134267
style={[
135268
style,
@@ -153,8 +286,26 @@ export default class ScrollViewBase extends Component<*> {
153286
};
154287
};
155288

289+
_handleWindowTouchMove = this._createPreventableScrollHandler(() => {
290+
const { onTouchMove } = this.props;
291+
if (typeof onTouchMove === 'function') {
292+
return onTouchMove();
293+
}
294+
});
295+
296+
_handleWindowWheel = this._createPreventableScrollHandler(() => {
297+
const { onWheel } = this.props;
298+
if (typeof onWheel === 'function') {
299+
return onWheel();
300+
}
301+
});
302+
156303
_handleScroll = (e: Object) => {
157-
e.persist();
304+
if (typeof e.persist === 'function') {
305+
// this is a react SyntheticEvent, but not for window scrolling
306+
e.persist();
307+
}
308+
158309
e.stopPropagation();
159310
const { scrollEventThrottle } = this.props;
160311
// A scroll happened, so the scroll bumps the debounce.
@@ -176,18 +327,20 @@ export default class ScrollViewBase extends Component<*> {
176327
}
177328

178329
_handleScrollTick(e: Object) {
179-
const { onScroll } = this.props;
330+
const { onScroll, useWindowScrolling } = this.props;
180331
this._state.scrollLastTick = Date.now();
181332
if (onScroll) {
182-
onScroll(normalizeScrollEvent(e));
333+
const transformEvent = useWindowScrolling ? normalizeWindowScrollEvent : normalizeScrollEvent;
334+
onScroll(transformEvent(e));
183335
}
184336
}
185337

186338
_handleScrollEnd(e: Object) {
187-
const { onScroll } = this.props;
339+
const { onScroll, useWindowScrolling } = this.props;
188340
this._state.isScrolling = false;
189341
if (onScroll) {
190-
onScroll(normalizeScrollEvent(e));
342+
const transformEvent = useWindowScrolling ? normalizeWindowScrollEvent : normalizeScrollEvent;
343+
onScroll(transformEvent(e));
191344
}
192345
}
193346

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

+19-3
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,19 @@ const ScrollView = createReactClass({
204204
/>
205205
);
206206

207-
const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical;
207+
// window scrolling should not block overflow automatically as it needs to be able to grow the page.
208+
const { useWindowScrolling } = other;
209+
const horizontalStyle = [
210+
styles.baseHorizontal,
211+
useWindowScrolling ? null : styles.baseHorizontalOverflow
212+
];
213+
const verticalStyle = [
214+
styles.baseVertical,
215+
useWindowScrolling ? null : styles.baseVerticalOverflow
216+
];
217+
218+
const baseStyle = horizontal ? horizontalStyle : verticalStyle;
219+
208220
const pagingEnabledStyle = horizontal
209221
? styles.pagingEnabledHorizontal
210222
: styles.pagingEnabledVertical;
@@ -299,13 +311,17 @@ const commonStyle = {
299311
const styles = StyleSheet.create({
300312
baseVertical: {
301313
...commonStyle,
302-
flexDirection: 'column',
314+
flexDirection: 'column'
315+
},
316+
baseVerticalOverflow: {
303317
overflowX: 'hidden',
304318
overflowY: 'auto'
305319
},
306320
baseHorizontal: {
307321
...commonStyle,
308-
flexDirection: 'row',
322+
flexDirection: 'row'
323+
},
324+
baseHorizontalOverflow: {
309325
overflowX: 'auto',
310326
overflowY: 'hidden'
311327
},

0 commit comments

Comments
 (0)