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

@@ -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