Skip to content

fix: add automaticOffset prop for correct KAV positioning in modals#1346

Merged
kirillzyusko merged 27 commits intokirillzyusko:mainfrom
thomasttvo:fix/kav-modal-measure-in-window
Mar 11, 2026
Merged

fix: add automaticOffset prop for correct KAV positioning in modals#1346
kirillzyusko merged 27 commits intokirillzyusko:mainfrom
thomasttvo:fix/kav-modal-measure-in-window

Conversation

@thomasttvo
Copy link
Copy Markdown
Contributor

@thomasttvo thomasttvo commented Mar 8, 2026

Problem

KeyboardAvoidingView doesn't work correctly in pageSheet modals on iOS because onLayout returns parent-relative coordinates. In a modal, the parent's y=0 doesn't correspond to screen y=0, so the keyboard overlap calculation uses incorrect positions — resulting in the keyboard covering the input or the view being offset incorrectly.

This is an iOS-specific issue. On Android, modals use a separate Dialog window, but measureInWindow (backed by getLocationOnScreen()) already returns correct absolute screen coordinates regardless of which window the view is in.

Solution

Add an opt-in automaticOffset prop (default false) that gates the measureInWindow behavior. This preserves backward compatibility — existing users who set keyboardVerticalOffset to their header height continue to work unchanged.

When automaticOffset={true}:

  • Uses measureInWindow instead of onLayout to get absolute screen coordinates, so the view's position is correctly detected in modals, behind navigation headers, etc.
  • keyboardVerticalOffset becomes purely additive extra space rather than compensation for unknown positioning — the default of 0 works correctly out of the box.

When automaticOffset={false} (default):

  • Behavior is identical to before this PR — uses onLayout parent-relative coordinates, and keyboardVerticalOffset must be set to the header height manually.

Uses the existing useCombinedRef hook to maintain both the internal ref (needed for measureInWindow) and the forwarded ref from the consumer.

Example app

Added a new "KeyboardAvoidingView Automatic" example screen (KeyboardAvoidingViewAutomatic) to showcase automaticOffset behavior:

  • Demonstrates all three behavior modes (padding, height, position)
  • Auto/Manual toggle to compare automaticOffset={true} vs automaticOffset={false}
  • Includes a pageSheet Modal to test the modal positioning fix
  • Configurable keyboardVerticalOffset toggle (+0, +50, +100)
  • Reusable KAVContent component shared between regular screen and modal

No changes to the existing KAV example screen or E2E tests.

Test Plan

iOS (iPhone 16 simulator), automaticOffset={true}, keyboardVerticalOffset={0}:

  • ✅ Regular screen — all content visible above keyboard (padding, position modes)
  • ✅ Modal — all content visible above keyboard (padding, position modes)

automaticOffset={false} (default), keyboardVerticalOffset={100}:

  • ✅ Regular screen — same behavior as before this PR
  • ✅ RN implementation — same behavior as before this PR

Fixes #867

KeyboardAvoidingView uses onLayout to get the view's frame, but
onLayout returns parent-relative coordinates. In pageSheet modals,
the parent's y=0 doesn't correspond to screen y=0, causing the
keyboard avoidance calculation to use incorrect absolute positions.

This commit fixes two issues:

1. Use measureInWindow instead of onLayout to get absolute screen
   coordinates. This ensures frame.y reflects the actual screen
   position, not the parent-relative position.

2. Compensate for the gap below pageSheet modals. In pageSheet
   presentation, the modal doesn't extend to the screen bottom,
   but the keyboard covers this gap. The overlap calculation now
   treats the view as extending to the screen bottom when it
   detects a modal-like configuration (gap > 0, gap < keyboard
   height, view > 50% of screen).

A combinedRef is used to maintain both the internal ref (needed
for measureInWindow) and the forwarded ref from the consumer.

Fixes kirillzyusko#867

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo force-pushed the fix/kav-modal-measure-in-window branch from 33bcd74 to 9436c60 Compare March 8, 2026 07:21
Type useRef as View | null to avoid the MutableRefObject cast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo force-pushed the fix/kav-modal-measure-in-window branch from 9436c60 to faf5e6f Compare March 8, 2026 07:22
measureInWindow already provides absolute screen coordinates, making
the modal gap compensation unnecessary. The heuristic was compensating
for onLayout returning parent-relative y=0 in modals, but that's now
fixed at the source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo marked this pull request as draft March 8, 2026 07:33
Comment thread src/components/KeyboardAvoidingView/index.tsx Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 8, 2026

📊 Package size report

Current size Target Size Difference
301688 bytes 300025 bytes 1663 bytes 📈

@kirillzyusko kirillzyusko self-assigned this Mar 8, 2026
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🌎 modal Anything that involves Modal usage KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component labels Mar 8, 2026
@kirillzyusko
Copy link
Copy Markdown
Owner

Thank you @thomasttvo for your PR

Unfortunately I can not merge it as is. Try to open example app and check the difference between package/RN implementations. You will see that package implementation will push the content significantly higher than it's expected to do. Ideally RN and package implementation should produce identical visual result.

This is a breaking change and we should understand what's causing this and fix it before a merge 🤞

You can also download e2e artifacts to see what exactly doesn't work...

@thomasttvo thomasttvo changed the title fix: use measureInWindow for correct KAV positioning in modals WIP fix: use measureInWindow for correct KAV positioning in modals Mar 8, 2026
@thomasttvo
Copy link
Copy Markdown
Contributor Author

hey @kirillzyusko sorry about that! I meant to put this PR back in a WIP state since there's some significant changes to it since it's opened. Let me check what's going on with these issues and get back to you. Thank you for your patience!

…ation

With measureInWindow, frame.y is already in absolute screen coordinates,
making keyboardVerticalOffset redundant in the formula. Removing it fixes
over-compensation on regular screens while maintaining correct behavior
in modals.

Also adds modal test case to the example app and uses the existing
useCombinedRef hook instead of a manual ref callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo changed the title WIP fix: use measureInWindow for correct KAV positioning in modals fix: use measureInWindow for correct KAV positioning in modals Mar 8, 2026
thomasvo and others added 3 commits March 8, 2026 12:38
With measureInWindow, the view's absolute position is already captured
in frame.y. keyboardVerticalOffset is now purely additive extra offset
rather than compensation for unknown view position.

Update example to use offset=0 since nav bar is handled automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cycles through 0, 50, 100 to demonstrate the additive offset behavior.
Shows "+N" in the header bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With measureInWindow, navigation headers and modals are handled
automatically. keyboardVerticalOffset is now purely additive extra
offset — no need to pass useHeaderHeight() or compensate manually.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo marked this pull request as ready for review March 8, 2026 20:10
@thomasttvo
Copy link
Copy Markdown
Contributor Author

thomasttvo commented Mar 8, 2026

@kirillzyusko ready for review!

I tested everything manually and verified it works locally, but I may have missed a few details. Let me know if you spot anything or have any feedback on the changes.

@thomasttvo thomasttvo changed the title fix: use measureInWindow for correct KAV positioning in modals fix: use measureInWindow for correct KAV absolute positioning Mar 9, 2026
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Mar 11, 2026, 9:02 AM

thomasvo and others added 3 commits March 9, 2026 10:29
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
measureInWindow can return stale y=0 when onLayout fires during a
modal's slide-up animation. This caused height and position modes to
under/over-compensate in modals.

Two fixes:
1. onLayoutWorklet for height mode now allows position (x, y) updates
   while preserving the original height — prevents stale y without
   reintroducing the height feedback loop.
2. Re-measure absolute position on keyboard start via useKeyboardHandler.
   By the time the user focuses an input, the modal animation is complete
   and measureInWindow returns the correct coordinates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useKeyboardAnimation and useTranslateAnimation already call
useKeyboardHandler (which includes useResizeMode). The reMeasure
handler only needs event subscription, not the resize mode side effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kirillzyusko
Copy link
Copy Markdown
Owner

Hey @thomasttvo

I reviewed your PR and I think it has breaking changes. Since we start to calculate keyboardVerticalOffset partially on our own -> all users that update from previous version will have breaking changes (i. e. the content will be pushed higher than expected).

I tend to think this is a breaking change and I don't want to created a release V2 just because of these changes. Do you think we can hide new functionality under feature flag/new prop?

For example automaticallyCalculateVerticalKeyboard/includeAbsoluteScreenPosition/whatever you-or-claude think is better and when it's true, then use new path for calculation?

Again, my concern is that onLayout and measureInWindow may produce different results. onLayout may not include Header/SafeArea etc. height. -- measureInWindow will account that spacing. Maybe we can add a new internal property like absoluteY which will be 0 by default, and when new prop is passed then we'll update it (via measure function) and we will always include it in calculations?

@thomasttvo
Copy link
Copy Markdown
Contributor Author

makes sense @kirillzyusko let me see what I can do

The measureInWindow change was a breaking change for users who set
keyboardVerticalOffset to their header height. This gates it behind
an `automaticOffset` prop (default false) so existing behavior is
preserved. When enabled, the view auto-detects its screen position
and keyboardVerticalOffset becomes purely additive.

Also moves example app settings from nav header to screen content
area, updates E2E tests for both modes, and updates docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kirillzyusko
Copy link
Copy Markdown
Owner

There's also the argos failure, which probably wasn't caused by this PR

You can ignore them for now. Argos job is flacky a little bit and needs to be fixed (I'll handle it separately)

Comment thread src/components/KeyboardAvoidingView/index.tsx Outdated
thomasvo and others added 3 commits March 10, 2026 09:14
- Remove reMeasure + useGenericKeyboardHandler (unnecessary: keyboard
  open triggers onLayout which corrects stale measureInWindow values)
- Remove runOnJS and useGenericKeyboardHandler imports
- Simplify onLayout to single if/else without nested null checks
- Scope onLayoutWorklet else branch to automaticOffset only (non-automatic
  mode uses parent-relative coords that are always correct)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove relativeKeyboardHeight comment (duplicates automaticOffset JSDoc)
- Remove onLayout ref existence comment (self-evident from optional chain)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite the automaticOffset else-branch comment to explain the full
chain: why the if-branch skips updates, why automaticOffset needs
the correction, and why height is preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo force-pushed the fix/kav-modal-measure-in-window branch from d96427c to ed904e4 Compare March 10, 2026 16:48
Split the single block comment into two focused comments — one per
branch — so readers understand each path independently. Add ref
safety comment on measureInWindow call. Remove debug console.log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo thomasttvo force-pushed the fix/kav-modal-measure-in-window branch from ed904e4 to 11ea978 Compare March 10, 2026 16:50
Position mode's outer view doesn't resize when the keyboard opens
(only the inner view shifts via { bottom }), so onLayout won't
re-fire to correct stale measureInWindow values from modal animation.
Add useGenericKeyboardHandler to re-measure on keyboard start.

Also: split onLayoutWorklet comments, add ref safety comment,
simplify else-branch with spread, remove debug console.log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/components/KeyboardAvoidingView/index.tsx Outdated
Comment thread src/components/KeyboardAvoidingView/index.tsx Outdated
thomasvo and others added 5 commits March 10, 2026 10:19
Reverts example app and e2e test modifications to keep this PR
focused on the core automaticOffset implementation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New dedicated example screen showcasing automaticOffset in
KeyboardAvoidingView, including a page-sheet Modal to test
all three behavior modes (padding, height, position).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Testing confirmed measureInWindow returns correct coordinates at
onLayout time in modals, making the useGenericKeyboardHandler
re-measurement unnecessary. Also adds Auto/Manual toggle to the
example screen and removes debug code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The else-if branch for automaticOffset in height mode was not
needed — the stale y=0 from measureInWindow gets corrected on
the next onLayout while the keyboard is still closed. Height
mode's shrunk-view bug exists regardless of this branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thomasttvo
Copy link
Copy Markdown
Contributor Author

thomasttvo commented Mar 11, 2026

@kirillzyusko 🤦 , this is very strange. I can see the y=0 on initial load but now it quickly changes to a positive value the moment the modal finishes the animation, which means we don't need the extra logic. It wasn't doing that before, maybe it was just me hallucinating, or things got weird after rounds of hot reloads. I removed all the code that workarounds the y=0 for height and position mode, and added a separate screen for the Automatic Offset mode. Let me know what you think!

Copy link
Copy Markdown
Owner

@kirillzyusko kirillzyusko left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you for your contribution ❤️

@kirillzyusko kirillzyusko merged commit 66e6278 into kirillzyusko:main Mar 11, 2026
21 checks passed
@kirillzyusko
Copy link
Copy Markdown
Owner

FYI @thomasttvo it seems like it doesn't work with new architecture (FabricExample) + Modal:

image

Paper architecture works well. Is it a bug in measureInWindow?..

@thomasttvo
Copy link
Copy Markdown
Contributor Author

thomasttvo commented Mar 11, 2026

@kirillzyusko on it! should have checked that we've been testing against Paper all this time.

@kirillzyusko
Copy link
Copy Markdown
Owner

@thomasttvo the problem seems to be in react-native and how measureInWindow is implemented on a new architecture. I'll prepare a fix in RN repo, no worries!

@kirillzyusko
Copy link
Copy Markdown
Owner

I opened a PR here: facebook/react-native#56062

kirillzyusko added a commit that referenced this pull request Mar 12, 2026
)

## 📜 Description

Automatically detect top border of `KeyboardAwareScrollView`.

## 💡 Motivation and Context

Continue the epic with better discovery of component location on the
screen and logical continuation of
#1346

In this PR I started to detect relative position of `ScrollView`, so
that I better understand if caret is not visible because it obscured by
other elements (header etc.) It's still not perfectly implemented and
there is still a "blind"/"dead" zone where text is already hidden but
scroll doesn't happen. I'll fix it in following PRs but for now I just
want to bring these changes to upcoming `1.21.0` release 🤞

We also can't use `measureInWindow` because it produces incorrect
measurements, so we need to use our custom implementation that has been
added in
#1355

Significantly improves UI for behavior described in
#1341

## 📢 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

- auto detect position of `KeyboardAwareScrollView` on the screen to
better understand top border of the component relative to screen;

## 🤔 How Has This Been Tested?

Tested manually on iPhone 17 Pro (iOS 26.2, simulator).

## 📸 Screenshots (if appropriate):


https://github.com/user-attachments/assets/43fe233f-9c83-40c9-9642-a68e2d8e8e7c

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
@kirillzyusko kirillzyusko added the 🚨 requires API changes 🚨 Changes that requires changes in library API label Mar 13, 2026
kirillzyusko added a commit that referenced this pull request Mar 18, 2026
## 📜 Description

Silent `could not fing view for tag` warning for
`KeyboardAwareScrollView` (similarly how it works for
`KeyboardAvoidingView` now).

## 💡 Motivation and Context

This issue got introduced after
#1352
(which was based on
#1346)

In certain cases `onLayout` may be triggered but view may be not laid
out yet (if you use it with `react-native-pager` lazy). Since changes in
#1352
were purely additional (we try to understand the position of element on
the screen, but before these changes we always assumed we were in the
top of the screen) I decided to wrap it for now in `try/catch` block.

Silenting error is not a correct fix, but this is the best what we can
do now - we will not spam logs when devs update the package (in
#1346
we also used `catch` block), but yes, in certain cases detection of top
border will not work (and it didn't work before, so it's fair enough to
silent the error).

Closes
#1375

## 📢 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

- handle rejected promises within `try/catch` block;

## 🤔 How Has This Been Tested?

Tested via e2e tests, I didn't test changes manually because I don't
have a repro.

## 📝 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

documentation Improvements or additions to documentation enhancement New feature or request KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component 🌎 modal Anything that involves Modal usage 🚨 requires API changes 🚨 Changes that requires changes in library API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyboardStickyView not moving with keyboard within modal

2 participants