Skip to content

feat: use contentInset for KeyboardAwareScrollView#797

Merged
kirillzyusko merged 8 commits into
mainfrom
fix/use-content-inset-for-keyboard-aware-scroll-view
Feb 1, 2026
Merged

feat: use contentInset for KeyboardAwareScrollView#797
kirillzyusko merged 8 commits into
mainfrom
fix/use-content-inset-for-keyboard-aware-scroll-view

Conversation

@kirillzyusko

@kirillzyusko kirillzyusko commented Feb 3, 2025

Copy link
Copy Markdown
Owner

📜 Description

Use ScrollViewWithBottomPadding component for KeyboardAwareScrollView to optimize performance and fix other issues that were caused by the fact of usage additional view inside ScrollView.

💡 Motivation and Context

In this PR I'm moving away from the idea of having a fake view in the end of the ScrollView and instead start to use ScrollViewWithBottomPadding component to achieve a desired visual effect.

The additional view causes many issues such as:

  • unintended layout shift if flex: 1 style is used;
  • broken auto-grow for multiline input;
  • unexpected styling issues if you use gap/justifyContent: "space-between" and other properties.

In this PR I'm switching to the component that has been added in #1294 With its new power I can achieve cross-platform behavior and:

  • without layout modification add a scrollable padding;
  • get the same behavior on both iOS/Android;
  • don't use any hidden children that can break something.

This PR has been opened for a long time, but finally can be merged because I got working version on Android.

Closes #794 #645 #929 #168

Potentially: #748 software-mansion/react-native-reanimated#5567 #719

Unlocks one item from #883

📢 Changelog

JS

  • added useScrollState hook;
  • use ScrollViewWithBottomPadding component in KeyboardAwareScrollView;
  • added removeGhostPadding in KeyboardAwareScrollView and use it there.

E2E

  • update assets for KeyboardToolbarClosed test-case.

🤔 How Has This Been Tested?

Tested manually on:

  • iPhone 17 Pro (iOS 26.2, simulator);
  • Pixel 7 Pro (API 36, real device);
  • e2e_emulator_28 (API 28, emulator);;
  • all e2e devices;

📸 Screenshots (if appropriate):

iOS Android
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-02-01.at.16.46.14.mov
Screen.Recording.2026-02-01.at.16.49.23.mov

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed

@kirillzyusko kirillzyusko added refactor You changed the code but it didn't affect functionality KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component labels Feb 3, 2025
@kirillzyusko kirillzyusko self-assigned this Feb 3, 2025
@github-actions

github-actions Bot commented Feb 3, 2025

Copy link
Copy Markdown
Contributor

📊 Package size report

Current size Target Size Difference
241763 bytes 237686 bytes 4077 bytes 📈

kirillzyusko added a commit that referenced this pull request Jan 28, 2026
## 📜 Description

Added `ClippingScrollView` component (on Android only) that is supposed
to act as a polyfill for `contentInset: {bottom}` property of
`ScrollView`.

## 💡 Motivation and Context

Those changes are based on my PR from
react/react-native#49145

The big problem with original PR is that it wasn't working correctly, if
we specify all insets/paddings simultaneously. But! It worked well for
`bottom` inset property (keyboard-controller case, because keyboard
appears from the bottom of the screen). I think it's risky to ship a
code in this state into facebook codebase, so I decided to add those
changes into `react-native-keyboard-controller` first. It's good
because:

- I have a full ownership of the code and I can fix bugs quickly
(without waiting for a new RN release);
- we don't ship buggy code in react-native repository;
- we don't depend on react-native version and we don't need to write a
conditional code, like "if prop is supported, then use new approach and
if not, then fallback to old implementation".

The approach that I've choose is based on "decorator" approach - this
approach has been used in many other libs, such as
`advanced-input-mask`, `live-markdown` etc. The idea is that we create
our custom view that wraps our target view and then we access a target
view as a children (and we can modify behavior/props of this view).

I'm going to use this component in `KeyboardAwareScrollView` and in new
`ChatKit` component. From lessons learned from the previous experience I
can confidently say, that additional view near children will cause
issues and this polyfill for `contentInset: {bottom}` should hopefully
solve all the issues that we had. I'll continue experiments with this
view in
#797

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### JS

- added codegen component;
- added new types props for new component;
- added jsdoc for new component;

### Android

- added `ClippingScrollView`;

## 🤔 How Has This Been Tested?

Tested in example app in `KeyboardAwareScrollView` screen and in new
component that is currently under active development 😊 Everywhere works
stable on both architectures 🤞

## 📸 Screenshots (if appropriate):

|KeyboardAwareScollView|Non inverted chat list|
|--------------------------|---------------------|
|<video
src="https://github.com/user-attachments/assets/0f7efef7-ec15-4fe6-96cb-40334f0c91ad">|<video
src="https://github.com/user-attachments/assets/92093e87-434a-4c82-91d5-1c7e9706e5f0">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
@kirillzyusko kirillzyusko force-pushed the fix/use-content-inset-for-keyboard-aware-scroll-view branch from 29efbf3 to f11412f Compare January 28, 2026 12:03
@kirillzyusko kirillzyusko force-pushed the fix/use-content-inset-for-keyboard-aware-scroll-view branch from 9e74ef9 to 17bc9d4 Compare January 29, 2026 10:10
@kirillzyusko kirillzyusko added enhancement New feature or request 🚀 optimization You optimize something and it becomes working faster and removed refactor You changed the code but it didn't affect functionality labels Jan 31, 2026
@kirillzyusko kirillzyusko marked this pull request as ready for review February 1, 2026 15:51
@kirillzyusko kirillzyusko merged commit 423dbef into main Feb 1, 2026
30 of 32 checks passed
@kirillzyusko kirillzyusko deleted the fix/use-content-inset-for-keyboard-aware-scroll-view branch February 1, 2026 15:54

@kirillzyusko kirillzyusko Feb 1, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caught a regression on iOS 26 + Xcode 16 👍

@kirillzyusko kirillzyusko mentioned this pull request Feb 1, 2026
2 tasks
kirillzyusko added a commit that referenced this pull request Feb 1, 2026
## 📜 Description

Fixed iOS e2e tests.

## 💡 Motivation and Context

The pipeline got broken here:
#797

iOS 18 tests were failing because of 1 asset <- just updated one
screenshot
iOS 26 + XCode 16 were failing because of 2 assets <- things went harder
here

On `iOS 26 + XCode 16` I used old iOS 26.0 version, which resulted in a
wrong behavior. It was hard to fix it in the lib codebase, so I just
decided to switch to a new iOS version. For that I had to update all
snapshots.

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### E2E

- updated iOS e2e assets;
- use iOS 26.2 instead of iOS 26.0.

## 🤔 How Has This Been Tested?

Tested manually via e2e run on a failed exam.

## 📸 Screenshots (if appropriate):

<img width="858" height="395" alt="image"
src="https://github.com/user-attachments/assets/58850c15-2bc4-4d6a-9159-97eafce64a25"
/>

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
kirillzyusko added a commit that referenced this pull request Mar 4, 2026
## 📜 Description

Make `scrollRectToVisible` of `ReactScrollView` no-op.

## 💡 Motivation and Context

This issue became visible after
#797

Before we were modifying `height` of fake view. And
`scrollRectToVisible` is not sensitive to this:

```objc
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
{
  // Limiting scroll area to an area where we actually have content.
  CGSize contentSize = self.contentSize;
  UIEdgeInsets contentInset = self.contentInset;
  CGSize fullSize = CGSizeMake(
      contentSize.width + contentInset.left + contentInset.right,
      contentSize.height + contentInset.top + contentInset.bottom);

  rect = CGRectIntersection((CGRect){CGPointZero, fullSize}, rect);
  if (CGRectIsNull(rect)) {
    return;
  }

  [super scrollRectToVisible:rect animated:animated];
}
```

But since we started to modify `contentInset` this function now can also
"scroll" and this causes strange effect such as
#1331

This is a `ReactScrollView` built-in solution for "avoiding"
`TextInput`. Logs:

```
1, 298.31225727257987, 116, 718.6666666666666
'syncKeyboardFrame', 308
14
116, 718.6666666666666, 834.6666666666666
1, 298.66666666666663, 116, 718.6666666666666
[scrollRectToVisible] rect=(16.0, 713.7, 10.3, 24.7) animated=1
  contentOffset=(0.0, 298.7) bounds=(402.0 x 758.0)
  contentSize=(402.0, 1084.7) contentInset=(t=0.0 l=0.0 b=339.0 r=0.0)
  fullSize=(402.0, 1423.7) visibleY=[298.7, 1056.7] alreadyVisible=1
  safeAreaInsets=(t=16.0 l=0.0 b=34.0 r=0.0)
  adjustedContentInset=(t=0.0 l=0.0 b=339.0 r=0.0)
  -> CALLING [super scrollRectToVisible]
'syncKeyboardFrame', 308

308 keyboard height + 1 static pixel + 30 extraKeyboardHeight = 339
```

Which we can evaluate as:

```
effective visible height = bounds.height - contentInset.bottom
                         = 758 - 339 = 419

visible range = [contentOffset.y, contentOffset.y + 419]
              = [298.7, 717.7]

rect bottom   = 713.7 + 24.7 = 738.4

738.4 > 717.7  →  NOT visible from UIKit's perspective!
```

So UIKit scrolls by 738.4 - 717.7 ≈ 20.7px 😡 I think this effect is
totally undesirable for `KeyboardAwareScrollVIew`/`ClippingScrollView`
since we control scroll position on our own, so via swizzling (this is
the only one option at the moment, correct option would be to add a new
prop to `ScrollView` in `react-native`) I make this method a no-op.

I checked and functionality like "tap-status-bar-to-scroll" works well.
So I think it's safe to have this fix 🤞

Closes
#1331

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### JS

- make `ClippingScrollView` real view on iOS;

### iOS

- add `ClippingScrollView` shadow node;
- replace `scrollRectToVisible` to no-op;

### Android

- add `ClippingScrollView` shadow node;

## 🤔 How Has This Been Tested?

Tested manually on iPhone 17 Pro (iOS 26.2).

## 📸 Screenshots (if appropriate):


https://github.com/user-attachments/assets/bb24aa16-661a-41af-b7d1-37fbbd9f4353

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
kirillzyusko added a commit that referenced this pull request Mar 19, 2026
## 📜 Description

Don't change keyboard padding in `KeyboardAwareScrollView` each frame as
keyboard moves and do it only in the beginning of the animation (when
keyboard appears) or in the end of the animation (when keyboard closed).

## 💡 Motivation and Context

After
#797
introduced removal of `ghost padding` we don't need to change `inset`
every frame. Instead we can use an optimized approach and:
- add full keyboard frame padding only in the beginning of the animation
(when keyboard appears)
- remove  full keyboard frame padding when keyboard fully closed.

Roughly it gives us `2x` less load on UI thread because now we only need
to adjust scroll position inside `onMove` handler. A similar approach is
already used in `KeyboardChatScrollView` (we also change padding only
one time there). So these changes just makes components consistent.

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### JS

- don't change `currentKeyboardFrame` each frame;

## 🤔 How Has This Been Tested?

Tested manually on iPhone 17 Pro (iOS 26.2, simulator), Pixel 7 Pro (API
36, real device).

## 📸 Screenshots (if appropriate):

|iOS|Android|
|---|--------|
|<video
src="https://github.com/user-attachments/assets/9e5f72cc-b6c7-4ea5-999f-d5c56c046809">|<video
src="https://github.com/user-attachments/assets/31f5a675-2de8-4136-91fe-7390364bbb97">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
@kirillzyusko kirillzyusko mentioned this pull request Mar 23, 2026
2 tasks
kirillzyusko added a commit that referenced this pull request Mar 23, 2026
## 📜 Description

Removed a fix with increased padding by `+1`.

## 💡 Motivation and Context

This code has been added in
#342
But these changes became irrelevant after
#1381
Since we started to change frame to full-height frame once per the
animation we no longer have a condition described in #342

Additionally
#332
is no longer reproducible, because in
#797
we already were fighting with "ghost view" issue and fix for this
problem also fixes
#332
(just in a different way).

So to sum it up:
- we no longer need to adjust spacer each frame;
- we no longer need a fix with artificial spacer increasing 🤞 

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### JS

- don't apply `+1` to keyboard spacer size in `KeyboardAwareScrollView`;

## 🤔 How Has This Been Tested?

Tested manually on Redmi Note 5 Pro (Android 9).

## 📸 Screenshots (if appropriate):

|Current code|Latest main (before PR changes)|1.20.7 release|
|-------------|---------------------------------|-------------|
|<video
src="https://github.com/user-attachments/assets/7ab3477c-eedd-42da-9af9-5bdb0c842284">|<video
src="https://github.com/user-attachments/assets/8697e5e6-84d3-49c5-adb6-2d286cd9ccaa">|<video
src="https://github.com/user-attachments/assets/37c9b30c-8aa8-470a-9255-8c4a3d752a95">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component 🚀 optimization You optimize something and it becomes working faster

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiline TextInput Auto-growing and Scrolling Behavior Issues with KeyboardAwareScrollView

1 participant