Skip to content

fix(scrollable): preserve scroll position when keyboard dismisses#2676

Open
ronnymajani wants to merge 1 commit into
gorhom:masterfrom
ronnymajani:fix/preserve-scroll-on-keyboard-dismiss
Open

fix(scrollable): preserve scroll position when keyboard dismisses#2676
ronnymajani wants to merge 1 commit into
gorhom:masterfrom
ronnymajani:fix/preserve-scroll-on-keyboard-dismiss

Conversation

@ronnymajani
Copy link
Copy Markdown

Motivation

When a BottomSheet contains a scrollable and a TextInput, the following sequence fails:

  1. User scrolls down inside the sheet content.
  2. User taps a TextInput further down — keyboard appears, sheet animates up (interactive keyboard behavior).
  3. User dismisses the keyboard.
  4. The inner scroll jumps back to the top instead of staying where it was.

The user's scroll position is lost on every keyboard dismiss, which makes long forms inside a sheet effectively unusable.

Root cause

The relevant code in useScrollable.ts:

if (
  animatedKeyboardState.get().status === KEYBOARD_STATUS.SHOWN &&
  animatedAnimationState.get().status === ANIMATION_STATUS.RUNNING
) {
  return SCROLLABLE_STATUS.UNLOCKED;
}

This unlocks the scrollable during keyboard-show animations so the scroll position is preserved while the sheet translates. But it only matches KEYBOARD_STATUS.SHOWN. During the dismiss animation, the keyboard status has already flipped to HIDDEN, while the sheet is still animating back from extendedPositionWithKeyboard to extendedPosition. So this branch doesn't apply on the way down.

During that dismiss window:

  • animatedPosition is between extendedPositionWithKeyboard and extendedPosition.
  • animatedSheetState (in BottomSheet.tsx) checks for animatedPosition.value === extendedPosition (exact equality) — false during animation.
  • The special "keyboard interactive + isInTemporaryPosition" branch is also bypassed because isInTemporaryPosition was just flipped to false in getEvaluatedPosition.
  • It falls through to SHEET_STATE.OPENED.

With sheet state OPENED and the keyboard branch not matching, useScrollable returns LOCKED. handleOnScroll (in useScrollEventsHandlersDefault.ts) then fires scrollTo(scrollableRef, 0, lockPosition, false). Because the user started dragging while the sheet was EXTENDED, shouldLockInitialPosition is false, so lockPosition = 0 — instant jump to top.

The bug only manifests because OPENED is reached mid-animation. Once the animation completes and position === extendedPosition again, animatedSheetState flips back to EXTENDED and unlocks the scroll — too late, the scrollTo(0) has already fired.

What this PR does

Extends the unlock condition to also cover keyboard-dismiss animations by checking the animation's source:

const animationState = animatedAnimationState.get();
if (
  animationState.status === ANIMATION_STATUS.RUNNING &&
  (animatedKeyboardState.get().status === KEYBOARD_STATUS.SHOWN ||
    animationState.source === ANIMATION_SOURCE.KEYBOARD)
) {
  return SCROLLABLE_STATUS.UNLOCKED;
}

animationState.source === ANIMATION_SOURCE.KEYBOARD is true for both the show and dismiss animations triggered by the keyboard state machine, so it cleanly covers the missing case without weakening the existing one.

Files changed

  • src/hooks/useScrollable.ts — extended unlock condition; added an ANIMATION_SOURCE import.

No API changes, no type changes.

Verification

  • yarn typescript — clean
  • yarn lint --error-on-warnings src/hooks/useScrollable.ts — clean
  • Manual test on Android with a BottomSheetScrollView containing several inputs: scroll position is now preserved across keyboard show and dismiss. Behavior unchanged for non-keyboard animations and for sheets without keyboard interactions.

Notes

This is a pure scroll-preservation fix and is independent of #2675 (which fixes a separate Android-only scroll-cancellation bug on first sheet open). Either can land independently.

When the keyboard dismisses, the inner scroll jumps back to offset 0
even though the user had scrolled.

During the keyboard-down animation, the sheet `position` is between
`extendedPositionWithKeyboard` and `extendedPosition`, so
`animatedSheetState` falls through to `OPENED` (it only reports
`EXTENDED` when `position` exactly matches `extendedPosition`).
`useScrollable` then returns `LOCKED`, and `handleOnScroll` forces
`scrollTo(0, 0)` because `shouldLockInitialPosition` was cleared earlier
when the sheet was `EXTENDED` at drag start.

The existing unlock condition only covers `KEYBOARD_STATUS.SHOWN`.
Extend it to also cover animations whose `source` is
`ANIMATION_SOURCE.KEYBOARD`, which handles the dismiss path where the
keyboard is already `HIDDEN` but the sheet is still animating.
@5ZYSZ3K
Copy link
Copy Markdown

5ZYSZ3K commented May 21, 2026

Hi, I was able to see the issue, I can confirm that it's there. Here's a recording previewing it:

Screen.Recording.2026-05-21.at.15.31.03.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants