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