facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
117.77k stars 24.16k forks source link

KeyboardAvoidingView has no effect on multiline TextInput #16826

Open peacechen opened 6 years ago

peacechen commented 6 years ago

KeyboardAvoidingView only works with single-line TextInputs. When the multiline prop is set, KeyboardAvoidingView does not shift the TextInput at all.

Is this a bug report?

Yes

Have you read the Contributing Guidelines?

Yes

Environment

Environment: OS: macOS Sierra 10.12.6 Node: 7.0.0 npm: 3.10.8 Watchman: 4.7.0 Xcode: 9.1

Packages: (wanted => installed) react-native: 0.49.3 react: 16.0.0-beta.5

Target Platform: iOS (10.3)

Steps to Reproduce

  1. Use a <TextInput> component with multiline prop set.
  2. Wrap this in a ScrollView
  3. Wrap that in a KeyboardAvoidingView.

Expected Behavior

Multiline TextInput should scroll above the soft keyboard.

Actual Behavior

Soft keyboard covers multiline TextInput.

Reproducible Demo

import React, { Component } from 'react';
import { Text, TextInput, View, ScrollView, KeyboardAvoidingView, Keyboard} from 'react-native';

...

    render() {
        return (
            <KeyboardAvoidingView style={{flex:1}} behavior="padding" keyboardVerticalOffset={64}>
                <ScrollView keyboardShouldPersistTaps={'handled'}>
                    <View style={{padding: 12}}>
                        // various content to fill the page
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 1</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 2</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 3</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 4</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 5</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 6</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 7</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 8</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 9</Text>
                        <Text style={{fontSize: 20, padding: 40}}>MESSAGE 10</Text>
                    </View>
                    <TextInput
                        style={{padding: 4}}
                        multiline={true}
                        placeholder={'Type something here...'}
                        onChangeText={this.updateMessage}
                        value={this.state.message}
                    />
                </ScrollView>
            </KeyboardAvoidingView>
        );
    }
raygerrard commented 4 years ago

This is still an issue on 0.61.4

thijs-qv commented 4 years ago

No fix yet? This issue is over two (2) years old now... Edit: To make this a more helpful post: I've achieved what I need by using the react-native-keyboard-aware-scroll-view library and adding the scrollEnabled={false} prop to the TextInput.

luco commented 4 years ago

@bob76828 Super thanks a lot man!!!

stale[bot] commented 4 years ago

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

dozoisch commented 4 years ago

Not stale

kuldeepworkid commented 4 years ago

Same issue, here's a simple snack for anyone to play with: https://snack.expo.io/r1qpj0k5Q

I've tried this and it works:

https://github.com/baijunjie/react-native-input-scroll-view

This Really worked for me. Thanks

warrenlalata commented 4 years ago

still unfixed 😭

willymulyana commented 4 years ago

If anyone still is looking for a fix, you could try

<View style={{position:relative}}>
    <TextInput ref='multilineText' value={this.state.textValue} 
        onChangeText={(textValue) => this.setState({textValue})}/>
    <TextInput style={{ color:'transparent', position: 'absolute', width: '100%', height: '100%' }}
        placeholderTextColor='transparent'
        onFocus={() => setTimeout(() => this.refs.multilineText.focus(), 1000) } 
        value={this.state.textValue}
        onChangeText={(textValue) => this.setState({textValue})}
    />
</View>

Its a dirty hack but it does the job, what it does it when user clicks on the input it would be clicking on the single line and the keyboard will scroll to that line then after 1 seconds (you can adjust this, i just found 1 seconds to be good with our app) it would focus on the multiline. The value and onChangeText makes sure that when the user star typing they would have the same value except the single line has transparent text.

The downside is when a user clicked the multiple line, the cursor wont go to the location they clicked but it would go to the end.

Thanks, it works for me. I add caretHidden={true} for transparent TextInput to hide its cursor.

colinfwren commented 4 years ago

If anyone still is looking for a fix, you could try

<View style={{position:relative}}>
    <TextInput ref='multilineText' value={this.state.textValue} 
        onChangeText={(textValue) => this.setState({textValue})}/>
    <TextInput style={{ color:'transparent', position: 'absolute', width: '100%', height: '100%' }}
        placeholderTextColor='transparent'
        onFocus={() => setTimeout(() => this.refs.multilineText.focus(), 1000) } 
        value={this.state.textValue}
        onChangeText={(textValue) => this.setState({textValue})}
    />
</View>

Its a dirty hack but it does the job, what it does it when user clicks on the input it would be clicking on the single line and the keyboard will scroll to that line then after 1 seconds (you can adjust this, i just found 1 seconds to be good with our app) it would focus on the multiline. The value and onChangeText makes sure that when the user star typing they would have the same value except the single line has transparent text.

The downside is when a user clicked the multiple line, the cursor wont go to the location they clicked but it would go to the end.

From an automation perspective (using Appium) this works, you just need to make sure you put the testID/accessibilityLabel on the hidden input so that the actions are carried out against that instead of the visible one.

I had to reduce the timeout to 100ms instead of the full second as this was causing issues with the text entry as it would miss characters.

And if you use hideKeyboard us the 'tapOutside' strategy as the default one which uses return just creates a new line

nguyenduy2392 commented 3 years ago

textinput will hidden when input too many line on scroll view? it only android. i must allway add height for textinput any solution for this bug ?

<ScrollView>
                    <View style={styles.container}>
                        <TextInput
                            onChangeText={(text) => {
                                this.setState({ moTa: text })
                            }}
                            value={this.state.moTa}
                            multiline={true}
                            scrollEnabled={false}
                            placeholder='Nhập thông tin mô tả'
                            style={styles.input}
                        />
                    </View>
                </ScrollView>

input: {                     
        ...(Platform.OS == 'android' ? { height: themes.screen.height - 400 } : {}),
        fontSize: 18,
        textAlignVertical: 'top', fontFamily: themes.fontFamily
    },
a-eid commented 3 years ago

this still is an issue with 63.4, it's weird that this issue still opened since 2017 and there is no proper workaround.

Sujan4k0 commented 3 years ago

Bumping this - setting scrollEnabled to false works, but it seems like an indirect fix to an underlying issue.

castalonirenz commented 3 years ago

i tried setting scrollEnabled to false but when you keep typing it return an error.

image

minhna commented 3 years ago

This works best for me:

import { SafeAreaView } from 'react-native-safe-area-context';

<SafeAreaView style={{ flex: 1 }}>
  <ScrollView>
    <TextInput
      multiline
      scrollEnabled={false}
    />
  </ScrollView>
</SafeAreaView>
aprilmintacpineda commented 3 years ago

This is still an issue, I'm using React-native 0.64.2, putting scrollEnabled={false} is not really a good idea when you are using a text input with a fixed height, because you want the user to actually be able to scroll through the text input.

boblitex commented 2 years ago

no proper workaround since 2017? damn

darpsoft commented 2 years ago

This worked for me

import React from "react";
import { Platform, TextInput, KeyboardAvoidingView, ScrollView } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

const Component = () => {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={["right", "bottom", "left"]}>
      <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"}>
        <ScrollView>
          <TextInput multiline scrollEnabled={false} />
        </ScrollView>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};
jetako commented 2 years ago

This solution leverages the scrollEnabled hack but reenables scroll after the keyboard appears. You just need to set the height precisely because partial lines are hidden when scrollEnabled={false}

import React, { useState, useRef, forwardRef } from 'react'
import { TextInput } from 'react-native'

export const TextArea = forwardRef((props, ref) => {
  const { onFocus, onBlur, ...inputProps } = props
  const [scrollEnabled, setScrollEnabled] = useState(false)
  const scrollEnabledTimerRef = useRef()

  return (
    <TextInput
      ref={ref}
      multiline
      scrollEnabled={scrollEnabled}
      onFocus={e => {
        scrollEnabledTimerRef.current = setTimeout(() => setScrollEnabled(true), 800)
        onFocus?.(e)
      }}
      onBlur={(e) => {
        clearTimeout(scrollEnabledTimerRef.current)
        setScrollEnabled(false)
        onBlur?.(e)
      }}
      {...inputProps}
    />
  )
})
tgwow commented 2 years ago

Setting multiline={true} and scrollEnabled={false} to <TextInput/> works fine for me!

aprilmintacpineda commented 2 years ago

Setting multiline={true} and scrollEnabled={false} to <TextInput/> works fine for me!

But you won't be able to scroll if the text overflows.

glintpursuit commented 2 years ago

it is working;

<KeyboardAvoidingView behavior="padding" style={Platform.OS !== 'android' && { flex: 1 }}> 
  <ScrollView>
      ... 
      <TextInput ... />
      ...
  </ScrollView>
</KeyboardAvoidingView>

Thanks

When using multiline textInput inside flatlist scrollEnabled={false} is the solution

vanenshi commented 2 years ago

We need a hero

jimuelpalaca commented 2 years ago

This worked for me

import React from "react";
import { Platform, TextInput, KeyboardAvoidingView, ScrollView } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

const Component = () => {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={["right", "bottom", "left"]}>
      <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"}>
        <ScrollView>
          <TextInput multiline scrollEnabled={false} />
        </ScrollView>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

This one works for me too.

yayobyte commented 2 years ago

This worked for me

import React from "react";
import { Platform, TextInput, KeyboardAvoidingView, ScrollView } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

const Component = () => {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={["right", "bottom", "left"]}>
      <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"}>
        <ScrollView>
          <TextInput multiline scrollEnabled={false} />
        </ScrollView>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

This worked for me as well guys

divasilevski commented 2 years ago

Can use keyboard!

import React from "react";
import { Keyboard } from "react-native";

const Component = () => {
  const [isScrollEnabled, setIsScrollEnabled] = React.useState(true);

  function onKeyboardWillShow() {
    setIsScrollEnabled(false);
  }

  function onKeyboardDidShow() {
    setIsScrollEnabled(true);
  }

  React.useEffect(() => {
    const subKWS = Keyboard.addListener("keyboardWillShow", onKeyboardWillShow);
    const subKDS = Keyboard.addListener("keyboardDidShow", onKeyboardDidShow);

    return () => {
      subKWS.remove();
      subKDS.remove();
    };
  }, []);

  return (
    <TextInput style={{ maxHeight: 180 }} scrollEnabled={isScrollEnabled} multiline>
  );
}; 

I love react native 🥲

Kasik-D commented 2 years ago

Hi guys, after 2 days suffers I found a solution

Preview

But you will need to connect library react-native-keyboard-aware-scroll-view

If you use Expo, go to app.json and write "softwareKeyboardLayoutMode": "pan" in android If you use CLI you need find similar solution

Let's start

Main component -> ChatComponent

import { Box } from 'native-base';
import React from 'react';
import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context';

import { useChat } from '../../hooks';
import { chatThemeType } from '../../types/chat';
import { pageEmpty } from '../../types/pager-view';
import { RequestHandler } from '../request-handler/request-handler';
import { ChatBottomInput } from './chat-bottom-input/chat-bottom-input';
import { ChatList } from './chat-list/chat-list';

type Props = {
  theme: chatThemeType;
  EmptypageData: pageEmpty;
};

export const ChatComponent = ({ theme, EmptypageData }: Props) => {
  const { chatLoading } = useChat();

  const scrollRef = React.useRef<KeyboardAwareScrollView>(null);

  return (
    <SafeAreaView edges={['bottom']} style={styles.safeAreaViewStyle}>
      <KeyboardAvoidingView
        style={styles.keyboardAvoidingViewStyle}
        behavior={Platform.OS === 'ios' ? undefined : 'height'}
      >
        <RequestHandler loading={chatLoading}>
          <KeyboardAwareScrollView
            ref={scrollRef}
            contentContainerStyle={styles.keyBoadStyle}
            scrollEnabled={false}
            nestedScrollEnabled={true}
          >
            <Box flexGrow={1}>
              <ChatList theme={theme} EmptypageData={EmptypageData} />

              <ChatBottomInput theme={theme} scrollRef={scrollRef} />
            </Box>
          </KeyboardAwareScrollView>
        </RequestHandler>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  keyBoadStyle: {
    backgroundColor: '#FFFFFF',
    flex: 1,
  },
  keyboardAvoidingViewStyle: {
    flex: 1,
  },
  safeAreaViewStyle: {
    backgroundColor: '#D5EDD8',
    flex: 1,
  },
});

Next ChatList

import { Box } from 'native-base';
import React from 'react';
import { FlatList, StyleSheet } from 'react-native';

import { Message, UserRole } from '../../../graphql/generated';
import { useAuth, useChat } from '../../../hooks';
import { chatThemeType } from '../../../types/chat';
import { pageEmpty } from '../../../types/pager-view';
import { EmptyListComponent } from '../../empty-list-component/empty-list-component';
import { ChatMessage } from '../chat-message/chat-message';

type Props = {
  EmptypageData: pageEmpty;
  theme: chatThemeType;
};

export const ChatList = React.memo(({ EmptypageData, theme }: Props) => {
  const { currentUserData } = useAuth();

  const { messages } = useChat();

  const renderItem = ({ item }: { item: Message }) => (
    <ChatMessage
      text={item.text}
      date={item.createdAt}
      theme={theme.chatMessage}
      id={item.id}
      isMymessage={currentUserData?.id === item.user.id}
      isAdmin={currentUserData?.role === UserRole.Admin}
    />
  );

  const keyExtractor = (item: Message, index: number) -> (item.id || index).toString();

  return (
    <Box flex={1}>
      <FlatList
        contentContainerStyle={styles.contentContainerStyle}
        data={messages}
        inverted={true}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
        ListEmptyComponent={
          <Box style={styles.emptyComponentStyle}>
            <EmptyListComponent
              IconComponent={EmptypageData.IconComponent}
              iconSize={EmptypageData.iconSize}
              title={EmptypageData.title}
              subTitle={EmptypageData.subTitle}
            />
          </Box>
        }
      />
    </Box>
  );
});

const styles = StyleSheet.create({
  contentContainerStyle: {
    flexGrow: 1,
    paddingHorizontal: 20,
    paddingTop: 20,
  },
  emptyComponentStyle: {
    flex: 1,
    transform: [
      {
        scaleY: -1,
      },
    ],
  },
});

Last -> ChatBottomInput

import { Box, Input } from 'native-base';
import React from 'react';
import { Platform, TouchableOpacity } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';

import { chatThemeType } from '../../../types/chat';
import { SendIcon } from '../../icons';

type Props = {
  theme: chatThemeType;
  scrollRef: React.RefObject<KeyboardAwareScrollView>;
};

export const ChatBottomInput = ({ theme, scrollRef }: Props) => {
  const [inputValue, setInputValue] = React.useState<string>('');

  const onChangeText = (text: string) => {
    setInputValue(text);
    if (Platform.OS === 'ios') scrollRef.current?.scrollToEnd();
  };

  console.log('text', inputValue);

  return (
    <Box
      bg={theme.inputBackgroundColor}
      paddingX={4}
      paddingY={2}
      paddingBottom={4}
      borderTopWidth='2'
      borderColor={theme.inputBorderColor}
      position='relative'
    >
      {/* Keep this button above the keyboard */}
      <Box bg='primary.100'>
        <Input
          value={inputValue}
          onChangeText={onChangeText}
          placeholder='Написать сообщение'
          h='auto'
          onContentSizeChange={() => {
            if (scrollRef && scrollRef.current && Platform.OS === 'ios') {
              scrollRef.current?.scrollToEnd();
            }
          }}
          multiline={true}
          minHeight={'36px'}
          textAlignVertical='top'
          scrollEnabled={false}
        />
      </Box>
      <Box position='absolute' top={4} right={6}>
        <TouchableOpacity>
          <SendIcon size={5} />
        </TouchableOpacity>
      </Box>
    </Box>
  );
};
coprocoder commented 2 years ago

Seriously, in 5 years they have not come up with anything other than "scrollEnabled={false}" ?? Even if you block the scroll (although it causes a lot of inconvenience), then when you wrap the line, it goes down under the keyboard. Why does the field grow up on android, but on ios it moves down under the keyboard? How many versions of RN have already been released over these 5 years, but this bug has not been fixed.

endruuu commented 2 years ago

Referring to my snippet posted above, there's no height on the TextInput, and KeyboardAvoidingView's behavior is set to padding. Its style needs flex:1 to allow the ScrollView to take up the entire height. Try the code with and without multiline set. It fails with multiline set, but the same code works for single line TextInputs.

^ This from @peacechen worked for me perfectly. I have scrollEnabled={true} & multiline={true}

Code:

<KeyboardAvoidingView
  behavior="padding"
  style={{flex: 1, flexDirection: "column"}}
>
  <View style={{flex: 1}}>
    <TextInput
        multiline={true}
        scrollEnabled={true}
        style={{flex:1}}
        // Other relevant props
      />
  </View>
<KeyboardAvoidingView />

Thanks!! It also works for me.

Abdullah2500 commented 1 year ago

When you give prop scrollEnabled={false} in textInput, it will solve your issue but the problem comes in lenthy texts. It won't allow scrolling inside textInput. So the best approach would be to create a ref as const refScroll = useRef(false) and then pass that ref to textinput scrollEnabled prop. <TextInput scrollEnabled={refScroll.current}. And then onFocus={() => refScroll.current = true}
Its a workaround but works fine with me...

josaric commented 1 year ago

Try this:


constructor(props) {
    super(props);
    this.state = {
        scrollEnabled: true
    }
}

onFocus() {
    let self = this;
    // disable scroll until the library does its own magic
    this.setState({
        scrollEnabled: false
    });
    // enable scroll again after 1 sec
    setTimeout(function() {
        self.setState({
            scrollEnabled: true
        })
    }, 1000)
}

onBlur() {
        // enable scroll on blur
        this.setState({
            scrollEnabled: true
        })
    }

<TextInput 
scrollEnabled={this.state.scrollEnabled}
onFocus={()=> this.onFocus()}
onBlur={()=> this.onBlur()}
multiline=true
 />
mathbalduino commented 1 year ago

Still happening...

zonblade commented 1 year ago

still happening on ios device, but with workaround above its ok now.

mmkhmk commented 1 year ago

Any updates? I'm facing the same issue.

kipperlhp commented 1 year ago

It's 2023 now

xDreamLand commented 1 year ago

Still happening

vcapretz commented 1 year ago

When you give prop scrollEnabled={false} in textInput, it will solve your issue but the problem comes in lenthy texts. It won't allow scrolling inside textInput.

I solved this by setting only a minHeight to my input, so it doesn't scroll, but it grows if the user keeps typing 😋 worked good enough for my use case

(still frustrating that this is an issue from 2017 and we are still here tho)

karimelsaidy commented 1 year ago

5 years and no direct solution yet?!

MoFerhat commented 1 year ago

Damn it's June 2023 and the only "solution" is scrollEnabled={false} ? stop a bit...

mathbalduino commented 1 year ago

I think FB has bigger concerns right now lol

alainib commented 1 year ago

2023 and not a proper way to avoid keyboard over input from reactnative ....

ahmetares commented 1 year ago

july 2023...

jbagaresgaray commented 1 year ago

Bump

honcon commented 1 year ago

Hi folks,

After a lot of search I've a workaround for my case, The thing I wanted was not have a scrollable TextInput but a non-scrollable infinite height as size of text as gives and this view contained to a ScrollView working properly with a KeyboardAvoidingView,

Voila:

import React, { useState } from 'react';
import { SafeAreaView, ScrollView, TextInput, View, KeyboardAvoidingView, Keyboard, TouchableWithoutFeedback } from 'react-native';

import { Header } from 'react-native/Libraries/NewAppScreen';

export default function App(): JSX.Element {
  const [text, setText] = useState(`
  One
  Two
  Three
  Four
  Five`);

  const [textInputHeight, setTextInputHeight] = useState(40);

  const handleTextChange = (newText: any) => {
    setText(newText);
  };

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
        <TouchableWithoutFeedback onPress={Keyboard.dismiss}>

          <ScrollView
            style={{
              backgroundColor: '#00ffff32',
              borderWidth: 1,
              borderColor: '#0000ff',
            }}
          >
            <Header />
            <View style={{}}>
              <TextInput
                style={{
                  borderWidth: 1,
                  height: Math.max(40, text.split('\n').length * 20),
                }}
                onChangeText={handleTextChange}
                value={text}

                onContentSizeChange={event => {
                  const { height } = event.nativeEvent.contentSize;
                  setTextInputHeight(Math.max(40, height));

                }}
                multiline={true}
                scrollEnabled={false}
              />
            </View>
          </ScrollView>
        </TouchableWithoutFeedback>
      </KeyboardAvoidingView>

    </SafeAreaView>
  );
}

I wrapped this functionality in a CustomInputView component of course

istvan-panczel commented 1 year ago

Hi,

I'm new to React-Native and ran into the same problem. I came up with a dirty hack for myself, probably can help out someone else in the future:

const [isScrollEnabledForTextArea, setIsScrollEnabledForTextArea] = useState<boolean>(false);

<TextArea
  keyboardType={'default'}
  isInvalid={invalid}
  value={value}
  onChangeText={onChange}
  onBlur={() => {
    setIsScrollEnabledForTextArea(false);
  }}
  autoCompleteType={'off'}
  autoCapitalize={'none'}
  autoCorrect={false}
  scrollEnabled={isScrollEnabledForTextArea}
  onFocus={() => {
    // dirty hack, the KeyboardAvoidingView cant handle an input if it has multiline text
    setTimeout(() => setIsScrollEnabledForTextArea(true), SEC_IN_MS);
  }}
></TextArea>

I'm using NativeBase with <Controller /> and <TextArea /> but the problem is the very same. This solved it for me.

Shelrothman commented 1 year ago

So setting scrollEnabled to false made the KeyboardAvoidingView logic work for me.. yay. BUT... now... my clearButtonMode prop doesn't work! Ack! Anyone else got to this point? Thank you community for your help.

Shelrothman commented 8 months ago

For anyone still looking for a solution to this. I created an NPM package, expo-clear-input that is a solution to the above by working on any platform (web, ios, android) and in both single and multiline <TextInput>s.

A little x button for all

Jonathan0wh commented 7 months ago

It is 2024 now... Still no direct fix?

joshenx commented 7 months ago

fix?

TaewookKwak commented 7 months ago

Key Down

스크린샷 2024-01-25 오후 11 47 23

Key Up

스크린샷 2024-01-25 오후 11 53 18

return code

return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      enabled
      style={styles.avoid}
      keyboardVerticalOffset={0}>
      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          editable
          multiline
          textAlignVertical="top"
          onChangeText={newText => setText(newText)}
          value={text}
          maxLength={maxLength}
          autoFocus={true}
        />
        <View style={styles.counterContainer}>
          <Text style={styles.counter}>{`${text.length}/${maxLength}`}</Text>
        </View>
        <DotButton style={{width: '100%'}}>
          <DotButton.ButtonText>Send</DotButton.ButtonText>
        </DotButton>
      </View>
    </KeyboardAvoidingView>
  );

style

 inputContainer: {
    maxHeight: 500,
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginHorizontal: 20,
  },
  input: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#000',
    borderRadius: 10,
    alignSelf: 'stretch',
  },
  counterContainer: {
    alignSelf: 'stretch',
  },
  counter: {
    textAlign: 'right',
    marginTop: 4,
  },
  avoid: {
    flex: 1,
  },

for those who have a similar structure as me

fabOnReact commented 6 months ago

I believe it is an issue with RCTScrollViewComponentView.mm and/or RCTUITextView.

The behaviour is not automatic on iOS, some libs provide it like IQKeyboardManager.

I tested the workaround from comment-1148477911 (branch).

My understanding is that the native iOS implementation of ScrollView will keep the current scroll position automatically, for example:

1) ScrollView displays item 1-10 2) ScrollView height is resized from 700 to 300 3) After resizing, the ScrollView will scroll down so that item 10 is still visible at the bottom. The scroll position does not change

step 3) does not work on iOS when using multiline TextInput (UITextView), while it works when using single line TextInput (UITextField). Probably, the fact that UITextView can scroll, breaks this functionality.

Maybe the iOS implementation of UITextView/ScrollView keeps the position constant based on the contentOffset, but as we have 2 scrollviews (ScrollView scrolls and UITextView scrolls), the implementation manages the content offset of the TextInput multiline, instead of the parent scrollview.

KeyboardAvoidingView just wraps a view with style={{padding: bottom}} and decreases the height of the native ScrollView by the height of the keyboard

https://github.com/facebook/react-native/blob/12155c464c116b0786e8a6f3115a99804cf9b91d/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L232-L246

as you can see, there is no native impl of KeyboardAvoidingView, simply it changes the ScrollView height

CLICK TO OPEN TESTS RESULTS

Screenshot 2024-02-02 at 11 56 15 AM

after opening the keyboard, the scrollview height is decreased by the height of the keyboard, but the scrollview fails to scroll to the previous position

CLICK TO OPEN TESTS RESULTS

Screenshot 2024-02-02 at 11 58 53 AM

I also added to the below example, logic to scroll down the scrollview after opening the keyboard.

CLICK TO OPEN TESTS RESULTS

```javascript import * as React from 'react'; import { Keyboard, StyleSheet, KeyboardAvoidingView, ScrollView, View, Text, TextInput, } from 'react-native'; const RNTesterApp = () => { const [text, setText] = React.useState( 'a line 1 \n a line 2 \n a line 3 \n a line 4 \n a line 5 \n a line 6 \n a line 7 \n', ); let scrollViewRef = React.useRef(null); const [isScrollEnabled, setIsScrollEnabled] = React.useState(true); const onKeyboardWillShow = () => { console.log('onKeyboardWillShow'); setIsScrollEnabled(false); if (scrollViewRef) { console.log('scrollToEnd'); scrollViewRef.scrollTo({x: 0, y: 300}); } }; function onKeyboardDidShow() { console.log('onKeyboardDidShow'); setIsScrollEnabled(true); } React.useEffect(() => { const subKWS = Keyboard.addListener('keyboardWillShow', onKeyboardWillShow); const subKDS = Keyboard.addListener('keyboardDidShow', onKeyboardDidShow); return () => { subKWS.remove(); subKDS.remove(); }; }, []); return ( (scrollViewRef = ref)} keyboardShouldPersistTaps={'handled'} maintainVisibleContentPosition={{minIndexForVisible: 1}}> MESSAGE 1 MESSAGE 2 MESSAGE 3 MESSAGE 4 MESSAGE 5 MESSAGE 6 MESSAGE 7 MESSAGE 8 MESSAGE 9 MESSAGE 10 MESSAGE 11 ); }; export default RNTesterApp; const styles = StyleSheet.create({ text: { fontSize: 20, padding: 40, }, input: { height: 50, }, }); ```

I may consider working on this issue and publishing a fix if requested by the community. I believe it requires an extensive implementation in UITextView and RCTScrollViewComponentView to control their content offset.

https://github.com/hackiftekhar/IQKeyboardManager/blob/aa656e1f0d95f0154622f453599318602848be3b/IQKeyboardManager/IQKeyboardManager.m#L838-L853

https://stackoverflow.com/a/20052756/7295772

https://stackoverflow.com/a/69811662/7295772

Similar issues, but on Android are https://github.com/facebook/react-native/issues/25239 and https://github.com/facebook/react-native/issues/30373#issuecomment-1524520237