[add] initial ScrollView

Supports the following props: `children`, `contentContainerStyle`,
`horizontal`, `onScroll`, `scrollEnabled`, `scrollEventThrottle`, and
`style`.

Fix #6
This commit is contained in:
Tom Ashworth
2015-10-20 15:57:51 -07:00
committed by Nicolas Gallagher
parent a1664927ce
commit 894fd0362d
6 changed files with 244 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
# ScrollView # 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 ## Props
@@ -11,27 +12,29 @@ Child content.
**contentContainerStyle**: style **contentContainerStyle**: style
These styles will be applied to the scroll view content container which wraps 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 **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 **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 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 events per seconds). A higher number yields better accuracy for code that is
tracking the scroll position, but can lead to scroll performance problems. tracking the scroll position, but can lead to scroll performance problems. The
Default: `0` (the scroll event will be sent only once each time the view is default value is `0`, which means the scroll event will be sent only once each
scrolled.) time the view is scrolled.
**style**: style **style**: style
@@ -40,20 +43,42 @@ scrolled.)
## Examples ## Examples
```js ```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 { export default class App extends React.Component {
static propTypes = { 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() { render() {
return ( return (
<ScrollView
children={this.state.items.map((item) => <Item {...item} />)}
contentContainerStyle={styles.container}
horizontal
onScroll={(e) => this.onScroll(e)}
scrollEventThrottle={60}
style={styles.root}
/>
) )
} }
} }
const styles = StyleSheet.create({
root: {
borderWidth: '1px'
},
container: {
padding: '10px'
}
})
``` ```

View File

@@ -1,7 +1,7 @@
import GridView from './GridView' import GridView from './GridView'
import Heading from './Heading' import Heading from './Heading'
import MediaQueryWidget from './MediaQueryWidget' 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 { export default class App extends React.Component {
static propTypes = { static propTypes = {
@@ -9,6 +9,13 @@ export default class App extends React.Component {
style: View.propTypes.style style: View.propTypes.style
} }
constructor(...args) {
super(...args)
this.state = {
scrollEnabled: true
}
}
render() { render() {
const { mediaQuery } = this.props const { mediaQuery } = this.props
const rootStyles = { const rootStyles = {
@@ -152,6 +159,52 @@ export default class App extends React.Component {
) )
})} })}
</GridView> </GridView>
<Heading size='large'>ScrollView</Heading>
<label>
<input
checked={this.state.scrollEnabled}
onChange={() => this.setState({
scrollEnabled: !this.state.scrollEnabled
})}
type='checkbox'
/> Enable scroll
</label>
<Heading>Default layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
onScroll={e => 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) => (
<View key={i} style={styles.box}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
<Heading>Horizontal layout</Heading>
<View style={styles.scrollViewContainer}>
<ScrollView
contentContainerStyle={styles.scrollViewContentContainerStyle}
horizontal
onScroll={e => 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) => (
<View key={i} style={{...styles.box, ...styles.horizontalBox}}>
<Text>{i}</Text>
</View>
))}
</ScrollView>
</View>
</View> </View>
) )
} }
@@ -179,6 +232,9 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
borderWidth: '1px' borderWidth: '1px'
}, },
horizontalBox: {
width: '50px'
},
boxFull: { boxFull: {
width: '100%' width: '100%'
}, },
@@ -194,5 +250,14 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
height: '200px', height: '200px',
justifyContent: 'center' justifyContent: 'center'
},
scrollViewContainer: {
height: '200px'
},
scrollViewStyle: {
borderWidth: '1px'
},
scrollViewContentContainerStyle: {
padding: '10px'
} }
}) })

View File

@@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"inline-style-prefixer": "^0.3.3", "inline-style-prefixer": "^0.3.3",
"lodash.debounce": "^3.1.1",
"react-tappable": "^0.7.1", "react-tappable": "^0.7.1",
"react-textarea-autosize": "^3.0.0" "react-textarea-autosize": "^3.0.0"
}, },

View File

@@ -0,0 +1,4 @@
import View from '../View'
export default {
...(View.stylePropTypes)
}

View File

@@ -1 +1,11 @@
/* eslint-env mocha */ /* eslint-env mocha */
import * as utils from '../../../modules/specHelpers'
import ScrollView from '../'
suite('components/ScrollView', () => {
test('prop "style"', () => {
utils.assertProps.style(ScrollView)
})
})

View File

@@ -1,18 +1,138 @@
import { pickProps } from '../../modules/filterObjectProps'
import debounce from 'lodash.debounce'
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ScrollViewStylePropTypes from './ScrollViewStylePropTypes'
import StyleSheet from '../../modules/StyleSheet'
import View from '../View' 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 { class ScrollView extends React.Component {
static propTypes = { 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 = { 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() { render() {
const {
children,
contentContainerStyle,
horizontal,
style
} = this.props
const resolvedStyle = pickProps(style, scrollViewStyleKeys)
const resolvedContentContainerStyle = pickProps(contentContainerStyle, scrollViewStyleKeys)
return ( return (
<View {...this.props} /> <View
_className='ScrollView'
onScroll={(e) => this._onScroll(e)}
onTouchMove={(e) => this._maybePreventScroll(e)}
onWheel={(e) => this._maybePreventScroll(e)}
style={{
...styles.initial,
...resolvedStyle
}}
>
{children ? (
<View
children={children}
style={{
...styles.initialContentContainer,
...resolvedContentContainerStyle,
...(horizontal && styles.row)
}}
/>
) : null}
</View>
) )
} }
} }