Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Keyboard disable and autogrow functionality to TextInput component #7318

Merged
merged 19 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 124 additions & 77 deletions src/components/TextInput/BaseTextInput.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'underscore';
import React, {Component} from 'react';
import {
Animated, View, TouchableWithoutFeedback, Pressable,
Animated, View, TouchableWithoutFeedback, Pressable, AppState, Keyboard,
} from 'react-native';
import Str from 'expensify-common/lib/str';
import TextInputLabel from './TextInputLabel';
Expand All @@ -13,6 +13,8 @@ import * as Expensicons from '../Icon/Expensicons';
import InlineErrorText from '../InlineErrorText';
import * as styleConst from './styleConst';
import TextInputWithName from '../TextInputWithName';
import Text from '../Text';
import * as StyleUtils from '../../styles/StyleUtils';

class BaseTextInput extends Component {
constructor(props) {
Expand All @@ -26,6 +28,7 @@ class BaseTextInput extends Component {
labelTranslateY: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y),
labelScale: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE),
passwordHidden: props.secureTextEntry,
textInputWidth: 0,
};

this.input = null;
Expand All @@ -35,9 +38,17 @@ class BaseTextInput extends Component {
this.onBlur = this.onBlur.bind(this);
this.setValue = this.setValue.bind(this);
this.togglePasswordVisibility = this.togglePasswordVisibility.bind(this);
this.dismissKeyboardWhenBackgrounded = this.dismissKeyboardWhenBackgrounded.bind(this);
}

componentDidMount() {
if (this.props.disableKeyboard) {
this.appStateSubscription = AppState.addEventListener(
parasharrajat marked this conversation as resolved.
Show resolved Hide resolved
'change',
this.dismissKeyboardWhenBackgrounded,
);
}

// We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514
if (!this.props.autoFocus || !this.input) {
return;
Expand All @@ -46,13 +57,14 @@ class BaseTextInput extends Component {
this.input.focus();
}

componentDidUpdate(prevProps) {
// activate or deactivate the label when value is changed programmatically from outside
if (prevProps.value === this.props.value) {
componentDidUpdate() {
// Activate or deactivate the label when value is changed programmatically from outside
if (this.value === this.props.value) {
return;
}

this.value = this.props.value;
this.input.setNativeProps({text: this.value});

if (this.props.value) {
this.activateLabel();
Expand All @@ -61,6 +73,15 @@ class BaseTextInput extends Component {
}
}

componentWillUnmount() {
if (!this.props.disableKeyboard || !this.appStateSubscription) {
return;
}

this.appStateSubscription.remove();
parasharrajat marked this conversation as resolved.
Show resolved Hide resolved
}


onPress(event) {
if (this.props.disabled) {
return;
Expand Down Expand Up @@ -123,6 +144,14 @@ class BaseTextInput extends Component {
this.isLabelActive = false;
}

dismissKeyboardWhenBackgrounded(nextAppState) {
if (!nextAppState.match(/inactive|background/)) {
return;
}

Keyboard.dismiss();
parasharrajat marked this conversation as resolved.
Show resolved Hide resolved
}

animateLabel(translateY, scale) {
Animated.parallel([
Animated.spring(this.state.labelTranslateY, {
Expand All @@ -147,84 +176,102 @@ class BaseTextInput extends Component {
const inputProps = _.omit(this.props, _.keys(baseTextInputPropTypes.propTypes));
const hasLabel = Boolean(this.props.label.length);
return (
<View>
<View
style={[
!this.props.multiline && styles.componentHeightLarge,
...this.props.containerStyles,
]}
>
<TouchableWithoutFeedback onPress={this.onPress} focusable={false}>
<View
style={[
styles.textInputContainer,
this.state.isFocused && styles.borderColorFocus,
this.props.errorText && styles.borderColorDanger,
]}
>
{hasLabel ? (
<>
{/* Adding this background to the label only for multiline text input,
<>
<View>
<View
style={[
!this.props.multiline && styles.componentHeightLarge,
...this.props.containerStyles,
]}
>
<TouchableWithoutFeedback onPress={this.onPress} focusable={false}>
<View
style={[
styles.textInputContainer,
...this.props.textInputContainerStyles,
this.props.autoGrow && StyleUtils.getAutoGrowTextInputStyle(this.state.textInputWidth),
!this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus,
(this.props.hasError || this.props.errorText) && styles.borderColorDanger,
]}
>
{hasLabel ? (
<>
{/* Adding this background to the label only for multiline text input,
to prevent text overlapping with label when scrolling */}
{this.props.multiline && <View style={styles.textInputLabelBackground} pointerEvents="none" />}
<TextInputLabel
label={this.props.label}
labelTranslateY={this.state.labelTranslateY}
labelScale={this.state.labelScale}
for={this.props.nativeID}
/>
</>
) : null}
<View style={[styles.textInputAndIconContainer]}>
<TextInputWithName
ref={(ref) => {
if (typeof this.props.innerRef === 'function') { this.props.innerRef(ref); }
this.input = ref;
}}
// eslint-disable-next-line
{...inputProps}
value={this.props.isFormInput ? undefined : this.value}
defaultValue={this.props.defaultValue}
placeholder={(this.state.isFocused || !this.props.label) ? this.props.placeholder : null}
placeholderTextColor={themeColors.placeholderText}
underlineColorAndroid="transparent"
style={[
this.props.inputStyle,
styles.flex1,
styles.w100,
!hasLabel && styles.pv0,
this.props.secureTextEntry && styles.secureInput,
]}
multiline={this.props.multiline}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChangeText={this.setValue}
secureTextEntry={this.state.passwordHidden}
onPressOut={this.props.onPress}
name={this.props.name}
/>
{this.props.secureTextEntry && (
<Pressable
accessibilityRole="button"
style={styles.secureInputEyeButton}
onPress={this.togglePasswordVisibility}
>
<Icon
src={this.state.passwordHidden ? Expensicons.Eye : Expensicons.EyeDisabled}
fill={themeColors.icon}
{this.props.multiline && <View style={styles.textInputLabelBackground} pointerEvents="none" />}
<TextInputLabel
label={this.props.label}
labelTranslateY={this.state.labelTranslateY}
labelScale={this.state.labelScale}
for={this.props.nativeID}
/>
</Pressable>
)}
</>
) : null}
<View style={[styles.textInputAndIconContainer]}>
<TextInputWithName
ref={(ref) => {
if (typeof this.props.innerRef === 'function') { this.props.innerRef(ref); }
this.input = ref;
}}
// eslint-disable-next-line
{...inputProps}
defaultValue={this.value}
placeholder={(this.state.isFocused || !this.props.label) ? this.props.placeholder : null}
placeholderTextColor={themeColors.placeholderText}
underlineColorAndroid="transparent"
style={[
styles.flex1,
styles.w100,
this.props.inputStyle,
!hasLabel && styles.pv0,
this.props.secureTextEntry && styles.pr2,
]}
multiline={this.props.multiline}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChangeText={this.setValue}
secureTextEntry={this.state.passwordHidden}
onPressOut={this.props.onPress}
name={this.props.name}
showSoftInputOnFocus={!this.props.disableKeyboard}
/>
{this.props.secureTextEntry && (
<Pressable
accessibilityRole="button"
style={styles.secureInputEyeButton}
onPress={this.togglePasswordVisibility}
>
<Icon
src={this.state.passwordHidden ? Expensicons.Eye : Expensicons.EyeDisabled}
fill={themeColors.icon}
/>
</Pressable>
)}
</View>
</View>
</View>
</TouchableWithoutFeedback>
</TouchableWithoutFeedback>
</View>
{!_.isEmpty(this.props.errorText) && (
<InlineErrorText>
{this.props.errorText}
</InlineErrorText>
)}
</View>
{!_.isEmpty(this.props.errorText) && (
<InlineErrorText>
{this.props.errorText}
</InlineErrorText>
{/*
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 component is intentionally positioned out of the screen.
*/}
{this.props.autoGrow && (
<Text
style={[...this.props.inputStyle, styles.hiddenElementOutsideOfWindow]}
onLayout={e => this.setState({textInputWidth: e.nativeEvent.layout.width})}
>
{this.props.value || this.props.placeholder}
</Text>
)}
</View>
</>
);
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/components/TextInput/baseTextInputPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const propTypes = {
errorText: PropTypes.string,

/** Customize the TextInput container */
textInputContainerStyles: PropTypes.arrayOf(PropTypes.object),

/** Customize the main container */
containerStyles: PropTypes.arrayOf(PropTypes.object),

/** input style */
Expand All @@ -32,6 +35,18 @@ const propTypes = {
/** Should the input auto focus? */
autoFocus: PropTypes.bool,

/** Disable the virtual keyboard */
disableKeyboard: PropTypes.bool,

/** Autogrow input container size based on the entered text */
autoGrow: PropTypes.bool,

/** Hide the focus styles on TextInput */
hideFocusedState: PropTypes.bool,

/** Forward the inner ref */
innerRef: PropTypes.func,

/** Indicates that the input is being used with the Form component */
isFormInput: PropTypes.bool,

Expand All @@ -55,6 +70,7 @@ const defaultProps = {
placeholder: '',
hasError: false,
containerStyles: [],
textInputContainerStyles: [],
inputStyle: [],
autoFocus: false,

Expand All @@ -65,6 +81,10 @@ const defaultProps = {
value: undefined,
defaultValue: undefined,
forceActiveLabel: false,
disableKeyboard: false,
autoGrow: false,
hideFocusedState: false,
innerRef: () => {},
shouldSaveDraft: false,
};

Expand Down
49 changes: 38 additions & 11 deletions src/components/TextInput/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
import React, {forwardRef} from 'react';
import React from 'react';
import _ from 'underscore';
import styles from '../../styles/styles';
import BaseTextInput from './BaseTextInput';
import * as baseTextInputPropTypes from './baseTextInputPropTypes';

const TextInput = forwardRef((props, ref) => (
<BaseTextInput
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
inputStyle={[styles.baseTextInput, styles.textInputDesktop]}
/>
));
class TextInput extends React.Component {
componentDidMount() {
if (!this.props.disableKeyboard) {
return;
}

this.textInput.setNativeProps({inputmode: 'none'});
parasharrajat marked this conversation as resolved.
Show resolved Hide resolved
}

render() {
return (
<BaseTextInput
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
innerRef={(el) => {
this.textInput = el;
if (!this.props.innerRef) {
return;
}

if (_.isFunction(this.props.innerRef)) {
this.props.innerRef(el);
return;
}

this.props.innerRef.current = el;
}}
inputStyle={[styles.baseTextInput, styles.textInputDesktop, ...this.props.inputStyle]}
/>
);
}
}

TextInput.propTypes = baseTextInputPropTypes.propTypes;
TextInput.defaultProps = baseTextInputPropTypes.defaultProps;
TextInput.displayName = 'TextInput';

export default TextInput;
export default React.forwardRef((props, ref) => (
/* eslint-disable-next-line react/jsx-props-no-spreading */
<TextInput {...props} innerRef={ref} />
));
2 changes: 1 addition & 1 deletion src/components/TextInput/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const TextInput = forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-multi-spaces
autoCompleteType={props.autoCompleteType === 'new-password' ? 'password' : props.autoCompleteType}
innerRef={ref}
inputStyle={[styles.baseTextInput]}
inputStyle={[styles.baseTextInput, ...props.inputStyle]}
/>
));

Expand Down
Loading