Compare commits

..

11 Commits

Author SHA1 Message Date
Nicolas Gallagher
011affb110 0.0.44 2016-08-18 16:27:39 -07:00
Nicolas Gallagher
87a4f56a89 [add] Switch component
'Switch' on Web can support custom sizes and colors. To do so,
Web-specific propTypes are introduced: `trackColor`, `thumbColor`,
`activeTrackColor`, and `activeThumbColor`.
2016-08-18 16:25:16 -07:00
Nathan Tran
2f94295739 [fix] TextInput 'keyboardType' propType 2016-08-17 15:51:55 -07:00
Nicolas Gallagher
fdf4a88251 Refactor DOM element helper 2016-08-17 15:46:10 -07:00
Nicolas Gallagher
acc7032d60 0.0.43 2016-08-16 14:05:48 -07:00
Nicolas Gallagher
0fc5644959 Add more details to README
Fix #57
2016-08-16 14:04:22 -07:00
Nicolas Gallagher
be923ec453 [fix] disabled Touchable 2016-08-16 10:29:26 -07:00
Nicolas Gallagher
248c2e1258 [fix] ActivityIndicator animation
Use 'Animated' to animate the 'ActivityIndicator'

Fix #182
2016-08-15 16:58:20 -07:00
Nicolas Gallagher
2e822c068d [fix] Image render thrashing
This patch removes several avoidable uses of `setState`, only
re-rendering the `Image` when necessary.

It also fixes a style prop warning for `resizeMode`, which was not being
removed in development due to the use of `Object.freeze` when styles are
registered.

Close #116
2016-08-15 15:00:47 -07:00
Nicolas Gallagher
fb5406e4ec [change] I18nManager manages global writing direction 2016-08-15 11:36:59 -07:00
Vitor Balocco
638479991e Fix broken link and typo in StyleSheet API docs (#189) 2016-08-14 19:42:42 -07:00
27 changed files with 782 additions and 226 deletions

View File

@@ -7,23 +7,16 @@
Browser support: Chrome, Firefox, Safari >= 7, IE 10, Edge.
[npm-image]: https://badge.fury.io/js/react-native-web.svg
[npm-url]: https://npmjs.org/package/react-native-web
[react-native-url]: https://facebook.github.io/react-native/
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master
[travis-url]: https://travis-ci.org/necolas/react-native-web
## Overview
"React Native for Web" is a project to bring React Native's building blocks and
touch handling to the Web.
React Native unlike React DOM is a comprehensive UI framework for
application developers. React Native's components are higher-level building
blocks than those provided by React DOM. React Native also provides
platform-agnostic JavaScript APIs for animating and styling components,
responding to touch events, and interacting with the host environment.
Bringing the React Native APIs and components to the Web allows React Native
components to be run on the Web platform. But it also solves several problems
facing the React DOM ecosystem: no framework-level animation or styling
solution; difficulty sharing and composing UI components (without pulling in
their build or runtime dependencies); and the lack of high-level base
components.
touch handling to the Web. [Read more](#why).
## Quick start
@@ -40,11 +33,8 @@ using [react-native-web-starter](https://github.com/grabcode/react-native-web-st
## Examples
Demos:
* React Native [examples running on Web](https://necolas.github.io/react-native-web/storybook/)
* [React Native for Web: Playground](http://codepen.io/necolas/pen/PZzwBR).
* [TicTacToe](http://codepen.io/necolas/full/eJaLZd/)
* [2048](http://codepen.io/necolas/full/wMVvxj/)
Sample:
@@ -103,6 +93,7 @@ Exported modules:
* [`Image`](docs/components/Image.md)
* [`ListView`](docs/components/ListView.md)
* [`ScrollView`](docs/components/ScrollView.md)
* [`Switch`](docs/components/Switch.md)
* [`Text`](docs/components/Text.md)
* [`TextInput`](docs/components/TextInput.md)
* [`TouchableHighlight`](http://facebook.github.io/react-native/releases/0.22/docs/touchablehighlight.html) (mirrors React Native)
@@ -124,12 +115,40 @@ Exported modules:
* [`StyleSheet`](docs/apis/StyleSheet.md)
* [`Vibration`](docs/apis/Vibration.md)
<span id="#why"></span>
## Why?
React Native is a comprehensive JavaScript framework for building application
user interfaces. It provides high-level, platform-agnostic components and APIs
e.g., `Text`, `View`, `Touchable*`, `Animated`, `StyleSheet` - that simplify
working with layout, gestures, animations, and styles. The entire React Native
ecosystem can depend on these shared building blocks.
In contrast, the React DOM ecosystem is limited by the lack of a higher-level
framework. At Twitter, we want to seamlessly author and share React component
libraries between different Web applications (with increasing interest from
product teams for multi-platform solutions). This goal draws together multiple,
inter-related problems including: styling, animation, gestures, themes,
viewport adaptation, accessibility, diverse build processes, and RTL layouts.
Almost all these problems are avoided, solved, or can be solved in React
Native. Central to this is React Native's JavaScript style API (not strictly
"CSS-in-JS") which avoids the key [problems with
CSS](https://speakerdeck.com/vjeux/react-css-in-js). By giving up some of the
complexity of CSS it also provides a reliable surface for style composition,
animation, gestures, server-side rendering, RTL layout; and removes the
requirement for CSS-specific build tools.
Bringing the React Native APIs and components to the Web has the added benefit
of allowing teams to explore code-sharing between Native and Web platforms.
## Related projects
* [react-native-web-starter](https://github.com/grabcode/react-native-web-starter)
* [react-native-web-player](https://github.com/dabbott/react-native-web-player)
* [react-web](https://github.com/taobaofed/react-web)
* [react-native-for-web](https://github.com/KodersLab/react-native-for-web)
## License
React Native for Web is [BSD licensed](LICENSE).
[npm-image]: https://badge.fury.io/js/react-native-web.svg
[npm-url]: https://npmjs.org/package/react-native-web
[react-native-url]: https://facebook.github.io/react-native/
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master
[travis-url]: https://travis-ci.org/necolas/react-native-web

View File

@@ -1,8 +1,6 @@
# I18nManager
Control and set the layout and writing direction of the application. You must
set `dir="rtl"` (and should set `lang="${lang}"`) on the root element of your
app.
Control and set the layout and writing direction of the application.
## Properties
@@ -16,12 +14,12 @@ static **allowRTL**(allowRTL: bool)
Allow the application to display in RTL mode.
static **forceRTL**(allowRTL: bool)
static **forceRTL**(forceRTL: bool)
Force the application to display in RTL mode.
static **setRTL**(allowRTL: bool)
static **setPreferredLanguageRTL**(isRTL: bool)
Set the application to display in RTL mode. You will need to determine the
user's preferred locale and if it is an RTL language. (This is best done on the
server as it is notoriously inaccurate to deduce client-side.)
Set the application's preferred writing direction to RTL. You will need to
determine the user's preferred locale server-side (from HTTP headers) and
decide whether it's an RTL language.

View File

@@ -2,8 +2,8 @@
The `StyleSheet` abstraction converts predefined styles to (vendor-prefixed)
CSS without requiring a compile-time step. Some styles cannot be resolved
outside of the render loop and are applied as inline styles. Read more about to
[how style your application](docs/guides/style).
outside of the render loop and are applied as inline styles. Read more about
[how to style your application](../guides/style.md).
## Methods

76
docs/components/Switch.md Normal file
View File

@@ -0,0 +1,76 @@
# Switch
This is a controlled component that requires an `onValueChange` callback that
updates the value prop in order for the component to reflect user actions. If
the `value` prop is not updated, the component will continue to render the
supplied `value` prop instead of the expected result of any user actions.
## Props
[...View props](./View.md)
**disabled**: bool = false
If `true` the user won't be able to interact with the switch.
**onValueChange**: func
Invoked with the new value when the value changes.
**value**: bool = false
The value of the switch. If `true` the switch will be turned on.
(web) **activeThumbColor**: color = #009688
The color of the thumb grip when the switch is turned on.
(web) **activeTrackColor**: color = #A3D3CF
The color of the track when the switch is turned on.
(web) **thumbColor**: color = #FAFAFA
The color of the thumb grip when the switch is turned off.
(web) **trackColor**: color = #939393
The color of the track when the switch is turned off.
## Examples
```js
import React, { Component } from 'react'
import { Switch, View } from 'react-native'
class ColorSwitchExample extends Component {
constructor(props) {
super(props)
this.state = {
colorTrueSwitchIsOn: true,
colorFalseSwitchIsOn: false
}
}
render() {
return (
<View>
<Switch
activeThumbColor='#428BCA'
activeTrackColor='#A0C4E3'
onValueChange={(value) => this.setState({ colorFalseSwitchIsOn: value })}
value={this.state.colorFalseSwitchIsOn}
/>
<Switch
activeThumbColor='#5CB85C'
activeTrackColor='#ADDAAD'
onValueChange={(value) => this.setState({ colorTrueSwitchIsOn: value })}
thumbColor='#EBA9A7'
trackColor='#D9534F'
value={this.state.colorTrueSwitchIsOn}
/>
</View>
)
}
}
```

View File

@@ -406,6 +406,7 @@ const examples = [
);
},
},
/*
{
title: 'Tint Color',
description: 'The `tintColor` style prop changes all the non-alpha ' +
@@ -456,6 +457,7 @@ const examples = [
);
},
},
*/
{
title: 'Resize Mode',
description: 'The `resizeMode` style prop controls how the image is ' +

View File

@@ -0,0 +1,190 @@
import { Platform, Switch, Text, View } from 'react-native'
import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
var BasicSwitchExample = React.createClass({
getInitialState() {
return {
trueSwitchIsOn: true,
falseSwitchIsOn: false,
};
},
render() {
return (
<View>
<Switch
onValueChange={(value) => this.setState({falseSwitchIsOn: value})}
style={{marginBottom: 10}}
value={this.state.falseSwitchIsOn}
/>
<Switch
onValueChange={(value) => this.setState({trueSwitchIsOn: value})}
value={this.state.trueSwitchIsOn}
/>
</View>
);
}
});
var DisabledSwitchExample = React.createClass({
render() {
return (
<View>
<Switch
disabled={true}
style={{marginBottom: 10}}
value={true} />
<Switch
disabled={true}
value={false} />
</View>
);
},
});
var ColorSwitchExample = React.createClass({
getInitialState() {
return {
colorTrueSwitchIsOn: true,
colorFalseSwitchIsOn: false,
};
},
render() {
return (
<View>
<Switch
activeThumbColor="#428bca"
activeTrackColor="#A0C4E3"
onValueChange={(value) => this.setState({colorFalseSwitchIsOn: value})}
style={{marginBottom: 10}}
value={this.state.colorFalseSwitchIsOn}
/>
<Switch
activeThumbColor="#5CB85C"
activeTrackColor="#ADDAAD"
onValueChange={(value) => this.setState({colorTrueSwitchIsOn: value})}
thumbColor="#EBA9A7"
trackColor="#D9534F"
value={this.state.colorTrueSwitchIsOn}
/>
</View>
);
},
});
var EventSwitchExample = React.createClass({
getInitialState() {
return {
eventSwitchIsOn: false,
eventSwitchRegressionIsOn: true,
};
},
render() {
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-around' }}>
<View>
<Switch
onValueChange={(value) => this.setState({eventSwitchIsOn: value})}
style={{marginBottom: 10}}
value={this.state.eventSwitchIsOn} />
<Switch
onValueChange={(value) => this.setState({eventSwitchIsOn: value})}
style={{marginBottom: 10}}
value={this.state.eventSwitchIsOn} />
<Text>{this.state.eventSwitchIsOn ? 'On' : 'Off'}</Text>
</View>
<View>
<Switch
onValueChange={(value) => this.setState({eventSwitchRegressionIsOn: value})}
style={{marginBottom: 10}}
value={this.state.eventSwitchRegressionIsOn} />
<Switch
onValueChange={(value) => this.setState({eventSwitchRegressionIsOn: value})}
style={{marginBottom: 10}}
value={this.state.eventSwitchRegressionIsOn} />
<Text>{this.state.eventSwitchRegressionIsOn ? 'On' : 'Off'}</Text>
</View>
</View>
);
}
});
var SizeSwitchExample = React.createClass({
getInitialState() {
return {
trueSwitchIsOn: true,
falseSwitchIsOn: false,
};
},
render() {
return (
<View>
<Switch
onValueChange={(value) => this.setState({falseSwitchIsOn: value})}
style={{marginBottom: 10, height: '3rem' }}
value={this.state.falseSwitchIsOn}
/>
<Switch
onValueChange={(value) => this.setState({trueSwitchIsOn: value})}
style={{marginBottom: 10, width: 150 }}
value={this.state.trueSwitchIsOn}
/>
</View>
);
}
});
var examples = [
{
title: 'set to true or false',
render(): ReactElement<any> { return <BasicSwitchExample />; }
},
{
title: 'disabled',
render(): ReactElement<any> { return <DisabledSwitchExample />; }
},
{
title: 'change events',
render(): ReactElement<any> { return <EventSwitchExample />; }
},
{
title: 'custom colors',
render(): ReactElement<any> { return <ColorSwitchExample />; }
},
{
title: 'custom size',
render(): ReactElement<any> { return <SizeSwitchExample />; }
},
{
title: 'controlled component',
render(): ReactElement<any> { return <Switch />; }
}
];
examples.forEach((example) => {
storiesOf('<Switch>', module)
.add(example.title, () => example.render())
})

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.0.42",
"version": "0.0.44",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [

View File

@@ -6,32 +6,40 @@ import I18nManager from '..'
suite('apis/I18nManager', () => {
suite('when RTL not enabled', () => {
setup(() => {
I18nManager.setRTL(false)
I18nManager.setPreferredLanguageRTL(false)
})
test('is "false" by default', () => {
assert.equal(I18nManager.isRTL, false)
assert.equal(document.documentElement.getAttribute('dir'), 'ltr')
})
test('is "true" when forced', () => {
I18nManager.forceRTL(true)
assert.equal(I18nManager.isRTL, true)
assert.equal(document.documentElement.getAttribute('dir'), 'rtl')
I18nManager.forceRTL(false)
})
})
suite('when RTL is enabled', () => {
setup(() => {
I18nManager.setRTL(true)
I18nManager.setPreferredLanguageRTL(true)
})
teardown(() => {
I18nManager.setPreferredLanguageRTL(false)
})
test('is "true" by default', () => {
assert.equal(I18nManager.isRTL, true)
assert.equal(document.documentElement.getAttribute('dir'), 'rtl')
})
test('is "false" when not allowed', () => {
I18nManager.allowRTL(false)
assert.equal(I18nManager.isRTL, false)
assert.equal(document.documentElement.getAttribute('dir'), 'ltr')
I18nManager.allowRTL(true)
})
})

View File

@@ -1,3 +1,5 @@
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
type I18nManagerStatus = {
allowRTL: (allowRTL: boolean) => {},
forceRTL: (forceRTL: boolean) => {},
@@ -5,28 +7,38 @@ type I18nManagerStatus = {
isRTL: boolean
}
let isApplicationLanguageRTL = false
let isPreferredLanguageRTL = false
let isRTLAllowed = true
let isRTLForced = false
const isRTL = () => {
if (isRTLForced) {
return true
}
return isRTLAllowed && isPreferredLanguageRTL
}
const onChange = () => {
if (ExecutionEnvironment.canUseDOM) {
document.documentElement.setAttribute('dir', isRTL() ? 'rtl' : 'ltr')
}
}
const I18nManager: I18nManagerStatus = {
allowRTL(bool) {
isRTLAllowed = bool
onChange()
},
forceRTL(bool) {
isRTLForced = bool
onChange()
},
setRTL(bool) {
isApplicationLanguageRTL = bool
setPreferredLanguageRTL(bool) {
isPreferredLanguageRTL = bool
onChange()
},
get isRTL() {
if (isRTLForced) {
return true
}
if (isRTLAllowed && isApplicationLanguageRTL) {
return true
}
return false
return isRTL()
}
}

View File

@@ -1,6 +1,5 @@
import I18nManager from '../I18nManager'
const CSS_UNIT_RE = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?(\w*)/
import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue'
/**
* Map of property names to their BiDi equivalent.
@@ -37,15 +36,7 @@ const PROPERTIES_SWAP_LTR_RTL = {
/**
* Invert the sign of a numeric-like value
*/
const additiveInverse = (value: String | Number) => {
if (typeof value === 'string') {
const number = parseFloat(value, 10) * -1
const unit = getUnit(value)
return `${number}${unit}`
} else if (isNumeric(value)) {
return value * -1
}
}
const additiveInverse = (value: String | Number) => multiplyStyleLengthValue(value, -1)
/**
* BiDi flip the given property.
@@ -65,15 +56,6 @@ const flipTransform = (transform: Object): Object => {
return transform
}
/**
* Get the CSS unit for string values
*/
const getUnit = (str) => str.match(CSS_UNIT_RE)[1]
const isNumeric = (n) => {
return !isNaN(parseFloat(n)) && isFinite(n)
}
const swapLeftRight = (value:String): String => {
return value === 'left' ? 'right' : value === 'right' ? 'left' : value
}

View File

@@ -1,30 +1,20 @@
import Animated from '../../apis/Animated'
import applyNativeMethods from '../../modules/applyNativeMethods'
import Easing from 'animated/lib/Easing'
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 }
]
const opacityInterpolation = { inputRange: [ 0, 1 ], outputRange: [ 0.5, 1 ] }
const scaleInterpolation = { inputRange: [ 0, 1 ], outputRange: [ 0.95, 1 ] }
class ActivityIndicator extends Component {
static propTypes = {
animating: PropTypes.bool,
color: PropTypes.string,
hidesWhenStopped: PropTypes.bool,
size: PropTypes.oneOf(['small', 'large']),
size: PropTypes.oneOf([ 'small', 'large' ]),
style: View.propTypes.style
};
@@ -36,10 +26,14 @@ class ActivityIndicator extends Component {
style: {}
};
componentDidMount() {
if (document.documentElement.animate) {
this._player = ReactDOM.findDOMNode(this._indicatorRef).animate(keyframeEffects, animationEffectTimingProperties)
constructor(props) {
super(props)
this.state = {
animation: new Animated.Value(1)
}
}
componentDidMount() {
this._manageAnimation()
}
@@ -57,37 +51,55 @@ class ActivityIndicator extends Component {
...other
} = this.props
const { animation } = this.state
return (
<View {...other} style={[ styles.container, style ]}>
<View
ref={this._createIndicatorRef}
<Animated.View
style={[
indicatorStyles[size],
hidesWhenStopped && !animating && styles.hidesWhenStopped,
{ borderColor: color }
{
borderColor: color,
opacity: animation.interpolate(opacityInterpolation),
transform: [ { scale: animation.interpolate(scaleInterpolation) } ]
}
]}
/>
</View>
)
}
_createIndicatorRef = (component) => {
this._indicatorRef = component
}
_manageAnimation() {
if (this._player) {
if (this.props.animating) {
this._player.play()
} else {
this._player.cancel()
}
const { animation } = this.state
const cycleAnimation = () => {
Animated.sequence([
Animated.timing(animation, {
duration: 600,
easing: Easing.inOut(Easing.ease),
toValue: 0
}),
Animated.timing(animation, {
duration: 600,
easing: Easing.inOut(Easing.ease),
toValue: 1
})
]).start((event) => {
if (event.finished) {
cycleAnimation()
}
})
}
if (this.props.animating) {
cycleAnimation()
} else {
animation.stopAnimation()
}
}
}
applyNativeMethods(ActivityIndicator)
const styles = StyleSheet.create({
container: {
alignItems: 'center',
@@ -113,4 +125,4 @@ const indicatorStyles = StyleSheet.create({
}
})
module.exports = ActivityIndicator
module.exports = applyNativeMethods(ActivityIndicator)

View File

@@ -1,6 +1,7 @@
/* global window */
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import BaseComponentPropTypes from '../../propTypes/BaseComponentPropTypes'
import createDOMElement from '../../modules/createDOMElement'
import ImageResizeMode from './ImageResizeMode'
import ImageStylePropTypes from './ImageStylePropTypes'
import resolveAssetSource from './resolveAssetSource'
@@ -26,8 +27,7 @@ class Image extends Component {
static displayName = 'Image'
static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessible: createReactDOMComponent.propTypes.accessible,
...BaseComponentPropTypes,
children: PropTypes.any,
defaultSource: ImageSourcePropType,
onError: PropTypes.func,
@@ -37,8 +37,7 @@ class Image extends Component {
onLoadStart: PropTypes.func,
resizeMode: PropTypes.oneOf(['center', 'contain', 'cover', 'none', 'repeat', 'stretch']),
source: ImageSourcePropType,
style: StyleSheetPropType(ImageStylePropTypes),
testID: createReactDOMComponent.propTypes.testID
style: StyleSheetPropType(ImageStylePropTypes)
};
static defaultProps = {
@@ -51,17 +50,18 @@ class Image extends Component {
constructor(props, context) {
super(props, context)
const uri = resolveAssetSource(props.source)
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
this._imageState = uri ? STATUS_PENDING : STATUS_IDLE
this.state = { isLoaded: false }
}
componentDidMount() {
if (this.state.status === STATUS_PENDING) {
if (this._imageState === STATUS_PENDING) {
this._createImageLoader()
}
}
componentDidUpdate() {
if (this.state.status === STATUS_PENDING && !this.image) {
if (this._imageState === STATUS_PENDING && !this.image) {
this._createImageLoader()
}
}
@@ -69,9 +69,7 @@ class Image extends Component {
componentWillReceiveProps(nextProps) {
const nextUri = resolveAssetSource(nextProps.source)
if (resolveAssetSource(this.props.source) !== nextUri) {
this.setState({
status: nextUri ? STATUS_PENDING : STATUS_IDLE
})
this._updateImageState(nextUri ? STATUS_PENDING : STATUS_IDLE)
}
}
@@ -80,6 +78,7 @@ class Image extends Component {
}
render() {
const { isLoaded } = this.state
const {
accessibilityLabel,
accessible,
@@ -90,13 +89,13 @@ class Image extends Component {
testID
} = this.props
const isLoaded = this.state.status === STATUS_LOADED
const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source)
const backgroundImage = displayImage ? `url("${displayImage}")` : null
const style = StyleSheet.flatten(this.props.style)
let style = StyleSheet.flatten(this.props.style)
const resizeMode = this.props.resizeMode || style.resizeMode || ImageResizeMode.cover
// remove resizeMode style, as it is not supported by View
// remove 'resizeMode' style, as it is not supported by View (N.B. styles are frozen in dev)
style = process.env.NODE_ENV !== 'production' ? { ...style } : style
delete style.resizeMode
/**
@@ -121,7 +120,7 @@ class Image extends Component {
]}
testID={testID}
>
{createReactDOMComponent({ component: 'img', src: displayImage, style: styles.img })}
{createDOMElement('img', { src: displayImage, style: styles.img })}
{children ? (
<View children={children} pointerEvents='box-none' style={styles.children} />
) : null}
@@ -153,7 +152,7 @@ class Image extends Component {
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_ERRORED })
this._updateImageState(STATUS_ERRORED)
this._onLoadEnd()
if (onError) onError(event)
}
@@ -163,7 +162,7 @@ class Image extends Component {
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_LOADED })
this._updateImageState(STATUS_LOADED)
if (onLoad) onLoad(event)
this._onLoadEnd()
}
@@ -175,9 +174,17 @@ class Image extends Component {
_onLoadStart() {
const { onLoadStart } = this.props
this.setState({ status: STATUS_LOADING })
this._updateImageState(STATUS_LOADING)
if (onLoadStart) onLoadStart()
}
_updateImageState(status) {
this._imageState = status
const isLoaded = this._imageState === STATUS_LOADED
if (isLoaded !== this.state.isLoaded) {
this.setState({ isLoaded })
}
}
}
applyNativeMethods(Image)

View File

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

View File

@@ -0,0 +1,176 @@
import applyNativeMethods from '../../modules/applyNativeMethods'
import createDOMElement from '../../modules/createDOMElement'
import ColorPropType from '../../propTypes/ColorPropType'
import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue'
import React, { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import UIManager from '../../apis/UIManager'
import View from '../View'
const thumbDefaultBoxShadow = '0px 1px 3px rgba(0,0,0,0.5)'
const thumbFocusedBoxShadow = `${thumbDefaultBoxShadow}, 0 0 0 10px rgba(0,0,0,0.1)`
class Switch extends Component {
static propTypes = {
...View.propTypes,
activeThumbColor: ColorPropType,
activeTrackColor: ColorPropType,
disabled: PropTypes.bool,
onValueChange: PropTypes.func,
thumbColor: ColorPropType,
trackColor: ColorPropType,
value: PropTypes.bool
};
static defaultProps = {
activeThumbColor: '#009688',
activeTrackColor: '#A3D3CF',
disabled: false,
style: {},
thumbColor: '#FAFAFA',
trackColor: '#939393',
value: false
};
blur() {
UIManager.blur(this._checkbox)
}
focus() {
UIManager.focus(this._checkbox)
}
render() {
const {
activeThumbColor,
activeTrackColor,
disabled,
onValueChange, // eslint-disable-line
style,
thumbColor,
trackColor,
value,
// remove any iOS-only props
onTintColor, // eslint-disable-line
thumbTintColor, // eslint-disable-line
tintColor, // eslint-disable-line
...other
} = this.props
const { height: styleHeight, width: styleWidth } = StyleSheet.flatten(style)
const height = styleHeight || 20
const minWidth = multiplyStyleLengthValue(height, 2)
const width = styleWidth > minWidth ? styleWidth : minWidth
const trackBorderRadius = multiplyStyleLengthValue(height, 0.5)
const trackCurrentColor = value ? activeTrackColor : trackColor
const thumbCurrentColor = value ? activeThumbColor : thumbColor
const thumbHeight = height
const thumbWidth = thumbHeight
const rootStyle = [
styles.root,
style,
{ height, width },
disabled && styles.cursorDefault
]
const trackStyle = [
styles.track,
{
backgroundColor: trackCurrentColor,
borderRadius: trackBorderRadius
},
disabled && styles.disabledTrack
]
const thumbStyle = [
styles.thumb,
{
alignSelf: value ? 'flex-end' : 'flex-start',
backgroundColor: thumbCurrentColor,
height: thumbHeight,
width: thumbWidth
},
disabled && styles.disabledThumb
]
const nativeControl = createDOMElement('label', {
children: createDOMElement('input', {
checked: value,
disabled: disabled,
onBlur: this._handleFocusState,
onChange: this._handleChange,
onFocus: this._handleFocusState,
ref: this._setCheckboxRef,
style: styles.cursorInherit,
type: 'checkbox'
}),
pointerEvents: 'none',
style: [ styles.nativeControl, styles.cursorInherit ]
})
return (
<View {...other} style={rootStyle}>
<View style={trackStyle} />
<View ref={this._setThumbRef} style={thumbStyle} />
{nativeControl}
</View>
)
}
_handleChange = (event: Object) => {
const { onValueChange } = this.props
onValueChange && onValueChange(event.nativeEvent.target.checked)
}
_handleFocusState = (event: Object) => {
const isFocused = event.nativeEvent.type === 'focus'
const boxShadow = isFocused ? thumbFocusedBoxShadow : thumbDefaultBoxShadow
this._thumb.setNativeProps({ style: { boxShadow } })
}
_setCheckboxRef = (component) => {
this._checkbox = component
}
_setThumbRef = (component) => {
this._thumb = component
}
}
const styles = StyleSheet.create({
root: {
cursor: 'pointer',
userSelect: 'none'
},
cursorDefault: {
cursor: 'default'
},
cursorInherit: {
cursor: 'inherit'
},
track: {
...StyleSheet.absoluteFillObject,
height: '70%',
margin: 'auto',
transition: 'background-color 0.1s',
width: '90%'
},
disabledTrack: {
backgroundColor: '#D5D5D5'
},
thumb: {
borderRadius: '100%',
boxShadow: thumbDefaultBoxShadow,
transition: 'background-color 0.1s'
},
disabledThumb: {
backgroundColor: '#BDBDBD'
},
nativeControl: {
...StyleSheet.absoluteFillObject,
opacity: 0
}
})
module.exports = applyNativeMethods(Switch)

View File

@@ -1,6 +1,7 @@
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import BaseComponentPropTypes from '../../propTypes/BaseComponentPropTypes'
import createDOMElement from '../../modules/createDOMElement'
import { Component, PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
@@ -10,16 +11,14 @@ class Text extends Component {
static displayName = 'Text'
static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
...BaseComponentPropTypes,
accessibilityRole: PropTypes.oneOf([ 'heading', 'link' ]),
accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any,
numberOfLines: PropTypes.number,
onLayout: PropTypes.func,
onPress: PropTypes.func,
selectable: PropTypes.bool,
style: StyleSheetPropType(TextStylePropTypes),
testID: createReactDOMComponent.propTypes.testID
style: StyleSheetPropType(TextStylePropTypes)
};
static defaultProps = {
@@ -37,9 +36,8 @@ class Text extends Component {
...other
} = this.props
return createReactDOMComponent({
return createDOMElement('span', {
...other,
component: 'span',
onClick: this._onPress,
style: [
styles.initial,

View File

@@ -1,5 +1,5 @@
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import createDOMElement from '../../modules/createDOMElement'
import omit from 'lodash/omit'
import pick from 'lodash/pick'
import React, { Component, PropTypes } from 'react'
@@ -22,7 +22,7 @@ class TextInput extends Component {
clearTextOnFocus: PropTypes.bool,
defaultValue: PropTypes.string,
editable: PropTypes.bool,
keyboardType: PropTypes.oneOf(['default', 'email-address', 'numeric', 'phone-pad', 'url']),
keyboardType: PropTypes.oneOf(['default', 'email-address', 'numeric', 'phone-pad', 'search', 'url', 'web-search']),
maxLength: PropTypes.number,
maxNumberOfLines: PropTypes.number,
multiline: PropTypes.bool,
@@ -135,23 +135,23 @@ class TextInput extends Component {
onFocus: this._handleFocus,
onSelect: onSelectionChange && this._handleSelectionChange,
readOnly: !editable,
ref: 'input',
style: [ styles.input, textStyles, { outline: style.outline } ],
value
}
const propsMultiline = {
...propsCommon,
component: TextareaAutosize,
maxRows: maxNumberOfLines || numberOfLines,
minRows: numberOfLines
}
const propsSingleline = {
...propsCommon,
component: 'input',
type
}
const component = multiline ? TextareaAutosize : 'input'
const props = multiline ? propsMultiline : propsSingleline
const optionalPlaceholder = placeholder && this.state.showPlaceholder && (
@@ -176,7 +176,7 @@ class TextInput extends Component {
testID={testID}
>
<View style={styles.wrapper}>
{createReactDOMComponent({ ...props, ref: 'input' })}
{createDOMElement(component, props)}
{optionalPlaceholder}
</View>
</View>

View File

@@ -31,6 +31,7 @@ var merge = require('../../modules/merge');
type Event = Object;
var DEFAULT_PROPS = {
accessibilityRole: 'button',
activeOpacity: 0.8,
underlayColor: 'black'
};
@@ -234,7 +235,8 @@ var TouchableHighlight = React.createClass({
<View
accessible={true}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={this.props.accessibilityRole || this.props.accessibilityTraits || 'button'}
accessibilityRole={this.props.accessibilityRole}
disabled={this.props.disabled}
hitSlop={this.props.hitSlop}
onKeyDown={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressIn) }}
onKeyPress={(e) => { this._onKeyEnter(e, this.touchableHandlePress) }}
@@ -247,8 +249,12 @@ var TouchableHighlight = React.createClass({
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}
ref={UNDERLAY_REF}
style={[ styles.root, this.state.underlayStyle ]}
tabIndex='0'
style={[
styles.root,
this.props.disabled && styles.disabled,
this.state.underlayStyle
]}
tabIndex={this.props.disabled ? null : '0'}
testID={this.props.testID}>
{React.cloneElement(
React.Children.only(this.props.children),
@@ -274,6 +280,9 @@ var styles = StyleSheet.create({
root: {
cursor: 'pointer',
userSelect: 'none'
},
disabled: {
cursor: 'default'
}
});

View File

@@ -64,6 +64,7 @@ var TouchableOpacity = React.createClass({
getDefaultProps: function() {
return {
accessibilityRole: 'button',
activeOpacity: 0.2,
};
},
@@ -168,8 +169,14 @@ var TouchableOpacity = React.createClass({
<Animated.View
accessible={this.props.accessible !== false}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={this.props.accessibilityRole || 'button'}
style={[styles.root, this.props.style, {opacity: this.state.anim}]}
accessibilityRole={this.props.accessibilityRole}
disabled={this.props.disabled}
style={[
styles.root,
this.props.disabled && styles.disabled,
this.props.style,
{opacity: this.state.anim}
]}
testID={this.props.testID}
onLayout={this.props.onLayout}
hitSlop={this.props.hitSlop}
@@ -182,7 +189,7 @@ var TouchableOpacity = React.createClass({
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}
tabIndex='0'
tabIndex={this.props.disabled ? null : '0'}
>
{this.props.children}
</Animated.View>
@@ -194,6 +201,9 @@ var styles = StyleSheet.create({
root: {
cursor: 'pointer',
userSelect: 'none'
},
disabled: {
cursor: 'default'
}
});

View File

@@ -161,12 +161,22 @@ const TouchableWithoutFeedback = React.createClass({
children.push(Touchable.renderDebugView({color: 'red', hitSlop: this.props.hitSlop}));
}
const style = (Touchable.TOUCH_TARGET_DEBUG && child.type && child.type.displayName === 'Text') ?
[styles.root, child.props.style, {color: 'red'}] :
[styles.root, child.props.style];
[
styles.root,
this.props.disabled && styles.disabled,
child.props.style,
{color: 'red'}
] :
[
styles.root,
this.props.disabled && styles.disabled,
child.props.style
];
return (React: any).cloneElement(child, {
accessible: this.props.accessible !== false,
accessibilityLabel: this.props.accessibilityLabel,
accessibilityRole: this.props.accessibilityRole,
disabled: this.props.disabled,
testID: this.props.testID,
onLayout: this.props.onLayout,
hitSlop: this.props.hitSlop,
@@ -178,7 +188,7 @@ const TouchableWithoutFeedback = React.createClass({
onResponderTerminate: this.touchableHandleResponderTerminate,
style,
children,
tabIndex: '0'
tabIndex: this.props.disabled ? null : '0'
});
}
});
@@ -186,6 +196,9 @@ const TouchableWithoutFeedback = React.createClass({
var styles = StyleSheet.create({
root: {
cursor: 'pointer'
},
disabled: {
cursor: 'default'
}
});

View File

@@ -1,6 +1,7 @@
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent'
import BaseComponentPropTypes from '../../propTypes/BaseComponentPropTypes'
import createDOMElement from '../../modules/createDOMElement'
import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType'
import normalizeNativeEvent from '../../modules/normalizeNativeEvent'
import { Component, PropTypes } from 'react'
@@ -35,10 +36,7 @@ class View extends Component {
static displayName = 'View'
static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion,
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
accessible: createReactDOMComponent.propTypes.accessible,
...BaseComponentPropTypes,
children: PropTypes.any,
collapsable: PropTypes.bool,
hitSlop: EdgeInsetsPropType,
@@ -64,8 +62,7 @@ class View extends Component {
onTouchStart: PropTypes.func,
onTouchStartCapture: PropTypes.func,
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
style: StyleSheetPropType(ViewStylePropTypes),
testID: createReactDOMComponent.propTypes.testID
style: StyleSheetPropType(ViewStylePropTypes)
};
static defaultProps = {
@@ -110,10 +107,10 @@ class View extends Component {
return handlerProps
}, {})
const component = this.context.isInAButtonView ? 'span' : 'div'
const props = {
...other,
...normalizedEventHandlers,
component: this.context.isInAButtonView ? 'span' : 'div',
style: [
styles.initial,
style,
@@ -122,7 +119,7 @@ class View extends Component {
]
}
return createReactDOMComponent(props)
return createDOMElement(component, props)
}
_normalizeEventForHandler(handler, handlerName) {

View File

@@ -26,6 +26,7 @@ import ActivityIndicator from './components/ActivityIndicator'
import Image from './components/Image'
import ListView from './components/ListView'
import ScrollView from './components/ScrollView'
import Switch from './components/Switch'
import Text from './components/Text'
import TextInput from './components/TextInput'
import Touchable from './components/Touchable/Touchable'
@@ -67,6 +68,7 @@ const ReactNative = {
PixelRatio,
Platform,
StyleSheet,
Switch,
UIManager,
Vibration,

View File

@@ -1,61 +1,60 @@
/* eslint-env mocha */
import assert from 'assert'
import createReactDOMComponent from '..'
import createDOMElement from '..'
import { shallow } from 'enzyme'
suite('modules/createReactDOMComponent', () => {
suite('modules/createDOMElement', () => {
test('renders correct DOM element', () => {
let element = shallow(createDOMElement('span'))
assert.equal(element.is('span'), true)
element = shallow(createDOMElement('main'))
assert.equal(element.is('main'), true)
})
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const element = shallow(createReactDOMComponent({ accessibilityLabel }))
const element = shallow(createDOMElement('span', { accessibilityLabel }))
assert.equal(element.prop('aria-label'), accessibilityLabel)
})
test('prop "accessibilityLiveRegion"', () => {
const accessibilityLiveRegion = 'polite'
const element = shallow(createReactDOMComponent({ accessibilityLiveRegion }))
const element = shallow(createDOMElement('span', { accessibilityLiveRegion }))
assert.equal(element.prop('aria-live'), accessibilityLiveRegion)
})
test('prop "accessibilityRole"', () => {
const accessibilityRole = 'banner'
let element = shallow(createReactDOMComponent({ accessibilityRole }))
let element = shallow(createDOMElement('span', { accessibilityRole }))
assert.equal(element.prop('role'), accessibilityRole)
assert.equal(element.is('header'), true)
const button = 'button'
element = shallow(createReactDOMComponent({ accessibilityRole: 'button' }))
element = shallow(createDOMElement('span', { accessibilityRole: 'button' }))
assert.equal(element.prop('type'), button)
assert.equal(element.is('button'), true)
})
test('prop "accessible"', () => {
// accessible (implicit)
let element = shallow(createReactDOMComponent({}))
let element = shallow(createDOMElement('span', {}))
assert.equal(element.prop('aria-hidden'), null)
// accessible (explicit)
element = shallow(createReactDOMComponent({ accessible: true }))
element = shallow(createDOMElement('span', { accessible: true }))
assert.equal(element.prop('aria-hidden'), null)
// not accessible
element = shallow(createReactDOMComponent({ accessible: false }))
element = shallow(createDOMElement('span', { accessible: false }))
assert.equal(element.prop('aria-hidden'), true)
})
test('prop "component"', () => {
const component = 'main'
let element = shallow(createReactDOMComponent({}))
assert.equal(element.is('span'), true, 'Default element must be a "span"')
element = shallow(createReactDOMComponent({ component }))
assert.equal(element.is('main'), true)
})
test('prop "testID"', () => {
// no testID
let element = shallow(createReactDOMComponent({}))
let element = shallow(createDOMElement('span', {}))
assert.equal(element.prop('data-testid'), null)
// with testID
const testID = 'Example.testID'
element = shallow(createReactDOMComponent({ testID }))
element = shallow(createDOMElement('span', { testID }))
assert.equal(element.prop('data-testid'), testID)
})
})

View File

@@ -0,0 +1,48 @@
import React from 'react'
import StyleSheet from '../../apis/StyleSheet'
const roleComponents = {
article: 'article',
banner: 'header',
button: 'button',
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
heading: 'h1',
link: 'a',
list: 'ul',
listitem: 'li',
main: 'main',
navigation: 'nav',
region: 'section'
}
const createDOMElement = (component, rnProps = {}) => {
const {
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible = true,
testID,
type,
...other
} = rnProps
const accessibilityComponent = accessibilityRole && roleComponents[accessibilityRole]
const Component = accessibilityComponent || component
return (
<Component
{...other}
{...StyleSheet.resolve(other)}
aria-hidden={accessible ? null : true}
aria-label={accessibilityLabel}
aria-live={accessibilityLiveRegion}
data-testid={testID}
role={accessibilityRole}
type={accessibilityRole === 'button' ? 'button' : type}
/>
)
}
module.exports = createDOMElement

View File

@@ -1,58 +0,0 @@
import React, { PropTypes } from 'react'
import StyleSheet from '../../apis/StyleSheet'
const roleComponents = {
article: 'article',
banner: 'header',
button: 'button',
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
heading: 'h1',
link: 'a',
list: 'ul',
listitem: 'li',
main: 'main',
navigation: 'nav',
region: 'section'
}
const createReactDOMComponent = ({
accessibilityLabel,
accessibilityLiveRegion,
accessibilityRole,
accessible = true,
component = 'span',
testID,
type,
...other
}) => {
const role = accessibilityRole
const Component = role && roleComponents[role] ? roleComponents[role] : component
return (
<Component
{...other}
{...StyleSheet.resolve(other)}
aria-hidden={accessible ? null : true}
aria-label={accessibilityLabel}
aria-live={accessibilityLiveRegion}
data-testid={testID}
role={role}
type={role === 'button' ? 'button' : type}
/>
)
}
createReactDOMComponent.propTypes = {
accessibilityLabel: PropTypes.string,
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
accessibilityRole: PropTypes.string,
accessible: PropTypes.bool,
component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]),
style: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
testID: PropTypes.string,
type: PropTypes.string
}
module.exports = createReactDOMComponent

View File

@@ -0,0 +1,19 @@
/* eslint-env mocha */
import assert from 'assert'
import multiplyStyleLengthValue from '..'
suite('modules/multiplyStyleLengthValue', () => {
test('number', () => {
assert.equal(multiplyStyleLengthValue(2, -1), -2)
assert.equal(multiplyStyleLengthValue(2, 2), 4)
assert.equal(multiplyStyleLengthValue(2.5, 2), 5)
})
test('length', () => {
assert.equal(multiplyStyleLengthValue('2rem', -1), '-2rem')
assert.equal(multiplyStyleLengthValue('2px', 2), '4px')
assert.equal(multiplyStyleLengthValue('2.5em', 2), '5em')
assert.equal(multiplyStyleLengthValue('2e3px', 2), '4000px')
})
})

View File

@@ -0,0 +1,19 @@
const CSS_UNIT_RE = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?(\w*)/
const getUnit = (str) => str.match(CSS_UNIT_RE)[1]
const isNumeric = (n) => {
return !isNaN(parseFloat(n)) && isFinite(n)
}
const multiplyStyleLengthValue = (value: String | Number, multiple) => {
if (typeof value === 'string') {
const number = parseFloat(value, 10) * multiple
const unit = getUnit(value)
return `${number}${unit}`
} else if (isNumeric(value)) {
return value * multiple
}
}
export default multiplyStyleLengthValue

View File

@@ -0,0 +1,13 @@
import { PropTypes } from 'react'
const { array, bool, number, object, oneOf, oneOfType, string } = PropTypes
const BaseComponentPropTypes = {
accessibilityLabel: string,
accessibilityLiveRegion: oneOf([ 'assertive', 'off', 'polite' ]),
accessibilityRole: string,
accessible: bool,
style: oneOfType([ array, number, object ]),
testID: string
}
module.exports = BaseComponentPropTypes