Jim
Jim

Reputation: 2322

How to prevent auto-grow text input container from growing behind keyboard in react-native

The goal: create a text input field that responsively expands as input lines expand, up to a certain point, like most messaging apps.

The problem: as input expands past 3 or 4 lines, instead of the top of the input container expanding upwards, the input container expands based on content size, as it should, but the container is repositioned behind the keyboard/keyboard word suggestions, which re-positions the button on the left (part of the input container) as well as the <TextInput> itself.

the picture below shows how I want the padding to be on the bottom. the "lower container" embodies the button and the input field. the off-grey color is the top of the keyboard suggestions (iOS)

enter image description here

The image below shows the undesired behavior. you can see the bottom border of the input, as well as the button start to disappear behind the suggestion box/keyboard. If the input kept expanding, the bottom border and button would progressively become less visible.

enter image description here

Here is the whole screen component:

render () {
    return (
        <KeyboardAvoidingView
            style={{ flex: 1, backgroundColor: Colors.PRIMARY_OFF_WHITE,
            }}
            behavior={Platform.OS === 'ios' ? 'position' : null}
            keyboardVerticalOffset={Platform.OS === 'ios' ?hp("11%") : 0}
        >

        /* Upper Container */
        <View style={{ justifyContent: 'flex-start', height: height*0.8, paddingBottom: 10 }}>
            <FlatList
                data={this.props.messages}
                ref={'list'}
                onContentSizeChange={() => this.refs.list.scrollToEnd()}
                keyExtractor={(item) => {
                    return (
                        item.toString() +
                        new Date().getTime().toString() +
                        Math.floor(Math.random() * Math.floor(new Date().getTime())).toString()
                    );
                }}
                renderItem={({ item, index }) => {
                    return (
                        <Components.ChatMessage
                            isSender={item.sender == this.props.uid ? true : false}
                            message={item.content.data}
                            read={item.read}
                            time={item.time}
                            onPress={() =>
                                this.props.readMsg(
                                    {
                                        id: this.props.uid,
                                        role: this.props.role,
                                        convoId: this.state.ownerConvoId
                                    },
                                    item.docId,
                                    this.props.navigation.state.params.uid
                                )}
                         />
                      );
                  }}                    
                />
                </View>

            /* Lower Container */
                <View
                    style={{
                        height: this.state.height+20,
                        flexDirection: 'row',
                        alignItems: 'center',
                        justifyContent:"space-around",
                        paddingVertical: 10
                    }}
                >
                    <Components.OptionFan
                        options={[
                            {
                                icon: require('../assets/img/white-voice.png'),
                                onPress: () => {
                                  this.props.navigation.navigate('Schedule', {
                                        receiver: this.conversants.receiver,
                                        sender: this.conversants.sender,
                                        type: 'Voice'
                                    });
                                },
                                key: 1
                             },
                             {
                                icon: require('../assets/img/white-video.png'),
                                onPress: () => {
                                    this.props.navigation.navigate('Schedule', {
                                        receiver: this.conversants.receiver,
                                        sender: this.conversants.sender,
                                        type: 'Video'
                                    });
                                },
                                key: 2
                              }
                          ]}
                      />


                        <TextInput
                            multiline={true}
                            textAlignVertical={'center'}
                            onChangeText={(text) => this.setState({ text })}
                            onFocus={() => this.refs.list.scrollToEnd()}
                            onContentSizeChange={(event) =>
                                this.setState({
                                    height:
                                        event.nativeEvent.contentSize.height <= 40
                                            ? 40
                                            : event.nativeEvent.contentSize.height
                                })}
                            style={{
                                height: Math.max(40, this.state.height),
                                width: wp('80%'),
                                borderWidth: 1,
                                alignSelf: this.state.height > 40 ? "flex-end":null,
                                borderColor: Colors.PRIMARY_GREEN,
                                borderRadius: 18,
                                paddingLeft: 15,
                                paddingTop: 10,
                                paddingRight: 15
                            }}
                            ref={'input'}
                            blurOnSubmit={true}
                            returnKeyType={'send'}
                            placeholder={'Write a message...'}
                            onSubmitEditing={() => {
                                if (this.props.role == 'Influencer') {
                                    this.send();
                                    this.refs.input.clear();
                                } else {

                                    this.setState({ visible: true });
                                }
                            }}
                        />
                    </View>
            </KeyboardAvoidingView>
        );
    }

How can I go about implementing an outgrow that stays in view with the desired padding on the bottom?

EDIT: to be clear, this question is not about how to make an auto-grow input field. the code above already accomplishes that. its about the container the auto-grow input field is in, and how the parent container, input field, and button grow behind the the keyboard/keyboard suggestions. the Goal is to have the text input always display directly above the keyboard, with the bottom of the input/button/container consistently the same space between the keyboard, while the height expands upward only.

the question posted here is similar: In React Native, How to have multiple multiline textinputs that don't hide behind the keyboard in one scrollable form?

Upvotes: 1

Views: 4518

Answers (2)

Jim
Jim

Reputation: 2322

As an additional note, when factoring in dynamic heights of portions of your screen, always remember to account for status bars (both iOS and android obviously) as well as the pesky Soft Android Navigation Bar at the bottom of the screen, as its not factored in when using the Dimension api for 'window'. My mistake to ignore this.

to get Status bar and nav bar:

import {NativeModules, Platform} from 'react-native';

const { StatusBarManager } = NativeModules; // Note: StatusBar API is not good used here because of platform inconsistencies.

// Note: 'window' does not contain Android/iOS status bar or Android soft Navigation Bar
const WINDOW_HEIGHT = Dimensions.get('window').height;

// 'screen' gets full hardware dimension
const DEVICE_HEIGHT = Dimensions.get('screen').height;
const STATUS_BAR_HEIGHT = Platform.OS === 'ios' ? 20 : StatusBarManager.HEIGHT;
const ANDROID_NAV_HEIGHT = Platform.OS === 'ios' ? 0 : DEVICE_HEIGHT -  (WINDOW_HEIGHT + STATUSBAR_HEIGHT);

Upvotes: 0

nipuna-g
nipuna-g

Reputation: 6652

You can set the parent container to be '100%' height and resize the upper content so that the input container always grows upwards.

A simple implementation of this is as follows. textInputHeight is taken from the state and it is set when TextInput resizes.

  return (
    <KeyboardAvoidingView
      style={{height: '100%'}}
      behavior={Platform.OS === 'ios' ? 'position' : undefined}
      keyboardVerticalOffset={100}>
      <ScrollView
        style={{
          backgroundColor: '#777',
          height: height - (80 + textInputHeight),
        }}>
        <View style={{height: 1000}} />
      </ScrollView>
      <TextInput
        multiline={true}
        onLayout={(event) => {
          if (textInputHeight === 0) {
            setTextInputHeight(event.nativeEvent.layout.height);
          }
        }}
        onContentSizeChange={(event) => {
          setTextInputHeight(event.nativeEvent.contentSize.height);
        }}
      />
    </KeyboardAvoidingView>
  );

Upvotes: 2

Related Questions