Properly scroll to search results (#2348)

* Properly scroll to search results

* Add search recent_item test

* Create a new component for the search modifiers
This commit is contained in:
Elias Nahum 2018-11-21 13:04:26 -03:00 committed by GitHub
parent 55652d0007
commit 9294dec68a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 438 additions and 103 deletions

View file

@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search RecentItem should match snapshot and respond to events 1`] = `
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 58,
}
}
>
<Component
style={
Object {
"flex": 1,
"flexDirection": "column",
"paddingHorizontal": 16,
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flexDirection": "row",
}
}
>
<Component
style={
Object {
"alignItems": "center",
"marginRight": 5,
}
}
>
<Icon
allowFontScaling={false}
name="plus-square-o"
size={12}
style={
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 16,
}
}
/>
</Component>
<Component
style={
Object {
"color": "#3d3c40",
"fontSize": 14,
}
}
>
channel-name
</Component>
</Component>
<Component
style={
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 12,
"marginTop": 5,
}
}
>
to find posts in specific channels
</Component>
</Component>
</Component>
</TouchableHighlight>
`;

View file

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search RecentItem should match snapshot and respond to events 1`] = `
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 42,
}
}
>
<Component
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 14,
"height": 20,
"paddingHorizontal": 16,
}
}
>
test
</Component>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 42,
"justifyContent": "center",
"width": 50,
}
}
>
<Icon
allowFontScaling={false}
color="rgba(61,60,64,0.6)"
name="ios-close-circle-outline"
size={20}
/>
</TouchableOpacity>
</Component>
</TouchableHighlight>
`;

View file

@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableHighlight, View} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export const MODIFIER_LABEL_HEIGHT = 58;
export default class Modifier extends PureComponent {
static propTypes = {
item: PropTypes.object.isRequired,
setModifierValue: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
handlePress = () => {
const {item, setModifierValue} = this.props;
setModifierValue(item.value);
};
render() {
const {item, theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableHighlight
key={item.modifier}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.handlePress}
>
<View style={style.modifierItemContainer}>
<View style={style.modifierItemWrapper}>
<View style={style.modifierItemLabelContainer}>
<View style={style.modifierLabelIconContainer}>
<AwesomeIcon
style={style.modifierLabelIcon}
name='plus-square-o'
/>
</View>
<Text
style={style.modifierItemLabel}
>
{item.modifier}
</Text>
</View>
<Text style={style.modifierItemDescription}>
{item.description}
</Text>
</View>
</View>
</TouchableHighlight>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
modifierItemContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: MODIFIER_LABEL_HEIGHT,
},
modifierItemWrapper: {
flex: 1,
flexDirection: 'column',
paddingHorizontal: 16,
},
modifierItemLabelContainer: {
alignItems: 'center',
flexDirection: 'row',
},
modifierLabelIconContainer: {
alignItems: 'center',
marginRight: 5,
},
modifierLabelIcon: {
fontSize: 16,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
modifierItemLabel: {
fontSize: 14,
color: theme.centerChannelColor,
},
modifierItemDescription: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 5,
},
};
});

View file

@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import Modifier from './modifier';
describe('Search RecentItem', () => {
const item = {
value: 'in:',
modifier: 'channel-name',
description: 'to find posts in specific channels',
};
const baseProps = {
item,
setModifierValue: jest.fn(),
theme: Preferences.THEMES.default,
};
test('should match snapshot and respond to events', () => {
const wrapper = shallow(
<Modifier {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.find('TouchableHighlight').first().props().onPress();
expect(baseProps.setModifierValue).toHaveBeenCalledTimes(1);
expect(baseProps.setModifierValue).toHaveBeenCalledWith(item.value);
});
});

View file

@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableHighlight, TouchableOpacity, View} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export const RECENT_LABEL_HEIGHT = 42;
export default class RecentItem extends PureComponent {
static propTypes = {
item: PropTypes.object.isRequired,
removeSearchTerms: PropTypes.func.isRequired,
setRecentValue: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
handlePress = () => {
const {item, setRecentValue} = this.props;
setRecentValue(item);
};
handleRemove = () => {
const {item, removeSearchTerms} = this.props;
removeSearchTerms(item);
};
render() {
const {item, theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableHighlight
key={item.terms}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.handlePress}
>
<View
style={style.recentItemContainer}
>
<Text
style={style.recentItemLabel}
>
{item.terms}
</Text>
<TouchableOpacity
onPress={this.handleRemove}
style={style.recentRemove}
>
<IonIcon
name='ios-close-circle-outline'
size={20}
color={changeOpacity(theme.centerChannelColor, 0.6)}
/>
</TouchableOpacity>
</View>
</TouchableHighlight>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
recentItemContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: RECENT_LABEL_HEIGHT,
},
recentItemLabel: {
color: theme.centerChannelColor,
fontSize: 14,
height: 20,
flex: 1,
paddingHorizontal: 16,
},
recentRemove: {
alignItems: 'center',
height: RECENT_LABEL_HEIGHT,
justifyContent: 'center',
width: 50,
},
};
});

View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import RecentItem from './recent_item';
describe('Search RecentItem', () => {
const item = {
terms: 'test',
};
const baseProps = {
item,
removeSearchTerms: jest.fn(),
setRecentValue: jest.fn(),
theme: Preferences.THEMES.default,
};
test('should match snapshot and respond to events', () => {
const wrapper = shallow(
<RecentItem {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.find('TouchableHighlight').first().props().onPress();
expect(baseProps.setRecentValue).toHaveBeenCalledTimes(1);
expect(baseProps.setRecentValue).toHaveBeenCalledWith(item);
wrapper.find('TouchableOpacity').first().props().onPress();
expect(baseProps.setRecentValue).toHaveBeenCalledTimes(1);
expect(baseProps.setRecentValue).toHaveBeenCalledWith(item);
});
});

View file

@ -9,11 +9,8 @@ import {
Platform,
SectionList,
Text,
TouchableHighlight,
TouchableOpacity,
View,
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {RequestStatus} from 'mattermost-redux/constants';
@ -37,12 +34,12 @@ import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDisplayName from './channel_display_name';
import Modifier, {MODIFIER_LABEL_HEIGHT} from './modifier';
import RecentItem, {RECENT_LABEL_HEIGHT} from './recent_item';
import SearchResultPost from './search_result_post';
const SECTION_HEIGHT = 20;
const RECENT_LABEL_HEIGHT = 42;
const RECENT_SEPARATOR_HEIGHT = 3;
const MODIFIER_LABEL_HEIGHT = 58;
const SCROLL_UP_MULTIPLIER = 6;
const SEARCHING = 'searching';
const NO_RESULTS = 'no results';
@ -97,6 +94,7 @@ export default class Search extends PureComponent {
cursorPosition: 0,
value: props.initialValue,
managedConfig: {},
recent: props.recent,
};
}
@ -118,12 +116,13 @@ export default class Search extends PureComponent {
}, 150);
}
componentDidUpdate(prevProps) {
const {searchingStatus: status, recent, enableDateSuggestion} = this.props;
componentDidUpdate(prevProps, prevState) {
const {searchingStatus: status, enableDateSuggestion} = this.props;
const {recent} = this.state;
const {searchingStatus: prevStatus} = prevProps;
const shouldScroll = prevStatus !== status &&
(status === RequestStatus.SUCCESS || status === RequestStatus.STARTED) &&
!this.props.isSearchGettingMore && !prevProps.isSearchGettingMore;
(status === RequestStatus.SUCCESS || status === RequestStatus.FAILURE) &&
!this.props.isSearchGettingMore && !prevProps.isSearchGettingMore && prevState.recent.length === recent.length;
if (this.props.isLandscape !== prevProps.isLandscape) {
this.refs.searchBar.blur();
@ -131,17 +130,21 @@ export default class Search extends PureComponent {
if (shouldScroll) {
setTimeout(() => {
const recentLabelsHeight = (recent.length + (Platform.OS === 'ios' ? 1 : 0)) * RECENT_LABEL_HEIGHT;
const recentSeparatorsHeight = (recent.length + (Platform.OS === 'ios' ? 0 : 2)) * RECENT_SEPARATOR_HEIGHT;
const recentLabelsHeight = recent.length * RECENT_LABEL_HEIGHT;
const recentSeparatorsHeight = (recent.length - 1) * RECENT_SEPARATOR_HEIGHT;
const modifiersCount = enableDateSuggestion ? 5 : 2;
const modifiersHeight = modifiersCount * MODIFIER_LABEL_HEIGHT;
const modifiersSeparatorHeight = (modifiersCount - 1) * RECENT_SEPARATOR_HEIGHT;
const offset = modifiersHeight + modifiersSeparatorHeight + SECTION_HEIGHT + recentLabelsHeight + recentSeparatorsHeight;
Keyboard.dismiss();
if (this.refs.list) {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
animated: true,
offset: SECTION_HEIGHT + (modifiersCount * MODIFIER_LABEL_HEIGHT) +
recentLabelsHeight + recentSeparatorsHeight,
offset,
});
}
}, 100);
}, 250);
}
}
@ -325,6 +328,14 @@ export default class Search extends PureComponent {
removeSearchTerms = preventDoubleTap((item) => {
const {actions, currentTeamId} = this.props;
const recent = [...this.state.recent];
const index = recent.indexOf(item);
if (index !== -1) {
recent.splice(index, 1);
this.setState({recent});
}
actions.removeSearchTerms(currentTeamId, item.terms);
});
@ -343,35 +354,13 @@ export default class Search extends PureComponent {
renderModifiers = ({item}) => {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableHighlight
key={item.modifier}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => this.setModifierValue(item.value)}
>
<View style={style.modifierItemContainer}>
<View style={style.modifierItemWrapper}>
<View style={style.modifierItemLabelContainer}>
<View style={style.modifierLabelIconContainer}>
<AwesomeIcon
style={style.modifierLabelIcon}
name='plus-square-o'
/>
</View>
<Text
style={style.modifierItemLabel}
>
{item.modifier}
</Text>
</View>
<Text style={style.modifierItemDescription}>
{item.description}
</Text>
</View>
</View>
</TouchableHighlight>
<Modifier
item={item}
setModifierValue={this.setModifierValue}
theme={theme}
/>
);
};
@ -455,34 +444,14 @@ export default class Search extends PureComponent {
renderRecentItem = ({item}) => {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableHighlight
key={item.terms}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => this.setRecentValue(item)}
>
<View
style={style.recentItemContainer}
>
<Text
style={style.recentItemLabel}
>
{item.terms}
</Text>
<TouchableOpacity
onPress={() => this.removeSearchTerms(item)}
style={style.recentRemove}
>
<IonIcon
name='ios-close-circle-outline'
size={20}
color={changeOpacity(theme.centerChannelColor, 0.6)}
/>
</TouchableOpacity>
</View>
</TouchableHighlight>
<RecentItem
item={item}
removeSearchTerms={this.removeSearchTerms}
setRecentValue={this.setRecentValue}
theme={theme}
/>
);
};
@ -537,10 +506,12 @@ export default class Search extends PureComponent {
}
};
search = (terms, isOrSearch) => {
search = (text, isOrSearch) => {
const {actions, currentTeamId, viewArchivedChannels} = this.props;
const recent = [...this.state.recent];
const terms = text.trim();
this.handleTextChanged(`${terms.trim()} `);
this.handleTextChanged(`${terms} `);
// Trigger onSelectionChanged Manually when submitting
this.handleSelectionChange({
@ -553,7 +524,7 @@ export default class Search extends PureComponent {
// timezone offset in seconds
const params = {
terms: terms.trim(),
terms,
is_or_search: isOrSearch,
time_zone_offset: this.props.timezoneOffsetInSeconds,
page: 0,
@ -561,6 +532,12 @@ export default class Search extends PureComponent {
include_deleted_channels: viewArchivedChannels,
};
actions.searchPostsWithParams(currentTeamId, params, true);
if (!recent.find((r) => r.terms === terms)) {
recent.push({
terms,
});
this.setState({recent});
}
};
setModifierValue = preventDoubleTap((modifier) => {
@ -593,7 +570,6 @@ export default class Search extends PureComponent {
const {
isLandscape,
postIds,
recent,
searchingStatus,
theme,
isSearchGettingMore,
@ -602,6 +578,7 @@ export default class Search extends PureComponent {
const {intl} = this.context;
const {
cursorPosition,
recent,
value,
} = this.state;
const style = getStyleFromTheme(theme);
@ -825,38 +802,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 12,
fontWeight: '600',
},
modifierItemContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: MODIFIER_LABEL_HEIGHT,
},
modifierItemWrapper: {
flex: 1,
flexDirection: 'column',
paddingHorizontal: 16,
},
modifierItemLabelContainer: {
alignItems: 'center',
flexDirection: 'row',
},
modifierLabelIconContainer: {
alignItems: 'center',
marginRight: 5,
},
modifierLabelIcon: {
fontSize: 16,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
modifierItemLabel: {
fontSize: 14,
color: theme.centerChannelColor,
},
modifierItemDescription: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 5,
},
recentItemContainer: {
alignItems: 'center',
flex: 1,