Add new components

- ActivityIndicator
- Portal
- StaticContainer
- StaticRenderer
This commit is contained in:
Nicolas Gallagher
2016-02-17 00:20:02 -08:00
parent 1c7fb4cb45
commit 3292ced765
26 changed files with 540 additions and 101 deletions

View File

@@ -0,0 +1,69 @@
# ActivityIndicator
## Props
[...View props](./View.md)
**animating**: bool = true
Whether to show the indicator (true, the default) or hide it (false).
**color**: string = #999999
The foreground color of the spinner (default is gray).
**hidesWhenStopped**: bool = true
Whether the indicator should hide when not animating (true by default).
**size**: oneOf('small, 'large')
Size of the indicator. Small has a height of `20`, large has a height of `36`.
## Examples
```js
import React, { ActivityIndicator, Component, StyleSheet, View } from 'react-native'
class ToggleAnimatingActivityIndicator extends Component {
constructor(props) {
super(props)
this.state = { animating: true }
}
componentDidMount: function() {
this.setToggleTimeout();
}
render() {
return (
<ActivityIndicator
animating={this.state.animating}
size="large"
style={[styles.centering, { height: 80 }]}
/>
);
}
_setToggleTimeout() {
setTimeout(() => {
this.setState({ animating: !this.state.animating })
this._setToggleTimeout()
}, 1200)
}
})
const styles = StyleSheet.create({
centering: {
alignItems: 'center',
justifyContent: 'center'
},
gray: {
backgroundColor: '#cccccc'
},
horizontal: {
flexDirection: 'row',
justifyContent: 'space-around'
}
})
```

View File

@@ -76,7 +76,7 @@ Used to locate a view in end-to-end tests.
```js
import placeholderAvatar from './placeholderAvatar.png'
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native-web'
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native'
export default class ImageExample extends Component {
constructor(props, context) {

View File

@@ -15,7 +15,7 @@ Content to display over the image.
## Examples
```js
import React, { Component, ListView, PropTypes } from 'react-native-web'
import React, { Component, ListView, PropTypes } from 'react-native'
export default class ListViewExample extends Component {
static propTypes = {}

67
docs/components/Portal.md Normal file
View File

@@ -0,0 +1,67 @@
# Portal
`Portal` is used to render modal content on top of everything else in the
application. It passes modal views all the way up to the root element created
by `AppRegistry.runApplication`.
There can only be one `Portal` instance rendered in an application, and this
instance is controlled by React Native for Web.
## Methods
static **allocateTag**()
Creates a new unique tag for the modal that your component is rendering. A
good place to allocate a tag is in `componentWillMount`. Returns a string. See
`showModal` and `closeModal`.
static **closeModal**(tag: string)
Remove a modal from the collection of modals to be rendered. The `tag` must
exactly match the tag previous passed to `showModal` to identify the React
component.
static **getOpenModals**()
Get an array of all the open modals, as identified by their tag string.
static **showModal**(tag: string, component: any)
Render a new modal. The `tag` must be unique as it is used to identify the
React component to render. This same tag can later be used in `closeModal`.
## Examples
```js
import React, { Portal, Text, Touchable } from 'react-native'
export default class PortalExample extends Component {
componentWillMount() {
this._portalTag = Portal.allocateTag()
}
render() {
return (
<Touchable onPress={this._handlePortalOpen.bind(this)}>
<Text>Open portal</Text>
</Touchable>
)
}
_handlePortalClose(e) {
Portal.closeModal(this._portalTag)
}
_handlePortalOpen(e) {
Portal.showModal(this._portalTag, this._renderPortalContent())
}
_renderPortalContent() {
return (
<Touchable onPress={this._handlePortalClose.bind(this)}>
<Text>Close portal</Text>
</Touchable>
)
}
}
```

View File

@@ -43,7 +43,7 @@ time the view is scrolled.
## Examples
```js
import React, { Component, ScrollView, StyleSheet } from 'react-native-web'
import React, { Component, ScrollView, StyleSheet } from 'react-native'
import Item from './Item'
export default class ScrollViewExample extends Component {

View File

@@ -74,7 +74,7 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native-web'
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native'
export default class PrettyText extends Component {
static propTypes = {

View File

@@ -150,7 +150,7 @@ user edits to the value set `editable={false}`.
## Examples
```js
import React, { Component, StyleSheet, TextInput } from 'react-native-web'
import React, { Component, StyleSheet, TextInput } from 'react-native'
export default class TextInputExample extends Component {
constructor(props, context) {

View File

@@ -84,7 +84,7 @@ Delay in ms, from the release of the touch, before `onPressOut` is called.
## Examples
```js
import React, { Component, PropTypes, Touchable } from 'react-native-web'
import React, { Component, PropTypes, Touchable } from 'react-native'
export default class Example extends Component {
static propTypes = {}

View File

@@ -168,7 +168,7 @@ Used to locate this view in end-to-end tests.
## Examples
```js
import React, { Component, PropTypes, StyleSheet, View } from 'react-native-web'
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
export default class ViewExample extends Component {
render() {

View File

@@ -20,34 +20,4 @@ suite('ReactNativeWeb', () => {
assert.ok(React.renderToStaticMarkup)
})
})
suite('render methods', () => {
const id = 'test'
let div
setup(() => {
div = document.createElement('div')
div.id = id
document.body.appendChild(div)
})
teardown(() => {
document.body.removeChild(div)
})
test('"render" creates style sheet', () => {
React.render(<div />, div)
assert.ok(document.getElementById('react-stylesheet'))
})
test('"renderToString" creates style sheet', () => {
const result = React.renderToString(<div />)
assert.ok(result.indexOf('react-stylesheet') > -1)
})
test('"renderToStaticMarkup" creates style sheet', () => {
const result = React.renderToStaticMarkup(<div />)
assert.ok(result.indexOf('react-stylesheet') > -1)
})
})
})

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('components/ActivityIndicator', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,108 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
const GRAY = '#999999'
const animationEffectTimingProperties = {
direction: 'alternate',
duration: 700,
easing: 'ease-in-out',
fill: 'forwards',
iterations: Infinity
}
const keyframeEffects = [
{ transform: 'scale(1)', opacity: 1.0 },
{ transform: 'scale(0.95)', opacity: 0.5 }
]
export default class ActivityIndicator extends Component {
static propTypes = {
animating: PropTypes.bool,
color: PropTypes.string,
hidesWhenStopped: PropTypes.bool,
size: PropTypes.oneOf(['small', 'large']),
style: PropTypes.object
};
static defaultProps = {
animating: true,
color: GRAY,
hidesWhenStopped: true,
size: 'small',
style: {}
};
componentDidMount() {
if (document.documentElement.animate) {
this._player = ReactDOM.findDOMNode(this._indicatorRef).animate(keyframeEffects, animationEffectTimingProperties)
}
if (this.props.animating) {
this._player.play()
} else {
this._player.cancel()
}
}
componentDidUpdate() {
if (this.props.animating) {
this._player.play()
} else {
this._player.cancel()
}
}
render() {
const {
animating,
color,
hidesWhenStopped,
size,
style,
...other
} = this.props
return (
<View {...other} style={{ ...styles.container, ...style }}>
<View
ref={(c) => { this._indicatorRef = c }}
style={{
...styles.indicator[size],
...(hidesWhenStopped && !animating && styles.hidesWhenStopped),
borderColor: color
}}
/>
</View>
)
}
}
const indicatorStyle = StyleSheet.create({
borderRadius: 100,
borderWidth: 3
})
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center'
},
hidesWhenStopped: {
visibility: 'hidden'
},
indicator: {
small: {
...indicatorStyle,
width: 20,
height: 20
},
large: {
...indicatorStyle,
borderWidth: 4,
width: 36,
height: 36
}
}
})

View File

@@ -1,4 +1,4 @@
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import StylePropTypes from '../../apis/StyleSheet/StylePropTypes'
import StyleSheet from '../../apis/StyleSheet'
@@ -18,7 +18,7 @@ const roleComponents = {
region: 'section'
}
class CoreComponent extends React.Component {
export default class CoreComponent extends Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
@@ -66,5 +66,3 @@ class CoreComponent extends React.Component {
)
}
}
export default CoreComponent

View File

@@ -3,7 +3,7 @@ import { pickProps } from '../../modules/filterObjectProps'
import StyleSheet from '../../apis/StyleSheet'
import CoreComponent from '../CoreComponent'
import ImageStylePropTypes from './ImageStylePropTypes'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
@@ -52,7 +52,7 @@ const styles = StyleSheet.create({
}
})
class Image extends React.Component {
export default class Image extends Component {
constructor(props, context) {
super(props, context)
const { uri } = props.source
@@ -209,5 +209,3 @@ class Image extends React.Component {
)
}
}
export default Image

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import ScrollView from '../ScrollView'
class ListView extends React.Component {
export default class ListView extends Component {
static propTypes = {
children: PropTypes.any,
style: PropTypes.style
@@ -17,5 +17,3 @@ class ListView extends React.Component {
)
}
}
export default ListView

View File

@@ -0,0 +1,156 @@
/**
* Copyright 2015-present, Nicolas Gallagher
* Copyright 2004-present, Facebook Inc.
* All Rights Reserved.
*
* @flow
*/
import invariant from 'invariant'
import Platform from '../../apis/Platform'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
let _portalRef: any
// unique identifiers for modals
let lastUsedTag = 0
/**
* A container that renders all the modals on top of everything else in the application.
*/
export default class Portal extends Component {
static propTypes = {
onModalVisibilityChanged: PropTypes.func.isRequired
};
/**
* Create a new unique tag.
*/
static allocateTag(): string {
return `__modal_${++lastUsedTag}`
}
/**
* Render a new modal.
*/
static showModal(tag: string, component: any) {
if (!_portalRef) {
console.error('Calling showModal but no "Portal" has been rendered.')
return
}
_portalRef._showModal(tag, component)
}
/**
* Remove a modal from the collection of modals to be rendered.
*/
static closeModal(tag: string) {
if (!_portalRef) {
console.error('Calling closeModal but no "Portal" has been rendered.')
return
}
_portalRef._closeModal(tag)
}
/**
* Get an array of all the open modals, as identified by their tag string.
*/
static getOpenModals(): Array<string> {
if (!_portalRef) {
console.error('Calling getOpenModals but no "Portal" has been rendered.')
return []
}
return _portalRef._getOpenModals()
}
static notifyAccessibilityService() {
if (!_portalRef) {
console.error('Calling closeModal but no "Portal" has been rendered.')
return
}
_portalRef._notifyAccessibilityService()
}
constructor(props) {
super(props)
this.state = { modals: {} }
this._closeModal = this._closeModal.bind(this)
this._getOpenModals = this._getOpenModals.bind(this)
this._showModal = this._showModal.bind(this)
}
render() {
invariant(
_portalRef === this || _portalRef === undefined,
'More than one Portal instance detected. Never use <Portal> in your code.'
)
_portalRef = this
if (!this.state.modals) { return null }
const modals = []
for (const tag in this.state.modals) {
modals.push(this.state.modals[tag])
}
if (modals.length === 0) { return null }
return (
<View style={styles.root}>
{modals}
</View>
)
}
_closeModal(tag: string) {
if (!this.state.modals.hasOwnProperty(tag)) {
return
}
// We are about to close last modal, so Portal will disappear.
// Let's enable accessibility for application view.
if (this._getOpenModals().length === 1) {
this.props.onModalVisibilityChanged(false)
}
// This way state is chained through multiple calls to
// _showModal, _closeModal correctly.
this.setState((state) => {
const modals = state.modals
delete modals[tag]
return { modals }
})
}
_getOpenModals(): Array<string> {
return Object.keys(this.state.modals)
}
_notifyAccessibilityService() {
if (Platform.OS === 'web') {
// We need to send accessibility event in a new batch, as otherwise
// TextViews have no text set at the moment of populating event.
}
}
_showModal(tag: string, component: any) {
// We are about to open first modal, so Portal will appear.
// Let's disable accessibility for background view on Android.
if (this._getOpenModals().length === 0) {
this.props.onModalVisibilityChanged(true)
}
// This way state is chained through multiple calls to
// _showModal, _closeModal correctly.
this.setState((state) => {
const modals = state.modals
modals[tag] = component
return { modals }
})
}
}
const styles = StyleSheet.create({
root: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0
}
})

View File

@@ -1,6 +1,6 @@
import { pickProps } from '../../modules/filterObjectProps'
import debounce from 'lodash.debounce'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import ScrollViewStylePropTypes from './ScrollViewStylePropTypes'
import StyleSheet from '../../apis/StyleSheet'
import View from '../View'
@@ -22,7 +22,7 @@ const styles = StyleSheet.create({
}
})
class ScrollView extends React.Component {
export default class ScrollView extends Component {
static propTypes = {
children: PropTypes.any,
contentContainerStyle: PropTypes.shape(ScrollViewStylePropTypes),
@@ -136,5 +136,3 @@ class ScrollView extends React.Component {
)
}
}
export default ScrollView

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('components/StaticContainer', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import React, { Component, PropTypes } from 'react'
/**
* Renders static content efficiently by allowing React to short-circuit the
* reconciliation process. This component should be used when you know that a
* subtree of components will never need to be updated.
*
* const someValue = ...; // We know for certain this value will never change.
* return (
* <StaticContainer>
* <MyComponent value={someValue} />
* </StaticContainer>
* );
*
* Typically, you will not need to use this component and should opt for normal
* React reconciliation.
*/
export default class StaticContainer extends Component {
static propTypes = {
children: PropTypes.any.isRequired,
shouldUpdate: PropTypes.bool.isRequired
};
shouldComponentUpdate(nextProps: { shouldUpdate: boolean }): boolean {
return nextProps.shouldUpdate
}
render() {
const child = this.props.children
return (child === null || child === false) ? null : React.Children.only(child)
}
}

View File

@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('components/StaticRenderer', () => {
test.skip('NO TEST COVERAGE', () => {})
})

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import { Component, PropTypes } from 'react'
/**
* Renders static content efficiently by allowing React to short-circuit the
* reconciliation process. This component should be used when you know that a
* subtree of components will never need to be updated.
*
* const someValue = ...; // We know for certain this value will never change.
* return (
* <StaticRenderer render={() => <MyComponent value={someValue} />} />
* );
*
* Typically, you will not need to use this component and should opt for normal
* React reconciliation.
*/
export default class StaticRenderer extends Component {
static propTypes = {
render: PropTypes.func.isRequired,
shouldUpdate: PropTypes.bool.isRequired
};
shouldComponentUpdate(nextProps: { shouldUpdate: boolean }): boolean {
return nextProps.shouldUpdate
}
render() {
return this.props.render()
}
}

View File

@@ -1,6 +1,6 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import TextStylePropTypes from './TextStylePropTypes'
@@ -24,7 +24,7 @@ const styles = StyleSheet.create({
}
})
class Text extends React.Component {
export default class Text extends Component {
static propTypes = {
_className: PropTypes.string, // escape-hatch for code migrations
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
@@ -76,5 +76,3 @@ class Text extends React.Component {
)
}
}
export default Text

View File

@@ -1,6 +1,6 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import StyleSheet from '../../apis/StyleSheet'
import Text from '../Text'
@@ -39,7 +39,7 @@ const styles = StyleSheet.create({
}
})
class TextInput extends React.Component {
export default class TextInput extends Component {
constructor(props, context) {
super(props, context)
this.state = { showPlaceholder: !props.value && !props.defaultValue }
@@ -221,5 +221,3 @@ class TextInput extends React.Component {
)
}
}
export default TextInput

View File

@@ -1,4 +1,4 @@
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import Tappable from 'react-tappable'
import View from '../View'
import StyleSheet from '../../apis/StyleSheet'
@@ -11,7 +11,7 @@ const styles = StyleSheet.create({
}
})
class Touchable extends React.Component {
export default class Touchable extends Component {
constructor(props, context) {
super(props, context)
this.state = {
@@ -130,5 +130,3 @@ class Touchable extends React.Component {
)
}
}
export default Touchable

View File

@@ -1,6 +1,6 @@
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import ViewStylePropTypes from './ViewStylePropTypes'
@@ -30,7 +30,7 @@ const styles = StyleSheet.create({
}
})
class View extends React.Component {
export default class View extends Component {
static propTypes = {
_className: PropTypes.string, // escape-hatch for code migrations
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
@@ -76,5 +76,3 @@ class View extends React.Component {
)
}
}
export default View

View File

@@ -2,63 +2,55 @@ import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
// api
import StyleSheet from './modules/StyleSheet'
// apis
import AppRegistry from './apis/AppRegistry'
import AppState from './apis/AppState'
import AsyncStorage from './apis/AsyncStorage'
import Dimensions from './apis/Dimensions'
import NetInfo from './apis/NetInfo'
import PixelRatio from './apis/PixelRatio'
import Platform from './apis/Platform'
import StyleSheet from './apis/StyleSheet'
// components
import ActivityIndicator from './components/ActivityIndicator'
import Image from './components/Image'
import ListView from './components/ListView'
import Portal from './components/Portal'
import ScrollView from './components/ScrollView'
import Text from './components/Text'
import TextInput from './components/TextInput'
import Touchable from './components/Touchable'
import View from './components/View'
const renderStyle = () => {
return `<style id='react-stylesheet'>${StyleSheet._renderToString()}</style>`
}
const render = (element, container, callback) => {
const styleElement = document.getElementById('react-stylesheet')
if (!styleElement) {
const style = renderStyle()
container.insertAdjacentHTML('beforebegin', style)
}
return ReactDOM.render(element, container, callback)
}
const renderToString = (element) => {
const style = renderStyle()
const html = ReactDOMServer.renderToString(element)
return `${style}\n${html}`
}
const renderToStaticMarkup = (element) => {
const style = renderStyle()
const html = ReactDOMServer.renderToStaticMarkup(element)
return `${style}\n${html}`
}
const ReactNative = {
// apis
AppRegistry,
AppState,
AsyncStorage,
Dimensions,
NetInfo,
PixelRatio,
Platform,
StyleSheet,
// components
ActivityIndicator,
Image,
ListView,
Portal,
ScrollView,
Text,
TextInput,
Touchable,
TouchableHighlight: Touchable,
TouchableOpacity: Touchable,
TouchableWithoutFeedback: Touchable,
View,
// React
...React,
...ReactDOM,
...ReactDOMServer,
render,
renderToString,
renderToStaticMarkup
...ReactDOMServer
}
module.exports = ReactNative