From 894fd0362dbaaa24dbe262bb5de54fbd70f101b6 Mon Sep 17 00:00:00 2001 From: Tom Ashworth Date: Tue, 20 Oct 2015 15:57:51 -0700 Subject: [PATCH] [add] initial ScrollView Supports the following props: `children`, `contentContainerStyle`, `horizontal`, `onScroll`, `scrollEnabled`, `scrollEventThrottle`, and `style`. Fix #6 --- docs/components/ScrollView.md | 55 +++++--- examples/components/App.js | 67 +++++++++- package.json | 1 + .../ScrollView/ScrollViewStylePropTypes.js | 4 + .../ScrollView/__tests__/index-test.js | 10 ++ src/components/ScrollView/index.js | 126 +++++++++++++++++- 6 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 src/components/ScrollView/ScrollViewStylePropTypes.js diff --git a/docs/components/ScrollView.md b/docs/components/ScrollView.md index 3ec507a3..39973d81 100644 --- a/docs/components/ScrollView.md +++ b/docs/components/ScrollView.md @@ -1,6 +1,7 @@ # ScrollView -TODO +Scrollable `View` for use with bounded height, either by setting the height of +the view directly (discouraged) or by bounding the height of ancestor views. ## Props @@ -11,27 +12,29 @@ Child content. **contentContainerStyle**: style These styles will be applied to the scroll view content container which wraps -all of the child views. Example: +all of the child views. **horizontal**: bool = false -When true, the scroll view's children are arranged horizontally in a row instead of vertically in a column. Default: `false`. +When true, the scroll view's children are arranged horizontally in a row +instead of vertically in a column. **onScroll**: function -Fires at most once per frame during scrolling. The frequency of the events can be contolled using the `scrollEventThrottle` prop. +Fires at most once per frame during scrolling. The frequency of the events can +be contolled using the `scrollEventThrottle` prop. -**scrollEnabled**: bool +**scrollEnabled**: bool = true -When false, the content does not scroll. Default: `true`. +When false, the content does not scroll. -**scrollEventThrottle**: number +**scrollEventThrottle**: number = 0 This controls how often the scroll event will be fired while scrolling (in events per seconds). A higher number yields better accuracy for code that is -tracking the scroll position, but can lead to scroll performance problems. -Default: `0` (the scroll event will be sent only once each time the view is -scrolled.) +tracking the scroll position, but can lead to scroll performance problems. The +default value is `0`, which means the scroll event will be sent only once each +time the view is scrolled. **style**: style @@ -40,20 +43,42 @@ scrolled.) ## Examples ```js -import React, { ScrollView } from 'react-native-web' +import React, { ScrollView, StyleSheet } from 'react-native-web' -const { Component, PropTypes } = React; +import Item from './Item' -class Example extends Component { - static propTypes = { +export default class App extends React.Component { + constructor(props, context) { + super(props, context) + this.state = { + items: Array.from({ length: 20 }).map((_, i) => ({ id: i })) + } } - static defaultProps = { + onScroll(e) { + console.log(e) } render() { return ( + )} + contentContainerStyle={styles.container} + horizontal + onScroll={(e) => this.onScroll(e)} + scrollEventThrottle={60} + style={styles.root} + /> ) } } + +const styles = StyleSheet.create({ + root: { + borderWidth: '1px' + }, + container: { + padding: '10px' + } +}) ``` diff --git a/examples/components/App.js b/examples/components/App.js index cefd87d7..858154d0 100644 --- a/examples/components/App.js +++ b/examples/components/App.js @@ -1,7 +1,7 @@ import GridView from './GridView' import Heading from './Heading' import MediaQueryWidget from './MediaQueryWidget' -import React, { Image, StyleSheet, Text, TextInput, Touchable, View } from '../../src' +import React, { Image, StyleSheet, ScrollView, Text, TextInput, Touchable, View } from '../../src' export default class App extends React.Component { static propTypes = { @@ -9,6 +9,13 @@ export default class App extends React.Component { style: View.propTypes.style } + constructor(...args) { + super(...args) + this.state = { + scrollEnabled: true + } + } + render() { const { mediaQuery } = this.props const rootStyles = { @@ -152,6 +159,52 @@ export default class App extends React.Component { ) })} + + ScrollView + + + Default layout + + console.log('ScrollView.onScroll', e)} + scrollEnabled={this.state.scrollEnabled} + scrollEventThrottle={1} // 1 event per second + style={styles.scrollViewStyle} + > + {Array.from({ length: 50 }).map((item, i) => ( + + {i} + + ))} + + + + Horizontal layout + + console.log('ScrollView.onScroll', e)} + scrollEnabled={this.state.scrollEnabled} + scrollEventThrottle={1} // 1 event per second + style={styles.scrollViewStyle} + > + {Array.from({ length: 50 }).map((item, i) => ( + + {i} + + ))} + + ) } @@ -179,6 +232,9 @@ const styles = StyleSheet.create({ justifyContent: 'center', borderWidth: '1px' }, + horizontalBox: { + width: '50px' + }, boxFull: { width: '100%' }, @@ -194,5 +250,14 @@ const styles = StyleSheet.create({ borderWidth: 1, height: '200px', justifyContent: 'center' + }, + scrollViewContainer: { + height: '200px' + }, + scrollViewStyle: { + borderWidth: '1px' + }, + scrollViewContentContainerStyle: { + padding: '10px' } }) diff --git a/package.json b/package.json index ac5be6ad..17dc89b4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "inline-style-prefixer": "^0.3.3", + "lodash.debounce": "^3.1.1", "react-tappable": "^0.7.1", "react-textarea-autosize": "^3.0.0" }, diff --git a/src/components/ScrollView/ScrollViewStylePropTypes.js b/src/components/ScrollView/ScrollViewStylePropTypes.js new file mode 100644 index 00000000..699761fa --- /dev/null +++ b/src/components/ScrollView/ScrollViewStylePropTypes.js @@ -0,0 +1,4 @@ +import View from '../View' +export default { + ...(View.stylePropTypes) +} diff --git a/src/components/ScrollView/__tests__/index-test.js b/src/components/ScrollView/__tests__/index-test.js index 38743c8e..5f162ce2 100644 --- a/src/components/ScrollView/__tests__/index-test.js +++ b/src/components/ScrollView/__tests__/index-test.js @@ -1 +1,11 @@ /* eslint-env mocha */ + +import * as utils from '../../../modules/specHelpers' + +import ScrollView from '../' + +suite('components/ScrollView', () => { + test('prop "style"', () => { + utils.assertProps.style(ScrollView) + }) +}) diff --git a/src/components/ScrollView/index.js b/src/components/ScrollView/index.js index 2fbd9e40..d4987bfa 100644 --- a/src/components/ScrollView/index.js +++ b/src/components/ScrollView/index.js @@ -1,18 +1,138 @@ +import { pickProps } from '../../modules/filterObjectProps' +import debounce from 'lodash.debounce' import React, { PropTypes } from 'react' +import ScrollViewStylePropTypes from './ScrollViewStylePropTypes' +import StyleSheet from '../../modules/StyleSheet' import View from '../View' +const scrollViewStyleKeys = Object.keys(ScrollViewStylePropTypes) + +const styles = StyleSheet.create({ + initial: { + flexGrow: 1, + flexShrink: 1, + overflow: 'scroll' + }, + initialContentContainer: { + flexGrow: 1, + flexShrink: 1 + }, + row: { + flexDirection: 'row' + } +}) + class ScrollView extends React.Component { static propTypes = { - children: PropTypes.any + children: PropTypes.any, + contentContainerStyle: PropTypes.shape(ScrollViewStylePropTypes), + horizontal: PropTypes.bool, + onScroll: PropTypes.func, + scrollEnabled: PropTypes.bool, + scrollEventThrottle: PropTypes.number, + style: PropTypes.shape(ScrollViewStylePropTypes) } static defaultProps = { - className: '' + contentContainerStyle: styles.initialContentContainer, + horizontal: false, + scrollEnabled: true, + scrollEventThrottle: 0, + style: styles.initial + } + + constructor(...args) { + super(...args) + this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100) + this.state = { + isScrolling: false + } + } + + _onScroll(e) { + const { scrollEventThrottle } = this.props + const { isScrolling, scrollLastTick } = this.state + + // A scroll happened, so the scroll bumps the debounce. + this._debouncedOnScrollEnd(e) + + if (isScrolling) { + // Scroll last tick may have changed, check if we need to notify + if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) { + this._onScrollTick(e) + } + } else { + // Weren't scrolling, so we must have just started + this._onScrollStart(e) + } + } + + _onScrollStart() { + this.setState({ + isScrolling: true, + scrollLastTick: Date.now() + }) + } + + _onScrollTick(e) { + const { onScroll } = this.props + this.setState({ + scrollLastTick: Date.now() + }) + if (onScroll) onScroll(e) + } + + _onScrollEnd(e) { + const { onScroll } = this.props + this.setState({ + isScrolling: false + }) + if (onScroll) onScroll(e) + } + + _shouldEmitScrollEvent(lastTick, eventThrottle) { + const timeSinceLastTick = Date.now() - lastTick + return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle)) + } + + _maybePreventScroll(e) { + const { scrollEnabled } = this.props + if (!scrollEnabled) e.preventDefault() } render() { + const { + children, + contentContainerStyle, + horizontal, + style + } = this.props + + const resolvedStyle = pickProps(style, scrollViewStyleKeys) + const resolvedContentContainerStyle = pickProps(contentContainerStyle, scrollViewStyleKeys) + return ( - + this._onScroll(e)} + onTouchMove={(e) => this._maybePreventScroll(e)} + onWheel={(e) => this._maybePreventScroll(e)} + style={{ + ...styles.initial, + ...resolvedStyle + }} + > + {children ? ( + + ) : null} + ) } }