Set rebuilt emojis only after search bar animation completes (#3191)

This commit is contained in:
Miguel Alatzar 2019-08-26 07:17:05 -07:00 committed by Elias Nahum
parent ada8d4863b
commit 4dfecea7d1
No known key found for this signature in database
GPG key ID: E038DB71E0B61702
4 changed files with 276 additions and 19 deletions

View file

@ -0,0 +1,129 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
>
<KeyboardAvoidingView
behavior="padding"
enabled={true}
keyboardVerticalOffset={107}
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"paddingVertical": 5,
}
}
>
<SearchBarIos
autoCapitalize="none"
backgroundColor="transparent"
blurOnSubmit={true}
cancelTitle="Cancel"
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
}
leftComponent={null}
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onFocus={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
searchIconCollapsedMargin={10}
searchIconExpandedMargin={10}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<SectionList
ListFooterComponent={[Function]}
data={Array []}
disableVirtualization={false}
getItemLayout={[Function]}
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
onEndReached={[Function]}
onEndReachedThreshold={0}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
pageSize={30}
removeClippedSubviews={true}
renderItem={[Function]}
renderSectionHeader={[Function]}
scrollEventThrottle={50}
sections={Array []}
showsVerticalScrollIndicator={false}
stickySectionHeadersEnabled={true}
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginBottom": 35,
},
Object {
"width": 370,
},
]
}
updateCellsBatchingPeriod={50}
windowSize={21}
/>
<View
style={
Object {
"bottom": 0,
"height": 35,
"left": 0,
"position": "absolute",
"right": 0,
"width": "100%",
}
}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"borderTopColor": "rgba(61,60,64,0.3)",
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "space-between",
}
}
/>
</View>
</View>
</KeyboardAvoidingView>
</Connect(SafeAreaIos)>
`;

View file

@ -35,6 +35,10 @@ const SECTION_MARGIN = 15;
const SECTION_HEADER_HEIGHT = 28;
const EMOJIS_PER_PAGE = 200;
export function filterEmojiSearchInput(searchText) {
return searchText.toLowerCase().replace(/^:|:$/g, '');
}
export default class EmojiPicker extends PureComponent {
static propTypes = {
customEmojisEnabled: PropTypes.bool.isRequired,
@ -87,9 +91,9 @@ export default class EmojiPicker extends PureComponent {
}
componentWillReceiveProps(nextProps) {
let rebuildEmojis = false;
this.rebuildEmojis = false;
if (this.props.deviceWidth !== nextProps.deviceWidth) {
rebuildEmojis = true;
this.rebuildEmojis = true;
if (this.refs.search_bar) {
this.refs.search_bar.blur();
@ -97,14 +101,16 @@ export default class EmojiPicker extends PureComponent {
}
if (this.props.emojis !== nextProps.emojis) {
rebuildEmojis = true;
this.rebuildEmojis = true;
this.deviceWidth = nextProps.deviceWidth;
}
}
if (rebuildEmojis) {
const emojis = this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth);
this.setState({
emojis,
});
setRebuiltEmojis = (searchBarAnimationComplete) => {
if (this.rebuildEmojis && searchBarAnimationComplete) {
this.rebuildEmojis = false;
const emojis = this.renderableEmojis(this.props.emojisBySection, this.deviceWidth);
this.setState({emojis});
}
}
@ -160,24 +166,25 @@ export default class EmojiPicker extends PureComponent {
});
};
changeSearchTerm = (text) => {
changeSearchTerm = (rawText) => {
const searchTerm = filterEmojiSearchInput(rawText);
const nextState = {
searchTerm: text,
searchTerm: rawText,
};
if (!text) {
nextState.currentSectionIndex = 0;
}
this.setState(nextState);
if (!searchTerm) {
nextState.currentSectionIndex = 0;
return;
}
clearTimeout(this.searchTermTimeout);
const timeout = text ? 100 : 0;
const timeout = searchTerm ? 100 : 0;
this.searchTermTimeout = setTimeout(async () => {
if (isMinimumServerVersion(this.props.serverVersion, 4, 7)) {
await this.props.actions.searchCustomEmojis(text);
await this.props.actions.searchCustomEmojis(searchTerm);
}
const filteredEmojis = this.searchEmojis(text);
const filteredEmojis = this.searchEmojis(searchTerm);
this.setState({
filteredEmojis,
});
@ -476,6 +483,7 @@ export default class EmojiPicker extends PureComponent {
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={styles.container}>

View file

@ -0,0 +1,117 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Preferences from 'mattermost-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper';
import EmojiPicker, {filterEmojiSearchInput} from './emoji_picker.js';
describe('components/emoji_picker/EmojiPicker', () => {
const baseProps = {
actions: {
getCustomEmojis: jest.fn(),
incrementEmojiPickerPage: jest.fn(),
searchCustomEmojis: jest.fn(),
},
customEmojisEnabled: false,
customEmojiPage: 200,
deviceWidth: 400,
emojis: [],
emojisBySection: [],
fuse: {},
isLandscape: false,
theme: Preferences.THEMES.default,
};
const testCases = [
{input: 'smile', output: 'smile'},
{input: 'SMILE', output: 'smile'},
{input: ':smile', output: 'smile'},
{input: ':SMILE', output: 'smile'},
{input: 'smile:', output: 'smile'},
{input: 'SMILE:', output: 'smile'},
{input: ':smile:', output: 'smile'},
{input: ':SMILE:', output: 'smile'},
];
testCases.forEach((testCase) => {
test(`'${testCase.input}' should return '${testCase.output}'`, () => {
expect(filterEmojiSearchInput(testCase.input)).toEqual(testCase.output);
});
});
test('should match snapshot', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should set rebuildEmojis to true when deviceWidth changes', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
expect(instance.rebuildEmojis).toBe(undefined);
const newDeviceWidth = baseProps.deviceWidth * 2;
wrapper.setProps({deviceWidth: newDeviceWidth});
expect(instance.rebuildEmojis).toBe(true);
});
test('should set rebuildEmojis to true and new deviceWidth when emojis change', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
expect(instance.rebuildEmojis).toBe(undefined);
expect(instance.deviceWidth).toBe(undefined);
const newDeviceWidth = baseProps.deviceWidth * 2;
const newEmojis = [{}];
wrapper.setProps({deviceWidth: newDeviceWidth, emojis: newEmojis});
expect(instance.rebuildEmojis).toBe(true);
expect(instance.deviceWidth).toBe(newDeviceWidth);
});
test('should set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is true', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.renderableEmojis = jest.spyOn(instance, 'renderableEmojis');
instance.rebuildEmojis = true;
const searchBarAnimationComplete = true;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).toHaveBeenCalledWith({emojis: []});
expect(instance.rebuildEmojis).toBe(false);
});
test('should not set rebuilt emojis when rebuildEmojis is false and searchBarAnimationComplete is true', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.rebuildEmojis = false;
const searchBarAnimationComplete = true;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).not.toHaveBeenCalled();
});
test('should not set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is false', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.rebuildEmojis = true;
const searchBarAnimationComplete = false;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).not.toHaveBeenCalled();
});
});

View file

@ -18,6 +18,7 @@ import EvilIcon from 'react-native-vector-icons/EvilIcons';
import IonIcon from 'react-native-vector-icons/Ionicons';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {emptyFunction} from 'app/utils/general';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon);
@ -77,6 +78,7 @@ export default class Search extends Component {
shadowVisible: PropTypes.bool,
leftComponent: PropTypes.element,
inputCollapsedMargin: PropTypes.number,
onAnimationComplete: PropTypes.func,
};
static defaultProps = {
@ -101,6 +103,7 @@ export default class Search extends Component {
value: '',
leftComponent: null,
inputCollapsedMargin: 10,
onAnimationComplete: emptyFunction,
};
constructor(props) {
@ -369,7 +372,7 @@ export default class Search extends Component {
useNativeDriver: true,
}
),
]).start();
]).start(({finished}) => this.props.onAnimationComplete(finished));
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
resolve();
});