[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:
parent
75e0854411
commit
2393eb3283
4 changed files with 117 additions and 35 deletions
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue