[MM-20200] Fix autocomplete when editing channel header (#3595)

* Fix autocomplete when editing channel header

* Fix iOS scroll

* Use zIndex on header help text view

* No need to use innerRef

* Fix typos
This commit is contained in:
Miguel Alatzar 2019-11-27 10:43:28 -07:00 committed by Saturnino Abril
parent 75e0854411
commit 2393eb3283
4 changed files with 117 additions and 35 deletions

View file

@ -34,6 +34,7 @@ export default class Autocomplete extends PureComponent {
valueEvent: PropTypes.string,
cursorPositionEvent: PropTypes.string,
nestedScrollEnabled: PropTypes.bool,
expandDown: PropTypes.bool,
};
static defaultProps = {
@ -160,32 +161,34 @@ export default class Autocomplete extends PureComponent {
}
render() {
const style = getStyleFromTheme(this.props.theme);
const {theme, isSearch, expandDown} = this.props;
const style = getStyleFromTheme(theme);
const wrapperStyle = [];
const containerStyle = [];
if (this.props.isSearch) {
wrapperStyle.push(style.base, style.searchContainer);
containerStyle.push(style.content);
const wrapperStyles = [];
const containerStyles = [];
if (isSearch) {
wrapperStyles.push(style.base, style.searchContainer);
containerStyles.push(style.content);
} else {
containerStyle.push(style.base, style.container);
const containerStyle = expandDown ? style.containerExpandDown : style.container;
containerStyles.push(style.base, containerStyle);
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
wrapperStyles.push(style.bordersSearch);
} else {
containerStyle.push(style.borders);
containerStyles.push(style.borders);
}
}
const maxListHeight = this.maxListHeight();
return (
<View style={wrapperStyle}>
<View style={containerStyle}>
<View style={wrapperStyles}>
<View style={containerStyles}>
<AtMention
{...this.props}
cursorPosition={cursorPosition}
@ -256,6 +259,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
bottom: 0,
},
containerExpandDown: {
top: 0,
},
content: {
flex: 1,
},

View file

@ -11,6 +11,8 @@ exports[`EditChannelInfo should match snapshot 1`] = `
extraScrollHeight={0}
keyboardOpeningTime={250}
keyboardShouldPersistTaps="always"
onKeyboardDidHide={[Function]}
onKeyboardDidShow={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
@ -199,6 +201,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
</View>
<View
onLayout={[Function]}
style={
Array [
Object {
@ -234,13 +237,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
}
/>
</View>
<ForwardRef(forwardConnectRef)
cursorPosition={6}
maxHeight={200}
nestedScrollEnabled={true}
onChangeText={[Function]}
value="header"
/>
<View
style={
Array [
@ -288,7 +284,21 @@ exports[`EditChannelInfo should match snapshot 1`] = `
value="header"
/>
</View>
<View>
<ForwardRef(forwardConnectRef)
cursorPosition={6}
expandDown={true}
maxHeight={200}
nestedScrollEnabled={true}
onChangeText={[Function]}
value="header"
/>
<View
style={
Object {
"zIndex": -1,
}
}
>
<FormattedText
defaultMessage="Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com)."
id="channel_modal.headerHelp"

View file

@ -7,7 +7,6 @@ import {
Platform,
TouchableWithoutFeedback,
View,
findNodeHandle,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
@ -67,8 +66,11 @@ export default class EditChannelInfo extends PureComponent {
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.lastText = React.createRef();
this.scroll = React.createRef();
this.state = {
keyboardVisible: false,
};
}
blur = () => {
@ -154,12 +156,37 @@ export default class EditChannelInfo extends PureComponent {
}
};
scrollToEnd = () => {
if (this.scroll?.current && this.lastText?.current) {
this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current));
onHeaderLayout = ({nativeEvent}) => {
this.setState({headerPosition: nativeEvent.layout.y});
}
onKeyboardDidShow = () => {
this.setState({keyboardVisible: true});
if (this.state.headerHasFocus) {
this.setState({headerHasFocus: false});
this.scrollHeaderToTop();
}
}
onKeyboardDidHide = () => {
this.setState({keyboardVisible: false});
}
onHeaderFocus = () => {
if (this.state.keyboardVisible) {
this.scrollHeaderToTop();
} else {
this.setState({headerHasFocus: true});
}
};
scrollHeaderToTop = () => {
if (this.scroll.current) {
this.scroll.current.scrollToPosition(0, this.state.headerPosition);
}
}
render() {
const {
theme,
@ -170,8 +197,10 @@ export default class EditChannelInfo extends PureComponent {
header,
purpose,
isLandscape,
error,
saving,
} = this.props;
const {error, saving} = this.props;
const {keyboardVisible} = this.state;
const style = getStyleSheet(theme);
@ -205,6 +234,9 @@ export default class EditChannelInfo extends PureComponent {
ref={this.scroll}
style={style.container}
keyboardShouldPersistTaps={'always'}
onKeyboardDidShow={this.onKeyboardDidShow}
onKeyboardDidHide={this.onKeyboardDidHide}
enableAutomaticScroll={!keyboardVisible}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
@ -277,7 +309,10 @@ export default class EditChannelInfo extends PureComponent {
</View>
</View>
)}
<View style={[style.titleContainer15, padding(isLandscape)]}>
<View
onLayout={this.onHeaderLayout}
style={[style.titleContainer15, padding(isLandscape)]}
>
<FormattedText
style={style.title}
id='channel_modal.header'
@ -289,13 +324,6 @@ export default class EditChannelInfo extends PureComponent {
defaultMessage='(optional)'
/>
</View>
<Autocomplete
cursorPosition={header.length}
maxHeight={200}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
/>
<View style={[style.inputContainer, padding(isLandscape)]}>
<TextInputWithLocalizedPlaceholder
ref={this.headerInput}
@ -308,14 +336,22 @@ export default class EditChannelInfo extends PureComponent {
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.scrollToEnd}
onFocus={this.onHeaderFocus}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View ref={this.lastText}>
<Autocomplete
cursorPosition={header.length}
maxHeight={200}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
expandDown={true}
/>
<View style={style.headerHelpText}>
<FormattedText
style={[style.helpText, padding(isLandscape)]}
id='channel_modal.headerHelp'
@ -382,5 +418,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 10,
marginHorizontal: 15,
},
headerHelpText: {
zIndex: -1,
},
};
});

View file

@ -64,4 +64,31 @@ describe('EditChannelInfo', () => {
expect(instance.enableRightButton).toHaveBeenCalledTimes(1);
expect(instance.enableRightButton).toHaveBeenCalledWith(true);
});
test('should call scrollHeaderToTop', () => {
const wrapper = shallow(
<EditChannelInfo {...baseProps}/>
);
const instance = wrapper.instance();
instance.scrollHeaderToTop = jest.fn();
expect(instance.scrollHeaderToTop).not.toHaveBeenCalled();
wrapper.setState({keyboardVisible: false});
instance.onHeaderFocus();
expect(instance.scrollHeaderToTop).not.toHaveBeenCalled();
wrapper.setState({keyboardVisible: true});
instance.onHeaderFocus();
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(1);
wrapper.setState({headerHasFocus: false});
instance.onKeyboardDidShow();
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(1);
wrapper.setState({headerHasFocus: true});
instance.onKeyboardDidShow();
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(2);
});
});