Skip to content

Long text inputs within ScrollView #937

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

Open
TimurKr opened this issue May 7, 2025 · 2 comments
Open

Long text inputs within ScrollView #937

TimurKr opened this issue May 7, 2025 · 2 comments
Assignees
Labels
👆 interactive keyboard Anything related to interactive keyboard dismissing KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component

Comments

@TimurKr
Copy link

TimurKr commented May 7, 2025

Hi, first of all, this seems like a great library, good job!

I'm quite new to react native but can already tell that getting th keyboard behaviour right is a major painpoint. I am building a simple notes app, almost an apple notes clone. No fancy rich text editing, just a couple simple multiline textinputs in a scroll view and a toolbar that should be sticky above the keyboard I tried using all of the hooks and components this library provides, but to no avail. I'll list my requirements, approaches I've tried, and share why they failed. This way you might point me in the directoin of a solution, you might notice some bugs in the library itself, and it might be a helpfull reference for other newcomers to this repo.

Requirements:

  • A basic note taking page (think apple notes, but without rich text editing features)
  • The view consists of:
    • Header
    • A scrollview with multiple components, this is where the textinputs are, but there might be other components, such as images, etc.
    • A toolbar that is sticky above the keyboard, it needs to be animated along with the keyboard
  1. The keyboard can be interactively dismissed (just like keyboardDismissMode="interactive"), but the dismissal start at the top of the toolbar, not the keyboard itself
  2. The toolbar is animated along the keyboard, not simply jumping between open and closed state
  3. When the keayboard is expanded, it does not obstruct the scrollview, so it is possible to scroll to the bottom
  4. Focusing a text input scrolls the view so that the caret "comfortably" within view (this means it is not obstructed by the keyboard, the toolbar, nor the header and has a safe padding, eg. 50 px from the header/toolbar)
  5. As the user types and the caret changes vertical position, the view should be scrolled so that the caret is within the "comfortable" boundaries, but only if it gets out of them.
  6. Lifting the finger after scrolling the view should not focus the text input the scrll happened over

All of the features above are directly taken from the behavior of the Apple notes app.

Tried solution

1. KeyboardGestureArea

I tried using the KeyboardGestureArea to dismiss the keyboard with an offset, but that only works if the currentyl focused input has a nativeID set to the same value as the textInputNativeID on the KeyboardGestureArea. As I have multiple text inputs, if I set the same nativeID for all of them, removing the focus from the currently focused input closes the keyboard. As if there was a listener on the blur event of the input with the same nativeID that closes the keyboard. I basically gave up on this and just accepted the closing starts on the top of the keyboard and not the toolbar.

2. KeyboardAvoidingView

I tried using KeyboardAvoidingView with a ScrollView, but just couldn't get it to work. Just too many issues and seems like to be the wrong approach anyway.

3. KeyboardAwareScrollView

I tried using KeyboardAwareScrollView, this got me very close, setting both bottomOffset and extraKeyboardSpace to the toolbar size, works nicely. Some issues are still present though:

  1. If the textinput is larger than the visible part, the scrolling starts to fail. Especially as you write at the end of the textinput and new lines are appended, the scroll jumps to the top of the textinput and then scrolls back down on each new line, making it basically unusable for longer textinputs.
  2. Releasing a tap after scrolling focuses the text input under the finger
  3. I couldn't find a solution to make the "comfortably" boundary for the caret to be placed in. The scroll is often unpredictably with long textinputs.

4. Custom solution using useReanimatedKeyboardAnimation, useFocusedInputHandler, and useReanimatedFocusedInput

see https://github.com/TimurKr/KeyboardControllerExample for a simple expo example

Finally I tried building my own solution, (see components/CustomKeyboardAwareScrollView.tsx). This got me the closest to the desired behaviour, but there are still some issues:

  1. If the textinput is larger than the visible part, the scrolling starts to fail. Especially as you write at the end of the textinput and new lines are appended, the scroll jumps to the top of the textinput and then scrolls back down on each new line, making it basically unusable for longer textinputs. No idea where this scrolling to the top is coming from...
  2. Focusing on the textinput in a place that get immedatelly obstructed by the keaboard does not trigger the custom scrolling behavior, so the "comfortably" boundary is not respected. Instead the default scrolling happens so the caret is just barely in the scrollview. (This is caused by the measure function returning the height of the scrollview before the keyboard is shown, so the calculations assumes the keyboard is hidden, so no scrolling happens)
  3. Releasing a tap after scrolling focuses the text input under the finger

Related issues

I see this PR #901 solving this issue #897 is attempting to fix the long textinput and scrolling issues. I haven't tried the changes in it and see there are multiple comments suggesting changes before merge.

Sorry for the long comment, but it seems like this repo did most of the hard work around handling keyboards in RN, but the last 10% is missing. I spent over 10 hours on this issue and I still couldn't figure it out. If the functionality is there, perhaps the docs need improvement. Thanks for you hard work, if there is anything I could help with, let me know. Thanks for any tips on how to get this working!

Setup I was testing on:

  • Desktop OS: MacOS 15.3.2
  • Device: iPhone16
  • OS: iOS 18.0
  • RN version: 0.79.2
  • RN architecture: new
  • Library version: 1.17.1
@kirillzyusko
Copy link
Owner

Hey @TimurKr

Thank you for such a descriptive issue ❤ Let's go over each item:

KeyboardGestureArea

Overall approach is correct.

As I have multiple text inputs, if I set the same nativeID for all of them, removing the focus from the currently focused input closes the keyboard. As if there was a listener on the blur event of the input with the same nativeID that closes the keyboard

So you added the same nativeID to all inputs, but if you change focus from one input to the other, then keyboard gets dismissed, correct? Basically it shouldn't be the case 🙈 I assume it's a bug in keyboard-controller (and potentially I even know where), so just need additional info how to reproduce. Set the same nativeID to all inputs and switch focus between inputs, right? Expected result is that keyboard should stay in the place, but a real result is that it causes "blur" event and keyboard gets closed? 🤔

KeyboardAvoidingView

Correct, KeyboardAvoidingView is not designed for that use-case. It's very good at small forms, but when you have multiple inputs in scrollable container it will not work correctly, because it simply resizes the view without processing other events, such as focused input layout changes etc. I tried to explain it in https://kirillzyusko.github.io/react-native-keyboard-controller/docs/guides/components-overview#keyboardawarescrollview but let me know if I can make it clearer somehow 😊

KeyboardAwareScrollView

If the textinput is larger than the visible part, the scrolling starts to fail. Especially as you write at the end of the textinput and new lines are appended, the scroll jumps to the top of the textinput and then scrolls back down on each new line, making it basically unusable for longer textinputs.

Yeah, this is a know issue. I already have several issues about it. I guess I just need to find a time and finally fix it. The problem is that in initial design I completely missed the case, when TextInput can be larger than screen (i. e. Apple Notes, Gmail message composer etc.). This is a bug and definitely needs to be fixed! Currently I'm preparing for AppJS Conf talk and literally don't have a lot of time for fixing this, but I promise I'll try to fix it in the June 🙈 (getting a lot of complaints, so it needs to be fixed, yes).

Releasing a tap after scrolling focuses the text input under the finger

Well, this one is strange 🤔 Can it be a default react-native behavior? My recommendation here would be to explore solutions from react-native-gesture-handler - from what I remember it tries to solve the issue with Buttons/Inputs during the scroll. Maybe worth to try to use equivalent component from react-native-gesture-handler, such as TextInput and ScrollView. This is how you can pass RNGH component in KeyboardAwareScrollView https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view#scrollviewcomponent

I couldn't find a solution to make the "comfortably" boundary for the caret to be placed in. The scroll is often unpredictably with long textinputs.

Yes, current implementation relies on input layout, not the caret position. And this is the problem, when input is very big I literlly don't know the direction for scrolling, because top part of input is obscured by header, and bottom part is obscured by keyboard, so it's literally unpredictable. The correct approach is to use cursor coordinates and I'll re-work it.

Custom solution

If the textinput is larger than the visible part, the scrolling starts to fail. Especially as you write at the end of the textinput and new lines are appended, the scroll jumps to the top of the textinput and then scrolls back down on each new line, making it basically unusable for longer textinputs. No idea where this scrolling to the top is coming from...

Maybe because of this?.. facebook/react-native#48412

Focusing on the textinput in a place that get immedatelly obstructed by the keaboard does not trigger the custom scrolling behavior, so the "comfortably" boundary is not respected. Instead the default scrolling happens so the caret is just barely in the scrollview. (This is caused by the measure function returning the height of the scrollview before the keyboard is shown, so the calculations assumes the keyboard is hidden, so no scrolling happens)

I think you can solve it by using onLayout (to store layout) and when keyboard appears you have an information about each frame (i. e. onMove handler)

Releasing a tap after scrolling focuses the text input under the finger

Yeah, don't think it's related to keyboard-controller, but maybe RNGH can help here.


So to sum up:

  1. The bug with nativeIDs most likely can be caused by keyboard-controller, please let me know if I correctly understand steps to reproduce and I'll try to reproduce and fix it quickly.
  2. Scroll/focus conflict - can be caused by default RN components. Worth to try react-native-gesture-handler (I believe it should solve the problem).
  3. Long inputs and incorrect scrolling... There is a plenty of issues. One of them is in KeyboardAwareScrollView, but after a quick look at your implementation you are correctly using useFocusedInputHandler, so yes, you can create your own implementation. I promise to try to fix the issue in the beginning of summer 😊 Another issue is this: View inside ScrollView makes scroll-to-top when multiline input grows facebook/react-native#48412 I provided a workaround (but haven't tested it). Also something that I should fix in react-native (if contentInset will work on Android then I can change the implementation in KeyboardAwareScrollView and don't modify layout anymore)

Let me now if you have any other questions - will be happy to answer on them 😊

@kirillzyusko kirillzyusko added 👆 interactive keyboard Anything related to interactive keyboard dismissing KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component labels May 8, 2025
@TimurKr
Copy link
Author

TimurKr commented May 8, 2025

Hey @kirillzyusko,

incredible response, thanks for the details. Better customer support than most payed products ❤️

Regarding the KeyboardGestureArea

So you added the same nativeID to all inputs, but if you change focus from one input to the other, then keyboard gets dismissed, correct? Basically it shouldn't be the case 🙈

I added a simple reproduction to the repo with the example (see here). To reproduce, simply add 2 text inputs with the same nativeID and try switching focus between them, instead of switching, it dismisses the keyboard and does not focus any of them. Another interesting observation is that if you add nativeID to only one of them, the keyboard gets dismissed when focusing away from the one with alid nativeID. Focusing toward it from another input works fine. It is also worth noting the offset is not taken into consideration at all if an input with a different nativeID is focused. I am not sure if this is expected, nor how this is implemented, but I believe the following functionality would be better:

  1. If no textInputNativeID is passed, the offset is obeyed no matter what input is focused. This would allow us to not pass the nativeID to all inputs we want this behavior for, effectively freeing us to set different nativeID.
  2. If textInputNativeID is passed, the current behavior is applied.

But with the current behavior, this requirement should be explicitly mentioned in the docs. I would suggest editing the docs with the following changes

  1. Marking textInputNativeID as required and expanding its definition to explain it is necessary to pass the same nativeID to all text inputs we want the offset to be applied to.
  2. Mentioning somewhere that the scrollview inside still needs the keyboardDismissMode="interactive" prop. I was under the impression the component would handle this, even the example in the docs shows no props passed to the <ScrollView>. It took quite a lot of trial and error to find out what the right configuration of props are to get this working.
  3. Maybe adding the example from this blog post to the examples
  4. The note about compatibility is outdated, as it states this is not supported on iOS, but it is since 1.16.0

Regarding KeyboardAvoidingView

I tried to explain it in https://kirillzyusko.github.io/react-native-keyboard-controller/docs/guides/components-overview#keyboardawarescrollview but let me know if I can make it clearer somehow 😊

I believe the explanation is clear and I tried using it just out of frustration as I was not able to get it to work with the other more fitting components 🫠

Regarding KeyboardAwareScrollView

This is a bug and definitely needs to be fixed! Currently I'm preparing for AppJS Conf talk and literally don't have a lot of time for fixing this, but I promise I'll try to fix it in the June 🙈

I saw the issues, it is funny that making a simple auto-scrolling long text editing view is not possible with one simple plug-n-play component and it needs you to finally build it 🤯 I would help, but I wrote my first line of RN 3 weeks ago, so I am a little out of depth here... I am looking forward to your talk later this month (and the fix afterwards).

When you are fixing it, I believe the issue is not only finding out where the caret is (I did that in the custom solution as well), but also about preventing the weird jumping up whenever new line is added. Exactly as you described here facebook/react-native#48412. But i understand there is a backlog of other issues that need to be fixed beforehand. It would also be nice if the rework allowed specifying something like caretPadding: number | { top?: number, bottom?: number } so the caret does not get scrolled into the view just barely, as that looks quite bad.

Regarding focusing textinput after releasing scroll

I found this discussion about it. I also found this library react-native-text-input-scroll-view, which makes a lot of promises, but has been archived for 5 years. It mentions that it solves this exact issue here as the fifth point.

So i guess this is a known issue in RN. I found out it does not happen when I render the page within a stack navigation defined as

<Stack.Screen
  name="note/[id]"
  options={{
    fullScreenGestureEnabled: true,
  }}
/>

no clue why, but for anyone else trying to figure this out, I would start by looking into the implementation of the fullScreenGestureEnabled prop. It would also by nice if the KeyboardAwareScrollView could handle this by default.

Thanks again for you response!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
👆 interactive keyboard Anything related to interactive keyboard dismissing KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component
Projects
None yet
Development

No branches or pull requests

2 participants