Files
react-native/Libraries/Components/Touchable/TouchableOpacity.js
Maksym Komarychev 9366ce416f Update opacity when disabled prop is changed
Summary:
fixes #17105

If you render

```
    <TouchableOpacity
        disabled={true}
        style={{opacity: 0.5}}
    >
        ...
    </TouchableOpacity>
```

and then

```
    <TouchableOpacity
        disabled={false}
        style={{opacity: 1}}
    >
        ...
    </TouchableOpacity>
```

The content of `TouchableOpacity` will still have opacity = 0.5 because real
opacity is controlled by animated property which should be properly updated
when `disabled` prop changes.

<!--
Thank you for sending the PR! We appreciate you spending the time to work on these changes.

Help us understand your motivation by explaining why you decided to make this change.

You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html

Happy contributing!

-->

Usually when a button or other UI component is built with `TouchableOpacity` you may want to change it's opacity if state of component is changed (enabled/disabled). Opacity provided in props is overridden with internally-managed animated value. Add extra check when component is updated to trigger opacity animation upon change of `disabled` flag.

You can use code from https://github.com/facebook/react-native/issues/17105.

(If this PR adds or changes functionality, please take some time to update the docs at https://github.com/facebook/react-native-website, and link to your PR here.)

[GENERAL][BUGFIX][TouchableOpacity] - trigger animation on `opacity` upon change in `disabled` prop.
<!--
Help reviewers and the release process by writing your own release notes

**INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.**

  CATEGORY
[----------]        TYPE
[ CLI      ]   [-------------]      LOCATION
[ DOCS     ]   [ BREAKING    ]   [-------------]
[ GENERAL  ]   [ BUGFIX      ]   [-{Component}-]
[ INTERNAL ]   [ ENHANCEMENT ]   [ {File}      ]
[ IOS      ]   [ FEATURE     ]   [ {Directory} ]   |-----------|
[ ANDROID  ]   [ MINOR       ]   [ {Framework} ] - | {Message} |
[----------]   [-------------]   [-------------]   |-----------|

[CATEGORY] [TYPE] [LOCATION] - MESSAGE

 EXAMPLES:

 [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things
 [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput
 [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with
 [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word
 [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position
 [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see
-->
Closes https://github.com/facebook/react-native/pull/17106

Differential Revision: D7158549

Pulled By: hramos

fbshipit-source-id: 209cc433b829b129810e8a884964c8853ca3fe8f
2018-03-05 16:12:00 -08:00

276 lines
7.3 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule TouchableOpacity
* @noflow
*/
'use strict';
// Note (avik): add @flow when Flow supports spread properties in propTypes
const Animated = require('Animated');
const Easing = require('Easing');
const NativeMethodsMixin = require('NativeMethodsMixin');
const React = require('React');
const PropTypes = require('prop-types');
const TimerMixin = require('react-timer-mixin');
const Touchable = require('Touchable');
const TouchableWithoutFeedback = require('TouchableWithoutFeedback');
const createReactClass = require('create-react-class');
const ensurePositiveDelayProps = require('ensurePositiveDelayProps');
const flattenStyle = require('flattenStyle');
type Event = Object;
const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
*
* Opacity is controlled by wrapping the children in an Animated.View, which is
* added to the view hiearchy. Be aware that this can affect layout.
*
* Example:
*
* ```
* renderButton: function() {
* return (
* <TouchableOpacity onPress={this._onPressButton}>
* <Image
* style={styles.button}
* source={require('./myButton.png')}
* />
* </TouchableOpacity>
* );
* },
* ```
* ### Example
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react'
* import {
* AppRegistry,
* StyleSheet,
* TouchableOpacity,
* Text,
* View,
* } from 'react-native'
*
* class App extends Component {
* constructor(props) {
* super(props)
* this.state = { count: 0 }
* }
*
* onPress = () => {
* this.setState({
* count: this.state.count+1
* })
* }
*
* render() {
* return (
* <View style={styles.container}>
* <TouchableOpacity
* style={styles.button}
* onPress={this.onPress}
* >
* <Text> Touch Here </Text>
* </TouchableOpacity>
* <View style={[styles.countContainer]}>
* <Text style={[styles.countText]}>
* { this.state.count !== 0 ? this.state.count: null}
* </Text>
* </View>
* </View>
* )
* }
* }
*
* const styles = StyleSheet.create({
* container: {
* flex: 1,
* justifyContent: 'center',
* paddingHorizontal: 10
* },
* button: {
* alignItems: 'center',
* backgroundColor: '#DDDDDD',
* padding: 10
* },
* countContainer: {
* alignItems: 'center',
* padding: 10
* },
* countText: {
* color: '#FF00FF'
* }
* })
*
* AppRegistry.registerComponent('App', () => App)
* ```
*
*/
const TouchableOpacity = createReactClass({
displayName: 'TouchableOpacity',
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin],
propTypes: {
...TouchableWithoutFeedback.propTypes,
/**
* Determines what the opacity of the wrapped view should be when touch is
* active. Defaults to 0.2.
*/
activeOpacity: PropTypes.number,
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
*/
hasTVPreferredFocus: PropTypes.bool,
/**
* Apple TV parallax effects
*/
tvParallaxProperties: PropTypes.object,
},
getDefaultProps: function() {
return {
activeOpacity: 0.2,
};
},
getInitialState: function() {
return {
...this.touchableGetInitialState(),
anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
};
},
componentDidMount: function() {
ensurePositiveDelayProps(this.props);
},
UNSAFE_componentWillReceiveProps: function(nextProps) {
ensurePositiveDelayProps(nextProps);
},
componentDidUpdate: function(prevProps, prevState) {
if (this.props.disabled !== prevProps.disabled) {
this._opacityInactive(250);
}
},
/**
* Animate the touchable to a new opacity.
*/
setOpacityTo: function(value: number, duration: number) {
Animated.timing(
this.state.anim,
{
toValue: value,
duration: duration,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}
).start();
},
/**
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
* defined on your component.
*/
touchableHandleActivePressIn: function(e: Event) {
if (e.dispatchConfig.registrationName === 'onResponderGrant') {
this._opacityActive(0);
} else {
this._opacityActive(150);
}
this.props.onPressIn && this.props.onPressIn(e);
},
touchableHandleActivePressOut: function(e: Event) {
this._opacityInactive(250);
this.props.onPressOut && this.props.onPressOut(e);
},
touchableHandlePress: function(e: Event) {
this.props.onPress && this.props.onPress(e);
},
touchableHandleLongPress: function(e: Event) {
this.props.onLongPress && this.props.onLongPress(e);
},
touchableGetPressRectOffset: function() {
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
},
touchableGetHitSlop: function() {
return this.props.hitSlop;
},
touchableGetHighlightDelayMS: function() {
return this.props.delayPressIn || 0;
},
touchableGetLongPressDelayMS: function() {
return this.props.delayLongPress === 0 ? 0 :
this.props.delayLongPress || 500;
},
touchableGetPressOutDelayMS: function() {
return this.props.delayPressOut;
},
_opacityActive: function(duration: number) {
this.setOpacityTo(this.props.activeOpacity, duration);
},
_opacityInactive: function(duration: number) {
this.setOpacityTo(
this._getChildStyleOpacityWithDefault(),
duration
);
},
_getChildStyleOpacityWithDefault: function() {
const childStyle = flattenStyle(this.props.style) || {};
return childStyle.opacity == undefined ? 1 : childStyle.opacity;
},
render: function() {
return (
<Animated.View
accessible={this.props.accessible !== false}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityComponentType={this.props.accessibilityComponentType}
accessibilityTraits={this.props.accessibilityTraits}
style={[this.props.style, {opacity: this.state.anim}]}
nativeID={this.props.nativeID}
testID={this.props.testID}
onLayout={this.props.onLayout}
isTVSelectable={true}
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
tvParallaxProperties={this.props.tvParallaxProperties}
hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}>
{this.props.children}
{Touchable.renderDebugView({color: 'cyan', hitSlop: this.props.hitSlop})}
</Animated.View>
);
},
});
module.exports = TouchableOpacity;