diff --git a/README.md b/README.md
index 5da797f0..e7149427 100644
--- a/README.md
+++ b/README.md
@@ -93,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)
diff --git a/docs/components/Switch.md b/docs/components/Switch.md
new file mode 100644
index 00000000..225610a3
--- /dev/null
+++ b/docs/components/Switch.md
@@ -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 (
+
+ this.setState({ colorFalseSwitchIsOn: value })}
+ value={this.state.colorFalseSwitchIsOn}
+ />
+ this.setState({ colorTrueSwitchIsOn: value })}
+ thumbColor='#EBA9A7'
+ trackColor='#D9534F'
+ value={this.state.colorTrueSwitchIsOn}
+ />
+
+ )
+ }
+}
+```
diff --git a/examples/Switch/SwitchExample.js b/examples/Switch/SwitchExample.js
new file mode 100644
index 00000000..9e34af58
--- /dev/null
+++ b/examples/Switch/SwitchExample.js
@@ -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 (
+
+ this.setState({falseSwitchIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.falseSwitchIsOn}
+ />
+ this.setState({trueSwitchIsOn: value})}
+ value={this.state.trueSwitchIsOn}
+ />
+
+ );
+ }
+});
+
+var DisabledSwitchExample = React.createClass({
+ render() {
+ return (
+
+
+
+
+ );
+ },
+});
+
+var ColorSwitchExample = React.createClass({
+ getInitialState() {
+ return {
+ colorTrueSwitchIsOn: true,
+ colorFalseSwitchIsOn: false,
+ };
+ },
+ render() {
+ return (
+
+ this.setState({colorFalseSwitchIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.colorFalseSwitchIsOn}
+ />
+ this.setState({colorTrueSwitchIsOn: value})}
+ thumbColor="#EBA9A7"
+ trackColor="#D9534F"
+ value={this.state.colorTrueSwitchIsOn}
+ />
+
+ );
+ },
+});
+
+var EventSwitchExample = React.createClass({
+ getInitialState() {
+ return {
+ eventSwitchIsOn: false,
+ eventSwitchRegressionIsOn: true,
+ };
+ },
+ render() {
+ return (
+
+
+ this.setState({eventSwitchIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.eventSwitchIsOn} />
+ this.setState({eventSwitchIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.eventSwitchIsOn} />
+ {this.state.eventSwitchIsOn ? 'On' : 'Off'}
+
+
+ this.setState({eventSwitchRegressionIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.eventSwitchRegressionIsOn} />
+ this.setState({eventSwitchRegressionIsOn: value})}
+ style={{marginBottom: 10}}
+ value={this.state.eventSwitchRegressionIsOn} />
+ {this.state.eventSwitchRegressionIsOn ? 'On' : 'Off'}
+
+
+ );
+ }
+});
+
+var SizeSwitchExample = React.createClass({
+ getInitialState() {
+ return {
+ trueSwitchIsOn: true,
+ falseSwitchIsOn: false,
+ };
+ },
+ render() {
+ return (
+
+ this.setState({falseSwitchIsOn: value})}
+ style={{marginBottom: 10, height: '3rem' }}
+ value={this.state.falseSwitchIsOn}
+ />
+ this.setState({trueSwitchIsOn: value})}
+ style={{marginBottom: 10, width: 150 }}
+ value={this.state.trueSwitchIsOn}
+ />
+
+ );
+ }
+});
+
+var examples = [
+ {
+ title: 'set to true or false',
+ render(): ReactElement { return ; }
+ },
+ {
+ title: 'disabled',
+ render(): ReactElement { return ; }
+ },
+ {
+ title: 'change events',
+ render(): ReactElement { return ; }
+ },
+ {
+ title: 'custom colors',
+ render(): ReactElement { return ; }
+ },
+ {
+ title: 'custom size',
+ render(): ReactElement { return ; }
+ },
+ {
+ title: 'controlled component',
+ render(): ReactElement { return ; }
+ }
+];
+
+examples.forEach((example) => {
+ storiesOf('', module)
+ .add(example.title, () => example.render())
+})
diff --git a/src/apis/StyleSheet/i18nStyle.js b/src/apis/StyleSheet/i18nStyle.js
index 3ab00fc7..e43706fc 100644
--- a/src/apis/StyleSheet/i18nStyle.js
+++ b/src/apis/StyleSheet/i18nStyle.js
@@ -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
}
diff --git a/src/components/Switch/__tests__/index-test.js b/src/components/Switch/__tests__/index-test.js
new file mode 100644
index 00000000..3b157742
--- /dev/null
+++ b/src/components/Switch/__tests__/index-test.js
@@ -0,0 +1,5 @@
+/* eslint-env mocha */
+
+suite('components/ActivityIndicator', () => {
+ test.skip('NO TEST COVERAGE', () => {})
+})
diff --git a/src/components/Switch/index.js b/src/components/Switch/index.js
new file mode 100644
index 00000000..9b55a3d9
--- /dev/null
+++ b/src/components/Switch/index.js
@@ -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 (
+
+
+
+ {nativeControl}
+
+ )
+ }
+
+ _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)
diff --git a/src/index.js b/src/index.js
index 8155ca92..02f95b67 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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,
diff --git a/src/modules/multiplyStyleLengthValue/__tests__/index-test.js b/src/modules/multiplyStyleLengthValue/__tests__/index-test.js
new file mode 100644
index 00000000..da204158
--- /dev/null
+++ b/src/modules/multiplyStyleLengthValue/__tests__/index-test.js
@@ -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')
+ })
+})
diff --git a/src/modules/multiplyStyleLengthValue/index.js b/src/modules/multiplyStyleLengthValue/index.js
new file mode 100644
index 00000000..09a3297c
--- /dev/null
+++ b/src/modules/multiplyStyleLengthValue/index.js
@@ -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