Files
mattermost-mobile/app/components/search_bar/search_box.js
Miguel Alatzar de7b88beb2 MM-14541 Fix search bar animation stutter in main search bar (#2672)
* Do not expand drawer on search start

* Add state.searching for determining showTeams

* Add leftComponent prop to SearchBar to render SwitchTeamsButton

* Update snapshot tests

* Use native driver when possible and fix start calls
2019-04-22 19:01:48 -07:00

564 lines
20 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Dimensions,
InteractionManager,
Keyboard,
Text,
TextInput,
TouchableWithoutFeedback,
StyleSheet,
View,
} from 'react-native';
import EvilIcon from 'react-native-vector-icons/EvilIcons';
import IonIcon from 'react-native-vector-icons/Ionicons';
import CustomPropTypes from 'app/constants/custom_prop_types';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon);
const AnimatedEvilcon = Animated.createAnimatedComponent(EvilIcon);
const containerHeight = 40;
const middleHeight = 20;
export default class Search extends Component {
static propTypes = {
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onSearch: PropTypes.func,
onChangeText: PropTypes.func,
onCancel: PropTypes.func,
onDelete: PropTypes.func,
onSelectionChange: PropTypes.func,
backgroundColor: PropTypes.string,
placeholderTextColor: PropTypes.string,
titleCancelColor: PropTypes.string,
tintColorSearch: PropTypes.string,
tintColorDelete: PropTypes.string,
selectionColor: PropTypes.string,
inputStyle: CustomPropTypes.Style,
onLayout: PropTypes.func,
cancelButtonStyle: CustomPropTypes.Style,
autoFocus: PropTypes.bool,
placeholder: PropTypes.string,
cancelTitle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
iconDelete: PropTypes.object,
iconSearch: PropTypes.object,
returnKeyType: PropTypes.string,
keyboardType: PropTypes.string,
autoCapitalize: PropTypes.string,
inputHeight: PropTypes.number,
inputBorderRadius: PropTypes.number,
contentWidth: PropTypes.number,
middleWidth: PropTypes.number,
editable: PropTypes.bool,
blurOnSubmit: PropTypes.bool,
keyboardShouldPersist: PropTypes.bool,
value: PropTypes.string,
positionRightDelete: PropTypes.number,
searchIconCollapsedMargin: PropTypes.number,
searchIconExpandedMargin: PropTypes.number,
placeholderCollapsedMargin: PropTypes.number,
placeholderExpandedMargin: PropTypes.number,
shadowOffsetHeightCollapsed: PropTypes.number,
shadowOffsetHeightExpanded: PropTypes.number,
shadowOffsetWidth: PropTypes.number,
shadowColor: PropTypes.string,
shadowOpacityCollapsed: PropTypes.number,
shadowOpacityExpanded: PropTypes.number,
shadowRadius: PropTypes.number,
shadowVisible: PropTypes.bool,
leftComponent: PropTypes.element,
};
static defaultProps = {
onSelectionChange: () => true,
onBlur: () => true,
editable: true,
blurOnSubmit: false,
keyboardShouldPersist: false,
placeholderTextColor: 'grey',
searchIconCollapsedMargin: 25,
searchIconExpandedMargin: 10,
placeholderCollapsedMargin: 15,
placeholderExpandedMargin: 20,
shadowOffsetWidth: 0,
shadowOffsetHeightCollapsed: 2,
shadowOffsetHeightExpanded: 4,
shadowColor: '#000',
shadowOpacityCollapsed: 0.12,
shadowOpacityExpanded: 0.24,
shadowRadius: 4,
shadowVisible: false,
value: '',
leftComponent: null,
};
constructor(props) {
super(props);
this.state = {
expanded: false,
leftComponentWidth: 0,
};
const {width} = Dimensions.get('window');
this.contentWidth = width;
this.middleWidth = width / 2;
this.iconSearchAnimated = new Animated.Value(this.props.searchIconCollapsedMargin);
this.iconDeleteAnimated = new Animated.Value(0);
this.leftComponentAnimated = new Animated.Value(0);
this.inputFocusAnimated = new Animated.Value(0);
this.inputFocusWidthAnimated = new Animated.Value(this.contentWidth - 10);
this.inputFocusPlaceholderAnimated = new Animated.Value(this.props.placeholderCollapsedMargin);
this.btnCancelAnimated = new Animated.Value(this.contentWidth);
this.shadowOpacityAnimated = new Animated.Value(this.props.shadowOpacityCollapsed);
this.placeholder = this.props.placeholder || 'Search';
this.cancelTitle = this.props.cancelTitle || 'Cancel';
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
}
componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
if (nextProps.value) {
this.iconDeleteAnimated = new Animated.Value(1);
} else {
this.iconDeleteAnimated = new Animated.Value(0);
}
}
}
blur = () => {
this.refs.input_keyword.getNode().blur();
this.setState({expanded: false});
this.collapseAnimation();
};
focus = () => {
InteractionManager.runAfterInteractions(() => {
const input = this.refs.input_keyword.getNode();
if (!input.isFocused()) {
input.focus();
}
});
};
onBlur = () => {
this.props.onBlur();
};
onLayout = (event) => {
const contentWidth = event.nativeEvent.layout.width;
this.contentWidth = contentWidth;
this.middleWidth = contentWidth / 2;
if (this.state.expanded) {
this.expandAnimation();
} else {
this.collapseAnimation();
}
};
onLeftComponentLayout = (event) => {
const leftComponentWidth = event.nativeEvent.layout.width;
this.setState({leftComponentWidth});
};
onSearch = async () => {
if (this.props.keyboardShouldPersist === false) {
await Keyboard.dismiss();
}
if (this.props.onSearch) {
this.props.onSearch(this.props.value);
}
};
onChangeText = (text) => {
Animated.timing(
this.iconDeleteAnimated,
{
toValue: (text.length > 0) ? 1 : 0,
duration: 200,
useNativeDriver: true,
}
).start();
if (this.props.onChangeText) {
this.props.onChangeText(text);
}
};
onFocus = () => {
InteractionManager.runAfterInteractions(async () => {
this.setState({expanded: true});
await this.expandAnimation();
if (this.props.onFocus) {
this.props.onFocus(this.props.value);
}
});
};
onDelete = () => {
Animated.timing(
this.iconDeleteAnimated,
{
toValue: 0,
duration: 200,
useNativeDriver: true,
}
).start();
this.focus();
if (this.props.onDelete) {
this.props.onDelete();
}
};
onCancel = async () => {
this.setState({expanded: false});
await this.collapseAnimation(true);
if (this.props.onCancel) {
this.props.onCancel();
}
};
onSelectionChange = (event) => {
this.props.onSelectionChange(event);
};
expandAnimation = () => {
return new Promise((resolve) => {
Animated.parallel([
Animated.timing(
this.inputFocusWidthAnimated,
{
toValue: this.contentWidth - 70,
duration: 200,
}
),
Animated.timing(
this.inputFocusAnimated,
{
toValue: this.state.leftComponentWidth,
duration: 200,
}
),
Animated.timing(
this.leftComponentAnimated,
{
toValue: this.contentWidth,
duration: 200,
}
),
Animated.timing(
this.btnCancelAnimated,
{
toValue: this.state.leftComponentWidth ? 15 - this.state.leftComponentWidth : 10,
duration: 200,
}
),
Animated.timing(
this.inputFocusPlaceholderAnimated,
{
toValue: this.props.placeholderExpandedMargin,
duration: 200,
}
),
Animated.timing(
this.iconSearchAnimated,
{
toValue: this.props.searchIconExpandedMargin,
duration: 200,
}
),
Animated.timing(
this.iconDeleteAnimated,
{
toValue: (this.props.value.length > 0) ? 1 : 0,
duration: 200,
useNativeDriver: true,
}
),
Animated.timing(
this.shadowOpacityAnimated,
{
toValue: this.props.shadowOpacityExpanded,
duration: 200,
useNativeDriver: true,
}
),
]).start();
this.shadowHeight = this.props.shadowOffsetHeightExpanded;
resolve();
});
};
collapseAnimation = (isForceAnim = false) => {
return new Promise((resolve) => {
Animated.parallel([
((this.props.keyboardShouldPersist === false) ? Keyboard.dismiss() : null),
Animated.timing(
this.inputFocusWidthAnimated,
{
toValue: this.contentWidth - this.state.leftComponentWidth - 10,
duration: 200,
}
),
Animated.timing(
this.inputFocusAnimated,
{
toValue: 0,
duration: 200,
}
),
Animated.timing(
this.leftComponentAnimated,
{
toValue: 0,
duration: 200,
}
),
Animated.timing(
this.btnCancelAnimated,
{
toValue: this.contentWidth,
duration: 200,
}
),
((this.props.keyboardShouldPersist === false) ?
Animated.timing(
this.inputFocusPlaceholderAnimated,
{
toValue: this.props.placeholderCollapsedMargin,
duration: 200,
}
) : null),
((this.props.keyboardShouldPersist === false || isForceAnim === true) ?
Animated.timing(
this.iconSearchAnimated,
{
toValue: this.props.searchIconCollapsedMargin + this.state.leftComponentWidth,
duration: 200,
}
) : null),
Animated.timing(
this.iconDeleteAnimated,
{
toValue: 0,
duration: 200,
useNativeDriver: true,
}
),
Animated.timing(
this.shadowOpacityAnimated,
{
toValue: this.props.shadowOpacityCollapsed,
duration: 200,
useNativeDriver: true,
}
),
]).start();
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
resolve();
});
};
render() {
const {backgroundColor, ...restOfInputPropStyles} = this.props.inputStyle;
return (
<Animated.View
ref='searchContainer'
style={[
styles.container,
this.props.backgroundColor && {backgroundColor: this.props.backgroundColor},
this.state.leftComponentWidth && {padding: 0},
]}
onLayout={this.onLayout}
>
{((this.props.leftComponent) ?
<Animated.View
style={{right: this.leftComponentAnimated}}
onLayout={this.onLeftComponentLayout}
>
{this.props.leftComponent}
</Animated.View> :
null
)}
<Animated.View style={{backgroundColor, right: this.inputFocusAnimated}}>
<AnimatedTextInput
ref='input_keyword'
style={[
styles.input,
this.props.placeholderTextColor && {color: this.props.placeholderTextColor},
this.props.inputHeight && {height: this.props.inputHeight},
this.props.inputBorderRadius && {borderRadius: this.props.inputBorderRadius},
{
width: this.inputFocusWidthAnimated,
paddingLeft: this.inputFocusPlaceholderAnimated,
},
restOfInputPropStyles,
this.props.shadowVisible && {
shadowOffset: {width: this.props.shadowOffsetWidth, height: this.shadowHeight},
shadowColor: this.props.shadowColor,
shadowOpacity: this.shadowOpacityAnimated,
shadowRadius: this.props.shadowRadius,
},
]}
autoFocus={this.props.autoFocus}
editable={this.props.editable}
value={this.props.value}
onChangeText={this.onChangeText}
placeholder={this.placeholder}
placeholderTextColor={this.props.placeholderTextColor}
selectionColor={this.props.selectionColor}
onSubmitEditing={this.onSearch}
onSelectionChange={this.onSelectionChange}
autoCorrect={false}
blurOnSubmit={this.props.blurOnSubmit}
returnKeyType={this.props.returnKeyType || 'search'}
keyboardType={this.props.keyboardType || 'default'}
autoCapitalize={this.props.autoCapitalize}
onBlur={this.onBlur}
onFocus={this.onFocus}
underlineColorAndroid='transparent'
enablesReturnKeyAutomatically={true}
/>
</Animated.View>
<TouchableWithoutFeedback onPress={this.onFocus}>
{((this.props.iconSearch) ?
<Animated.View
style={[
styles.iconSearch,
{left: this.iconSearchAnimated},
]}
>
{this.props.iconSearch}
</Animated.View> :
<AnimatedEvilcon
name='search'
size={24}
style={[
styles.iconSearch,
styles.iconSearchDefault,
this.props.tintColorSearch && {color: this.props.tintColorSearch},
{
left: this.iconSearchAnimated,
top: middleHeight - 10,
},
]}
/>
)}
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={this.onDelete}>
{((this.props.iconDelete) ?
<Animated.View
style={[
styles.iconDelete,
this.props.positionRightDelete && {right: this.props.positionRightDelete},
{opacity: this.iconDeleteAnimated},
]}
>
{this.props.iconDelete}
</Animated.View> :
<View style={[styles.iconDelete, this.props.inputHeight && {height: this.props.inputHeight}]}>
<AnimatedIonIcon
name='ios-close-circle'
size={17}
style={[
styles.iconDeleteDefault,
this.props.tintColorDelete && {color: this.props.tintColorDelete},
this.props.positionRightDelete && {right: this.props.positionRightDelete},
{
opacity: this.iconDeleteAnimated,
},
]}
/>
</View>
)}
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={this.onCancel}>
<Animated.View
style={[
styles.cancelButton,
this.props.cancelButtonStyle && this.props.cancelButtonStyle,
{left: this.btnCancelAnimated},
]}
>
<Text
style={[
styles.cancelButtonText,
this.props.titleCancelColor && {color: this.props.titleCancelColor},
this.props.cancelButtonStyle && this.props.cancelButtonStyle,
]}
>
{this.cancelTitle}
</Text>
</Animated.View>
</TouchableWithoutFeedback>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'grey',
height: containerHeight,
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
padding: 5,
},
input: {
height: containerHeight - 10,
paddingTop: 7,
paddingBottom: 5,
paddingRight: 32,
borderColor: '#444',
borderRadius: 5,
fontSize: 15,
},
iconSearch: {
flex: 1,
position: 'absolute',
},
iconSearchDefault: {
color: 'grey',
},
iconDelete: {
alignItems: 'flex-start',
justifyContent: 'center',
position: 'absolute',
paddingLeft: 1,
paddingTop: 3,
right: 65,
width: 25,
},
iconDeleteDefault: {
color: 'grey',
},
cancelButton: {
justifyContent: 'center',
alignItems: 'flex-start',
backgroundColor: 'transparent',
width: 60,
height: 50,
},
cancelButtonText: {
fontSize: 14,
color: '#fff',
},
});