diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 61d72d4ed06c..12211de67ba3 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import React, {Component} from 'react'; import { - Animated, View, TouchableWithoutFeedback, AppState, Keyboard, + Animated, View, TouchableWithoutFeedback, AppState, Keyboard, StyleSheet, } from 'react-native'; import Str from 'expensify-common/lib/str'; import RNTextInput from '../RNTextInput'; @@ -219,21 +219,36 @@ class BaseTextInput extends Component { !this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus, (this.props.hasError || this.props.errorText) && styles.borderColorDanger, ], (finalStyles, s) => ({...finalStyles, ...s}), {}); + const maxHeight = StyleSheet.flatten(this.props.containerStyles).maxHeight; + const isMultiline = this.props.multiline || this.props.autoGrowHeight; return ( <> !this.props.multiline && this.setState({height: event.nativeEvent.layout.height})} + // When autoGrowHeight is true we calculate the width for the textInput, so It will break lines properly + // or if multiline is not supplied we calculate the textinput height, using onLayout. + onLayout={(event) => { + if (!this.props.autoGrowHeight && this.props.multiline) { + return; + } + + const layout = event.nativeEvent.layout; + + this.setState(prevState => ({ + width: this.props.autoGrowHeight ? layout.width : prevState.width, + height: !isMultiline ? layout.height : prevState.height, + })); + }} style={[ textInputContainerStyles, @@ -245,7 +260,7 @@ class BaseTextInput extends Component { <> {/* Adding this background to the label only for multiline text input, to prevent text overlapping with label when scrolling */} - {this.props.multiline && } + {isMultiline && } @@ -295,15 +310,18 @@ class BaseTextInput extends Component { styles.flex1, styles.w100, this.props.inputStyle, - (!hasLabel || this.props.multiline) && styles.pv0, + (!hasLabel || isMultiline) && styles.pv0, this.props.prefixCharacter && StyleUtils.getPaddingLeft(this.state.prefixWidth + styles.pl1.paddingLeft), this.props.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) - !this.props.multiline && {height: this.state.height, lineHeight: undefined}, + !isMultiline && {height: this.state.height, lineHeight: undefined}, + + // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. + this.props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(this.state.textInputHeight, maxHeight), ]} - multiline={this.props.multiline} + multiline={isMultiline} maxLength={this.props.maxLength} onFocus={this.onFocus} onBlur={this.onBlur} @@ -318,7 +336,7 @@ class BaseTextInput extends Component { // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: this.props.multiline && this.props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && this.props.submitOnEnter}} /> {Boolean(this.props.secureTextEntry) && ( @@ -352,15 +370,15 @@ class BaseTextInput extends Component { {/* Text input component doesn't support auto grow by default. We're using a hidden text input to achieve that. - This text view is used to calculate width of the input value given textStyle in this component. + This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {this.props.autoGrow && ( + {(this.props.autoGrow || this.props.autoGrowHeight) && ( // Add +2 to width so that the first digit of amount do not cut off on mWeb - https://github.com/Expensify/App/issues/8158. this.setState({textInputWidth: e.nativeEvent.layout.width + 2})} + style={[...this.props.inputStyle, this.props.autoGrowHeight ? {maxWidth: this.state.width} : {}, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden]} + onLayout={e => this.setState({textInputWidth: e.nativeEvent.layout.width + 2, textInputHeight: e.nativeEvent.layout.height})} > {this.state.value || this.props.placeholder} diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 4e82f0984670..d7506322842a 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -40,9 +40,12 @@ const propTypes = { /** Disable the virtual keyboard */ disableKeyboard: PropTypes.bool, - /** Autogrow input container size based on the entered text */ + /** Autogrow input container length based on the entered text */ autoGrow: PropTypes.bool, + /** Autogrow input container height based on the entered text */ + autoGrowHeight: PropTypes.bool, + /** Hide the focus styles on TextInput */ hideFocusedState: PropTypes.bool, @@ -108,6 +111,7 @@ const defaultProps = { forceActiveLabel: false, disableKeyboard: false, autoGrow: false, + autoGrowHeight: false, hideFocusedState: false, innerRef: () => {}, shouldSaveDraft: false, diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index f91da6c12a8d..5123a7b9d8a2 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -195,9 +195,8 @@ class WorkspaceInviteMessagePage extends React.Component { label={this.props.translate('workspace.inviteMessage.personalMessagePrompt')} autoCompleteType="off" autoCorrect={false} - numberOfLines={4} + autoGrowHeight textAlignVertical="top" - multiline containerStyles={[styles.workspaceInviteWelcome]} defaultValue={this.state.welcomeNote} value={this.state.welcomeNote} diff --git a/src/stories/TextInput.stories.js b/src/stories/TextInput.stories.js index 7472f8bc2848..07c4273193b3 100644 --- a/src/stories/TextInput.stories.js +++ b/src/stories/TextInput.stories.js @@ -69,6 +69,17 @@ AutoGrowInput.args = { }], }; +const AutoGrowHeightInput = Template.bind({}); +AutoGrowHeightInput.args = { + label: 'Autogrowheight input', + name: 'AutoGrowHeight', + placeholder: 'My placeholder text', + autoGrowHeight: true, + textInputContainerStyles: [{ + maxHeight: 115, + }], +}; + const PrefixedInput = Template.bind({}); PrefixedInput.args = { label: 'Prefixed input', @@ -118,6 +129,7 @@ export { ForceActiveLabel, PlaceholderInput, AutoGrowInput, + AutoGrowHeightInput, PrefixedInput, MaxLengthInput, HintAndErrorInput, diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index b2cd47beace9..63a3841288c6 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -300,6 +300,24 @@ function getWidthStyle(width) { }; } +/** + * Returns auto grow height text input style + * + * @param {Number} textInputHeight + * @param {Number} maxHeight + * @returns {Object} + */ +function getAutoGrowHeightInputStyle(textInputHeight, maxHeight) { + if (textInputHeight > maxHeight) { + return styles.overflowAuto; + } + + return { + ...styles.overflowHidden, + height: maxHeight, + }; +} + /** * Returns a style with backgroundColor and borderColor set to the same color * @@ -1103,6 +1121,7 @@ export { getZoomCursorStyle, getZoomSizingStyle, getWidthStyle, + getAutoGrowHeightInputStyle, getBackgroundAndBorderStyle, getBackgroundColorStyle, getBackgroundColorWithOpacityStyle, diff --git a/src/styles/styles.js b/src/styles/styles.js index d5cc518300dc..ac8ccda52068 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -800,6 +800,11 @@ const styles = { backgroundColor: themeColors.buttonDefaultBG, }, + autoGrowHeightInputContainer: (textInputHeight, maxHeight) => ({ + height: textInputHeight >= maxHeight ? maxHeight : textInputHeight, + minHeight: variables.componentSizeLarge, + }), + textInputContainer: { flex: 1, justifyContent: 'center', @@ -808,6 +813,7 @@ const styles = { borderBottomWidth: 2, borderColor: themeColors.border, overflow: 'hidden', + scrollPaddingTop: '100%', }, textInputLabel: { @@ -848,6 +854,7 @@ const styles = { paddingTop: 23, paddingBottom: 8, paddingLeft: 0, + paddingRight: 0, borderWidth: 0, }, @@ -2566,7 +2573,7 @@ const styles = { }, workspaceInviteWelcome: { - minHeight: 115, + maxHeight: 115, }, peopleRow: {