Skip to content

fix: KeyboardAwareScrollView re-focus after hardware keyboard dismissal#1403

Merged
kirillzyusko merged 9 commits intomainfrom
fix/kasw-re-focus-without-selection-event
Mar 27, 2026
Merged

fix: KeyboardAwareScrollView re-focus after hardware keyboard dismissal#1403
kirillzyusko merged 9 commits intomainfrom
fix/kasw-re-focus-without-selection-event

Conversation

@kirillzyusko
Copy link
Copy Markdown
Owner

@kirillzyusko kirillzyusko commented Mar 26, 2026

📜 Description

Fixed a problem, when re-focusing field (after dismiss via system button) pushes the field significantly higher.

💡 Motivation and Context

The onEnd handler nulls lastSelection.value when the keyboard hides (e.height === 0). This was added intentionally in #1234 to solve an iOS 15 problem: without nulling, onStart couldn't distinguish a fresh selection (that arrived before onStart in the current session) from a stale selection (leftover from the previous session). On iOS 15, selection sometimes arrives after onStart, so the pendingSelectionForFocus mechanism was introduced to defer layout setup until the selection arrives.

However, nulling lastSelection causes two regressions:

1️⃣ Android — refocus same input

Android doesn't re-emit onSelectionChange when refocusing the same input at the same cursor position. After the null, onStart sees lastSelection.value?.target !== e.target (because null?.target is undefined), falls back to layout.value = input.value (full input height, e.g. 180px instead of caret height 43px), and sets pendingSelectionForFocus = true. But the selection event never arrives - so onMove runs the entire animation with the wrong height.

2️⃣ iOS 15 — refocus same input with new cursor

Similar to Android - lastSelection is null, so onStart can'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:

  • Set to false in onEnd when keyboard hides
  • Set to true in onSelectionChange when any selection arrives
  • In onStart, the "fresh selection" check becomes:
lastSelection.value?.target === e.target && selectionUpdatedSinceHide.value

This preserves the iOS 15 detection (stale selection has selectionUpdatedSinceHide = false, so onStart correctly enters the pending path) while keeping the data available as a fallback.

2️⃣ Use stale selection as best-effort fallback in onStart

When the selection is stale (not fresh) but targets the same input, use updateLayoutFromSelection() instead of falling back to input.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.

if (lastSelection.value?.target === e.target) {
  updateLayoutFromSelection(); // stale but same target — use as fallback
} else if (input.value) {
  layout.value = input.value; // different target or no selection at all
}
pendingSelectionForFocus.value = true;

3️⃣ Handle same-target refocus in onSelectionChange

Since lastSelection is no longer nulled, when iOS 15 refocuses the same input, onSelectionChange arrives with e.target === lastTarget. The existing check if (e.target !== lastTarget) won't enter the deferred-setup block. Fix by also checking the pending flag:

if (e.target !== lastTarget || pendingSelectionForFocus.value) {

This ensures the deferred selection setup runs regardless of whether the target changed, as long as onStart flagged 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: onEnd fires immediately (no animation), clearing the flag before onSelectionChange has a chance to process the deferred scroll.

if (e.height === 0) {
  selectionUpdatedSinceHide.value = false;
} else if (keyboardWillAppear.value) {
  pendingSelectionForFocus.value = false;
}

Closes #1394

📢 Changelog

JS

  • create separate types.ts file;
  • created KeyboardAwareScrollView tests (70% test coverage, texting 5 most critical/hard to catch bugs);
  • fixed a problem with re-focus on Android;

🤔 How Has This Been Tested?

Tested manually and via this PR.

📸 Screenshots (if appropriate):

Before After
Screen.Recording.2026-03-27.at.11.03.48.mov
Screen.Recording.2026-03-27.at.11.02.07.mov
iOS 15 iOS 26 Android
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

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

@kirillzyusko kirillzyusko self-assigned this Mar 26, 2026
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🤖 android Android specific tests You added or changed unit tests KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component labels Mar 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 26, 2026

📊 Package size report

Current size Target Size Difference
309372 bytes 307771 bytes 1601 bytes 📈

@kirillzyusko kirillzyusko marked this pull request as ready for review March 27, 2026 10:14
@kirillzyusko kirillzyusko merged commit ba41d5d into main Mar 27, 2026
16 checks passed
@kirillzyusko kirillzyusko deleted the fix/kasw-re-focus-without-selection-event branch March 27, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Android specific 🐛 bug Something isn't working KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component tests You added or changed unit tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyboardAwareScrollView 1.21.1 doesn't work as expected

1 participant