├── .gitignore ├── LICENSE ├── README.md ├── TouchableSetActive.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeff Stout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-TouchableSetActive 2 | Touchable component for [React Native](https://github.com/facebook/react-native) that enables more advanced styling by setting an active state. Most useful for building your own touchable/button components on top of. 3 | 4 | ## Install 5 | ```sh 6 | $ npm install react-native-touchable-set-active --save 7 | ``` 8 | 9 | ## Usage 10 | First, require the `TouchableSetActive` component in your project. 11 | ```javascript 12 | var TouchableSetActive = require('react-native-touchable-set-active'); 13 | ``` 14 | 15 | There are two different ways you can use this component. They both involve passing a value to the `setActive` property on `TouchableSetActive`. 16 | 17 | ###setActive={this} 18 | The simplest implementation is achieved by just passing `this`. The component will set an `active` state (using `this.setState`) on the parent component. To toggle a style, set one conditionally in the style property that is dependent on `this.state.active`. 19 | 20 | ```javascript 21 | class ExampleButton extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = {}; 25 | } 26 | render() { 27 | return( 28 | 35 | 38 | Example Button 39 | 40 | 41 | ); 42 | } 43 | } 44 | ``` 45 | 46 | ###setActive={*function*} 47 | Instead of passing `this`, you can provide a function. It will be called whenever the component's active state changes, with a boolean value representing the active state as the only argument. 48 | ```javascript 49 | class ExampleButton extends React.Component { 50 | constructor(props) { 51 | super(props); 52 | this.state = { 53 | active: false, 54 | }; 55 | } 56 | render() { 57 | return( 58 | { 60 | this.setState({active: isActive}); 61 | }} 62 | style={[ 63 | !this.state.active && styles.inactiveButton, 64 | this.state.active && styles.activeButton, 65 | ]} 66 | > 67 | 70 | Example Button 71 | 72 | 73 | ); 74 | } 75 | } 76 | ``` 77 | 78 | ## Additional Props 79 | `TouchableSetActive` is just like any other [Touchable component](https://facebook.github.io/react-native/docs/touchablewithoutfeedback.html) in that it supports the following properties: 80 | ```javascript 81 | onPressIn 82 | onPressOut 83 | onPress 84 | onLongPress 85 | ``` 86 | 87 | It also supports touchable delay properties that are (*hopefully*) landing in React Native core soon (via [\#1255](https://github.com/facebook/react-native/pull/1255)): 88 | ```javascript 89 | /** 90 | * Delay in ms, from the release of the touch, before onPress is called. 91 | */ 92 | delayOnPress: React.PropTypes.number, 93 | /** 94 | * Delay in ms, from the start of the touch, before onPressIn is called. 95 | */ 96 | delayOnPressIn: React.PropTypes.number, 97 | /** 98 | * Delay in ms, from the release of the touch, before onPressOut is called. 99 | */ 100 | delayOnPressOut: React.PropTypes.number, 101 | /** 102 | * Delay in ms, from onPressIn, before onLongPress is called. 103 | */ 104 | delayOnLongPress: React.PropTypes.number, 105 | ``` 106 | *Support for `delayOnLongPress` is dependent on some underlying changes to the `Touchable` library. Unfortunately, it won't be available until those changes are committed. If you really need it now, take a look at [the PR](https://github.com/facebook/react-native/pull/1255) or [my branch](https://github.com/jmstout/react-native/tree/touchable-custom-delays) which adds this functionality. It should also be noted that until this PR lands, `delayOnPressIn` can be set to a maximum of `249` ms before throwing an error.* 107 | 108 | Additionally, the props `delayActive` and `delayInactive` can be used to decouple the active state from the press events. 109 | ```javascript 110 | /** 111 | * Delay in ms, from the start of the touch, before the active state is shown. 112 | */ 113 | delayActive: React.PropTypes.number, 114 | /** 115 | * Delay in ms, from the start of the active state, before it becomes inactive. 116 | */ 117 | delayInactive: React.PropTypes.number, 118 | ``` 119 | 120 | ## License 121 | MIT © [Jeff Stout](http://jmstout.com) -------------------------------------------------------------------------------- /TouchableSetActive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Jeff Stout 3 | * MIT License 4 | * 5 | * The TouchableSetActive component was adapted from a fork of React Native's 6 | * original Touchable components. Therefore, the following license notice also 7 | * applies to parts of this source code. 8 | * See http://github.com/facebook/react-native for the files it refers to. 9 | * 10 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 11 | * 12 | * Copyright (c) 2015-present, Facebook, Inc. 13 | * All rights reserved. 14 | * 15 | * This source code is licensed under the BSD-style license found in the 16 | * LICENSE file in the root directory of this source tree. An additional grant 17 | * of patent rights can be found in the PATENTS file in the same directory. 18 | */ 19 | 'use strict'; 20 | 21 | var React = require('react-native'); 22 | var TimerMixin = require('react-timer-mixin'); 23 | var Touchable = require('Touchable'); 24 | var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); 25 | var View = require('View'); 26 | 27 | var merge = require('merge'); 28 | 29 | var DEFAULT_HIDE_MS = 150; 30 | var DEFAULT_ACTIVE_MS = 120; 31 | var DEFAULT_LONG_PRESS_MS = 400; 32 | 33 | var DEFAULT_PROPS = { 34 | delayOnPressOut: DEFAULT_HIDE_MS, 35 | }; 36 | 37 | var TouchableSetActive = React.createClass({ 38 | propTypes: { 39 | ...TouchableWithoutFeedback.propTypes, 40 | style: View.propTypes.style, 41 | /** 42 | * Required property used for setting the active state. 43 | * Accepts a React component class (this) or function. 44 | */ 45 | setActive: function(props, propName, componentName) { 46 | if (!props[propName] || typeof(props[propName]) !== 'function' && 47 | !React.addons.TestUtils.isCompositeComponent(props[propName])) { 48 | return new Error( 49 | componentName + ': prop type `' + propName + '` is ' + 50 | (props[propName] ? 'invalid' : 'missing') + 51 | '; it must be a React component class (this) or function.' 52 | ); 53 | } 54 | }, 55 | /** 56 | * Delay in ms, from the start of the touch, before the active state is shown. 57 | */ 58 | delayActive: React.PropTypes.number, 59 | /** 60 | * Delay in ms, from the start of the active state, before it becomes inactive. 61 | */ 62 | delayInactive: React.PropTypes.number, 63 | 64 | // Note: remove the following delay props when they land in core 65 | 66 | /** 67 | * Delay in ms, from the release of the touch, before onPress is called. 68 | */ 69 | delayOnPress: React.PropTypes.number, 70 | /** 71 | * Delay in ms, from the start of the touch, before onPressIn is called. 72 | */ 73 | delayOnPressIn: React.PropTypes.number, 74 | /** 75 | * Delay in ms, from the release of the touch, before onPressOut is called. 76 | */ 77 | delayOnPressOut: React.PropTypes.number, 78 | /** 79 | * Delay in ms, from onPressIn, before onLongPress is called. 80 | */ 81 | delayOnLongPress: React.PropTypes.number, 82 | }, 83 | 84 | mixins: [TimerMixin, Touchable.Mixin], 85 | 86 | getDefaultProps: () => DEFAULT_PROPS, 87 | 88 | _computeState: function(props) { 89 | return { 90 | setActive: props.setActive, 91 | componentStyle: props.style, 92 | }; 93 | }, 94 | 95 | getInitialState: function() { 96 | return merge( 97 | this.touchableGetInitialState(), this._computeState(this.props) 98 | ); 99 | }, 100 | 101 | componentDidMount: function() { 102 | this._activeType = this._getActiveType(); 103 | this._delayActive = !!(this.props.delayActive || 104 | this.props.delayActive === 0); 105 | this._delayInactive = !!(this.props.delayInactive || 106 | this.props.delayInactive === 0); 107 | }, 108 | 109 | componentWillReceiveProps: function(nextProps) { 110 | if (nextProps.setActive !== this.props.setActive || 111 | nextProps.style !== this.props.style) { 112 | this.setState(this._computeState(nextProps)); 113 | } 114 | }, 115 | 116 | _getActiveType: function() { 117 | var activeProp = this.state.setActive || this.props.setActive; 118 | if (!activeProp) return false; 119 | if (typeof(activeProp) === 'function') return 'func'; 120 | if (React.addons.TestUtils.isCompositeComponent(activeProp)) return 'class'; 121 | return false; 122 | }, 123 | 124 | /** 125 | * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are 126 | * defined on your component. 127 | */ 128 | touchableHandleActivePressIn: function() { 129 | this._fromPressIn = true; 130 | this.clearTimeout(this._hideTimeout); 131 | this._hideTimeout = null; 132 | !this._delayActive && this._showActive(); 133 | if (this._delayInactive) { 134 | this._hideTimeout = this.setTimeout(this._hideActive, 135 | this.props.delayInactive); 136 | } 137 | this.props.onPressIn && this.props.onPressIn(); 138 | }, 139 | 140 | touchableHandleActivePressOut: function() { 141 | if (this.props.delayOnPressOut) { 142 | this._onPressOutTimeout = this.setTimeout(function() { 143 | this._onPressOut(); 144 | }, this.props.delayOnPressOut); 145 | } else { 146 | this._onPressOut(); 147 | } 148 | }, 149 | 150 | _onPressOut: function() { 151 | if (!this._hideTimeout && !this._delayInactive) { 152 | this._isHiding = true; 153 | this._hideActive(); 154 | } 155 | this.props.onPressOut && this.props.onPressOut(); 156 | }, 157 | 158 | touchableHandlePress: function() { 159 | if (this.props.delayOnPress) { 160 | if (!this._onPressTimeout) { 161 | this._onPressTimeout = this.setTimeout(function() { 162 | this.clearTimeout(this._onPressTimeout); 163 | this._onPressTimeout = null; 164 | this._onPress(); 165 | }, this.props.delayOnPress); 166 | } 167 | } else { 168 | this._onPress(); 169 | } 170 | }, 171 | 172 | _onPress: function() { 173 | if (!this._fromPressIn) { 174 | !this._delayActive && this._showActive(); 175 | this._isHiding = true; 176 | this._hideTimeout = this.setTimeout(this._hideActive, 177 | this._delayInactive ? this.props.delayInactive : 178 | this.props.delayOnPressOut || DEFAULT_HIDE_MS); 179 | } 180 | this.props.onPress && this.props.onPress(); 181 | }, 182 | 183 | touchableHandleLongPress: function() { 184 | this.props.onLongPress && this.props.onLongPress(); 185 | }, 186 | 187 | touchableGetPressRectOffset: function() { 188 | return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! 189 | }, 190 | 191 | touchableGetHighlightDelayMS: function() { 192 | return this.props.delayOnPressIn === 0 ? 0 : 193 | this.props.delayOnPressIn || DEFAULT_ACTIVE_MS; 194 | }, 195 | 196 | touchableGetLongPressDelayMS: function() { 197 | return this.props.delayOnLongPress === 0 ? 0 : 198 | this.props.delayOnLongPress || DEFAULT_LONG_PRESS_MS; 199 | }, 200 | 201 | _showActive: function() { 202 | if (!this._activeStatus) { 203 | this._activeStatus = true; 204 | if (this._activeType === 'class') { 205 | this.state.setActive.setState({active: true}); 206 | } else if (this._activeType === 'func') { 207 | this.state.setActive(true); 208 | } 209 | } 210 | }, 211 | 212 | _hideActive: function() { 213 | if (this._activeStatus) { 214 | this._activeStatus = false; 215 | this.clearTimeout(this._hideTimeout); 216 | this._hideTimeout = null; 217 | if (this._activeType === 'class') { 218 | this.state.setActive.setState({active: false}); 219 | } else if (this._activeType === 'func') { 220 | this.state.setActive(false); 221 | } 222 | } 223 | }, 224 | 225 | _componentHandleResponderGrant: function(e, dispatchID) { 226 | this._fromPressIn = false; 227 | this._isHiding = false; 228 | this.clearTimeout(this._onPressOutTimeout); 229 | this._onPressOutTimeout = null; 230 | this.clearTimeout(this._hideTimeout); 231 | this._hideTimeout = null; 232 | if (this._delayActive && !this._showTimeout) { 233 | this._showTimeout = this.setTimeout(function() { 234 | this.clearTimeout(this._showTimeout); 235 | this._showTimeout = null; 236 | this._showActive(); 237 | if (this._delayInactive || this._isHiding) { 238 | this._isHiding && this.clearTimeout(this._hideTimeout); 239 | this._hideTimeout = this.setTimeout(this._hideActive, 240 | this._delayInactive ? this.props.delayInactive : 241 | this.props.delayOnPressOut || DEFAULT_HIDE_MS); 242 | } 243 | }, this.props.delayActive); 244 | } 245 | this.touchableHandleResponderGrant(e, dispatchID); 246 | }, 247 | 248 | _renderChildren: function(onlyChild) { 249 | if (this.props.children && onlyChild) return this.props.children; 250 | else return ({this.props.children}); 251 | }, 252 | 253 | render: function() { 254 | var { children } = this.props; 255 | var onlyChild = React.Children.count(children) === 1; 256 | return React.cloneElement(this._renderChildren(onlyChild), { 257 | style: [this.state.componentStyle, onlyChild && children.props.style], 258 | accessible: true, 259 | testID: this.props.testID, 260 | onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, 261 | onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, 262 | onResponderGrant: this._componentHandleResponderGrant, 263 | onResponderMove: this.touchableHandleResponderMove, 264 | onResponderRelease: this.touchableHandleResponderRelease, 265 | onResponderTerminate: this.touchableHandleResponderTerminate, 266 | }); 267 | } 268 | }); 269 | 270 | var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; 271 | 272 | module.exports = TouchableSetActive; 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-touchable-set-active", 3 | "version": "0.0.5", 4 | "description": "Touchable component for React Native that enables more advanced styling by setting an active state. Most useful for building your own touchable/button components on top of.", 5 | "license": "MIT", 6 | "repository": "jmstout/react-native-TouchableSetActive", 7 | "author": { 8 | "name": "Jeff Stout", 9 | "url": "http://jmstout.com" 10 | }, 11 | "main": "TouchableSetActive.js", 12 | "peerDependencies": { 13 | "react-native": "^0.4.x" 14 | }, 15 | "keywords": [ "react-component", "react-native", "react native", "react", "touchable", "touch", "setState", "active", "style", "css", "highlight", "ios" ] 16 | } 17 | --------------------------------------------------------------------------------