Set rebuilt emojis only after search bar animation completes (#3191)
This commit is contained in:
parent
ada8d4863b
commit
4dfecea7d1
4 changed files with 276 additions and 19 deletions
|
|
@ -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)>
|
||||
`;
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
117
app/components/emoji_picker/emoji_picker.test.js
Normal file
117
app/components/emoji_picker/emoji_picker.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue