fix: KeyboardAwareScrollView re-focus after hardware keyboard dismissal#1403
Merged
kirillzyusko merged 9 commits intomainfrom Mar 27, 2026
Merged
fix: KeyboardAwareScrollView re-focus after hardware keyboard dismissal#1403kirillzyusko merged 9 commits intomainfrom
KeyboardAwareScrollView re-focus after hardware keyboard dismissal#1403kirillzyusko merged 9 commits intomainfrom
Conversation
Contributor
📊 Package size report
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
Fixed a problem, when re-focusing field (after dismiss via system button) pushes the field significantly higher.
💡 Motivation and Context
The
onEndhandler nullslastSelection.valuewhen the keyboard hides (e.height === 0). This was added intentionally in #1234 to solve an iOS 15 problem: without nulling,onStartcouldn't distinguish a fresh selection (that arrived beforeonStartin the current session) from a stale selection (leftover from the previous session). On iOS 15, selection sometimes arrives afteronStart, so thependingSelectionForFocusmechanism was introduced to defer layout setup until the selection arrives.However, nulling
lastSelectioncauses two regressions:1️⃣ Android — refocus same input
Android doesn't re-emit
onSelectionChangewhen refocusing the same input at the same cursor position. After thenull,onStartsees lastSelection.value?.target !== e.target(becausenull?.targetisundefined), falls back tolayout.value = input.value(full input height, e.g.180pxinstead of caret height43px), and setspendingSelectionForFocus = true. But the selection event never arrives - soonMoveruns the entire animation with the wrong height.2️⃣ iOS 15 — refocus same input with new cursor
Similar to Android -
lastSelectionisnull, soonStartcan't use the stale selection as a reasonable fallback. It uses the full input height instead.The fix consist of several coordinated changes
1️⃣ Replace lastSelection.value = null with a flag
Instead of destroying selection data, introduce
selectionUpdatedSinceHide:falseinonEndwhen keyboard hidestrueinonSelectionChangewhen any selection arrivesonStart, the "fresh selection" check becomes:This preserves the iOS 15 detection (stale selection has
selectionUpdatedSinceHide = false, soonStartcorrectly enters the pending path) while keeping the data available as a fallback.2️⃣ Use stale selection as best-effort fallback in
onStartWhen the selection is stale (not fresh) but targets the same input, use
updateLayoutFromSelection()instead of falling back toinput.value. The stale caret position (e.g.y=43) is much closer to correct than the full input height (e.g.180). If a fresh selection arrives later (iOS 15), it will overwrite this — but on Android where it never arrives, the stale value is already correct.3️⃣ Handle same-target refocus in onSelectionChange
Since
lastSelectionis no longer nulled, when iOS 15 refocuses the same input,onSelectionChangearrives withe.target === lastTarget. The existing check if (e.target !== lastTarget) won't enter the deferred-setup block. Fix by also checking the pending flag:This ensures the deferred selection setup runs regardless of whether the target changed, as long as
onStartflagged it as pending.4️⃣ Conditional cleanup of pendingSelectionForFocus in onEnd
To prevent the pending flag from leaking into the next focus session (Android case where selection never arrives), clear it in
onEnd- but only when the keyboard was actually appearing (keyboardWillAppear.value), not during a focus switch with the same keyboard height. Otherwise, toolbar focus switching breaks:onEndfires immediately (no animation), clearing the flag beforeonSelectionChangehas a chance to process the deferred scroll.Closes #1394
📢 Changelog
JS
types.tsfile;KeyboardAwareScrollViewtests (70% test coverage, texting 5 most critical/hard to catch bugs);🤔 How Has This Been Tested?
Tested manually and via this PR.
📸 Screenshots (if appropriate):
Screen.Recording.2026-03-27.at.11.03.48.mov
Screen.Recording.2026-03-27.at.11.02.07.mov
Simulator.Screen.Recording.-.iPhone.13.Pro.-.2026-03-27.at.10.58.29.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-03-27.at.11.00.05.mov
Screen.Recording.2026-03-27.at.10.56.38.mov
📝 Checklist