You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Split - Input is not focused when keyboard is opened after switching to other tab #101157
Version Number: 9.4.78-0 Reproducible in staging?: Yes Reproducible in production?: No If this was caught during regression testing, add the test name, ID and link from BrowserStack:#100149 Email or phone of affected tester (no customers):jsdoijnodsdisojo@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Samsung Galaxy S25 FE / Android 16 App Component: Money Requests
Action Performed:
Launch Expensify app.
Go to workspace chat.
Create an expense.
Open the expense.
Tap More > Split > Percent.
Tap on the input field.
Switch to Amount tab.
Expected Result:
When keyboard is opened on Amount tab, the input field should be focused.
Actual Result:
When keyboard is opened on Amount tab, the input field is not focused.
Entering amount changes the amount split field without focus.
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Classification: Frontend bug Causing PR: #100149 - "fix: Android - Split - Percentage input has to be focused twice to be able to edit it" by @TaduJR (High confidence) Related Issues: #78089 (the bug #100149 was fixing — a revert reintroduces it)
Recommendation: ROLL FORWARD
#100149 bumps react-native-pager-view 8.0.0 → 9.0.4, and v9.0.0 replaced the entire Android pager with a Jetpack Compose HorizontalPager, which no longer preserves Android view focus across a page change. Reverting is clean but reintroduces #78089 and swaps the native pager under all 7 OnyxTabNavigator call sites on deploy-blocker timing, so the one-prop App-side fix below is the lower-risk path.
Assigned: @TaduJR (PR author), @brunovjk and @carlosmiceli (approving reviewers) from #100149 — already assigned by melvin-bot, and this investigation confirms that assignment was correct (see caveat below). Labels: No changes. DeployBlockerCash stays (an App PR caused this, and it does not reproduce on production). DeployBlocker was not present, so nothing to remove.
📋 Detailed Analysis
Evidence
The auto-assignment came from the tester pasting the PR link into the "caught during regression testing" field, so it was not independent evidence. I verified it separately, and it holds up.
1. The entire Split-expense focus code path is byte-identical between production and staging.
git diff origin/production origin/staging (27adede6d33 → 5b9770e2155, i.e. 9.4.77-4 → 9.4.78-0) across every file that can affect this flow — src/pages/iou/SplitList/, SplitListItem.tsx, SplitListItem/, useAutoFocusInput.ts, DynamicSplitExpensePage.tsx, OnyxTabNavigator.tsx, OnyxTabNavigatorConfig/, TabSelector/, PercentageForm, MoneyRequestAmountInput, ScreenFocusArbiter.ts, NavigationFocusReturn.ts — returns only:
The TabSelector delta only adds an EducationalTooltip around a tab badge, and it renders solely when badgeText is set — the Split tabs have no badges, so it is inert here. The package.json delta is the version string plus react-native-pager-view: 8.0.0 → 9.0.4.
git log over the Split focus files confirms the same thing from the other direction: nothing has landed in src/pages/iou/SplitList/, SplitListItem.tsx, SplitListItem/, useAutoFocusInput.ts or DynamicSplitExpensePage.tsx since 2026-09-10, before this deploy window opened.
2. No other PR in the checklist is a candidate. I scanned all 38 PRs in #101132. #100149 is the only one touching anything in this code path, and it is the only native/dependency change affecting the pager.
So the only behavioural delta that can explain an Android-only, staging-only tab-focus regression is the react-native-pager-view major bump.
3. The bump is a full Android rewrite, not a patch. Upstream release notes for v9.0.0 read feat(android): jetpack compose support (#1092). v8 shipped ViewPagerAdapter.kt / ViewPagerViewHolder.kt / NestedScrollableHost.kt backed by androidx.viewpager2.widget.ViewPager2; v9 ships ComposePagerView.kt backed by androidx.compose.foundation.pager.HorizontalPager.
Reproduces on staging only, not production — consistent with the bump being the sole delta.
Verification
react-native-tab-view (used by @react-navigation/material-top-tabs, which OnyxTabNavigator builds on) renders through react-native-pager-view — confirmed in node_modules/react-native-tab-view/src/PagerViewAdapter.tsx. So every OnyxTabNavigator, including the Split tabs, goes through the swapped native implementation.
Affected App files (all permalinked at the deployed staging SHA 5b9770e2155):
src/pages/iou/DynamicSplitExpensePage.tsx#L640-L695 — the Amount / Percent / Date tabs. lazyLoadEnabled is not passed (defaults false), so all three tab screens are mounted at once inside the pager, and shouldDismissKeyboardBeforeTabSwitch is not passed.
src/libs/Navigation/OnyxTabNavigator.tsx#L221 and #L298-L307 — because the Split page does not opt in, hasKeyboardToDismiss() is false and handleTabPress returns early, letting react-navigation jump synchronously while the percentage input still holds native focus and the keyboard is up.
src/hooks/useAutoFocusInput.ts#L83-L88 — even when it does run, shouldSkipAutoFocusDueToExistingFocus() and tryClaim(Priorities.AUTO) gate the programmatic focus.
Library-side confirmation in the installed v9.0.4:
node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt:584-610 — each RN page is hosted in an AndroidView, and attachChildre-parents the page view ((child.parent as? ViewGroup)?.removeView(child) → container.removeAllViews() → container.addView(child, …)).
Same file, :646-656 — DEFAULT_BEYOND_VIEWPORT_PAGE_COUNT = 1, so a page two positions away leaves the composition and its AndroidView is disposed.
grep -rn -i "focus\|descendant" node_modules/react-native-pager-view/android/src/main/ returns zero matches: the v9 Android implementation never requests focus for the incoming page, nor manages focus containment.
Root Cause
On the Percent tab the user taps a percentage input. The native EditText takes Android focus and the soft keyboard opens.
The user taps the Amount tab. Because DynamicSplitExpensePage does not pass shouldDismissKeyboardBeforeTabSwitch, handleTabPress returns early (OnyxTabNavigator.tsx:299-302) and the jump happens immediately, with keyboardDismissMode="none" keeping the keyboard up.
Under v8, pages lived in a ViewPager2/RecyclerViewViewGroup with ordinary Android view-focus semantics and stayed attached within offscreenPageLimit. Under v9, the page change runs through Compose: attachChild detaches and re-parents the page views, and ViewGroup.removeView on a subtree that holds focus makes Android clear that focus.
Nothing then re-establishes focus on the Amount tab — not the library (no focus code at all in v9's Android sources) and not JS (the auto-focus ref is only attached for a selected row, and its focus attempt is gated).
The result is exactly the reported state: the keyboard is up, but no input is visibly focused, so onFocus → focusHandler → onInputFocus never fires and the JS-side focused styling (hideFocusedState={false}) and scroll-into-view never run, even though keystrokes still land in the amount field.
Flagged as unverified: I could not run an Android build here, so the precise native detail of which view ends up holding the input connection after the re-parent (step 4) is Medium confidence. The causation — that the react-native-pager-view bump in #100149 is the only possible source of this regression — is High confidence, since it is the sole delta in this code path between production and staging.
Suggested fix (roll forward)
One prop, in src/pages/iou/DynamicSplitExpensePage.tsx:640:
This prop exists for exactly this case — see its doc comment at OnyxTabNavigator.tsx#L73-L78 ("Opt in from tab screens that hold a text input next to tabs…") — it already ships in production, and DistanceRequestStartPage.tsx#L181 is the existing precedent. With it, the keyboard is fully dismissed before the jump, so the user never lands on the Amount tab with a keyboard up over an unfocused input; tapping the amount input then focuses it normally.
Fallback: if that cannot be validated on an Android device inside the deploy window, revert #100149 instead. It is a clean 3-file revert (package.json, package-lock.json, ios/Podfile.lock), but note it reintroduces #78089 and has a companion Mobile-Expensify PR linked from #100149 that needs to be handled alongside it.
💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for Expensify/App:
Rule:UI-4 Dismiss the keyboard before switching tabs when a tab screen holds a text input
Condition: Flag an OnyxTabNavigator whose tab screens render a text input (TextInput, MoneyRequestAmountInput, PercentageForm, AmountForm) but that does not pass shouldDismissKeyboardBeforeTabSwitch; add the prop so the tab only changes once the keyboard is fully hidden.
Why: With keyboardDismissMode="none", a synchronous tab jump leaves the soft keyboard up over the outgoing tab's focused input. Whether focus survives is then up to the native pager's view-recycling semantics, which is why a react-native-pager-view major bump (fix: Android - Split - Percentage input has to be focused twice to be able to edit it #100149) turned this latent gap into a deploy blocker (this issue). Currently 6 of the 7 OnyxTabNavigator call sites omit the prop, including IOURequestStartPage.tsx#L298 whose Manual tab holds the amount input.
Search pattern: files matching grep -rl "OnyxTabNavigator" src/pages that do not also match shouldDismissKeyboardBeforeTabSwitch, then check whether any TopTab.Screen subtree renders a text input.
The full rule file, with the ❌/✅ code inline in App's coding-standards format, would go in Expensify/App itself (.claude/skills/app-coding-standards/rules/ui-4-*.md plus a SKILL.md index bullet). If it's worth adding, a maintainer can open it there as a draft PR.
📣 @KisaneNeko! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>
Activity
applause-bot commentedon Sep 15, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/3866294c2dd6dd971b489a05a4c1421cdca437c61c5fae2207317f6384ec831e
melvin-bot commentedon Sep 15, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #100149
melvin-bot commentedon Sep 15, 2026
@carlosmiceli @brunovjk you have been assigned to this deploy blocker because you reviewed the offending PR: #100149
github-actions commentedon Sep 15, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
melvin-bot commentedon Sep 15, 2026
@TaduJR you have been assigned to this deploy blocker because you authored the offending PR: #100149
melvin-bot commentedon Sep 15, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 15, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: #100149 - "fix: Android - Split - Percentage input has to be focused twice to be able to edit it" by @TaduJR (High confidence)
Related Issues: #78089 (the bug #100149 was fixing — a revert reintroduces it)
Recommendation: ROLL FORWARD
#100149 bumps
react-native-pager-view8.0.0 → 9.0.4, and v9.0.0 replaced the entire Android pager with a Jetpack ComposeHorizontalPager, which no longer preserves Android view focus across a page change. Reverting is clean but reintroduces #78089 and swaps the native pager under all 7OnyxTabNavigatorcall sites on deploy-blocker timing, so the one-prop App-side fix below is the lower-risk path.Assigned: @TaduJR (PR author), @brunovjk and @carlosmiceli (approving reviewers) from #100149 — already assigned by melvin-bot, and this investigation confirms that assignment was correct (see caveat below).
Labels: No changes.
DeployBlockerCashstays (an App PR caused this, and it does not reproduce on production).DeployBlockerwas not present, so nothing to remove.📋 Detailed Analysis
Evidence
The auto-assignment came from the tester pasting the PR link into the "caught during regression testing" field, so it was not independent evidence. I verified it separately, and it holds up.
1. The entire Split-expense focus code path is byte-identical between production and staging.
git diff origin/production origin/staging(27adede6d33→5b9770e2155, i.e.9.4.77-4→9.4.78-0) across every file that can affect this flow —src/pages/iou/SplitList/,SplitListItem.tsx,SplitListItem/,useAutoFocusInput.ts,DynamicSplitExpensePage.tsx,OnyxTabNavigator.tsx,OnyxTabNavigatorConfig/,TabSelector/,PercentageForm,MoneyRequestAmountInput,ScreenFocusArbiter.ts,NavigationFocusReturn.ts— returns only:The
TabSelectordelta only adds anEducationalTooltiparound a tab badge, and it renders solely whenbadgeTextis set — the Split tabs have no badges, so it is inert here. Thepackage.jsondelta is the version string plusreact-native-pager-view: 8.0.0 → 9.0.4.git logover the Split focus files confirms the same thing from the other direction: nothing has landed insrc/pages/iou/SplitList/,SplitListItem.tsx,SplitListItem/,useAutoFocusInput.tsorDynamicSplitExpensePage.tsxsince 2026-09-10, before this deploy window opened.2. No other PR in the checklist is a candidate. I scanned all 38 PRs in #101132. #100149 is the only one touching anything in this code path, and it is the only native/dependency change affecting the pager.
So the only behavioural delta that can explain an Android-only, staging-only tab-focus regression is the
react-native-pager-viewmajor bump.3. The bump is a full Android rewrite, not a patch. Upstream release notes for v9.0.0 read
feat(android): jetpack compose support (#1092). v8 shippedViewPagerAdapter.kt/ViewPagerViewHolder.kt/NestedScrollableHost.ktbacked byandroidx.viewpager2.widget.ViewPager2; v9 shipsComposePagerView.ktbacked byandroidx.compose.foundation.pager.HorizontalPager.Reproduces on staging only, not production — consistent with the bump being the sole delta.
Verification
react-native-tab-view(used by@react-navigation/material-top-tabs, whichOnyxTabNavigatorbuilds on) renders throughreact-native-pager-view— confirmed innode_modules/react-native-tab-view/src/PagerViewAdapter.tsx. So everyOnyxTabNavigator, including the Split tabs, goes through the swapped native implementation.Affected App files (all permalinked at the deployed staging SHA
5b9770e2155):src/pages/iou/DynamicSplitExpensePage.tsx#L640-L695— the Amount / Percent / Date tabs.lazyLoadEnabledis not passed (defaultsfalse), so all three tab screens are mounted at once inside the pager, andshouldDismissKeyboardBeforeTabSwitchis not passed.src/libs/Navigation/OnyxTabNavigator.tsx#L378—keyboardDismissMode="none", so the soft keyboard survives a tab switch.src/libs/Navigation/OnyxTabNavigator.tsx#L221and#L298-L307— because the Split page does not opt in,hasKeyboardToDismiss()isfalseandhandleTabPressreturns early, letting react-navigation jump synchronously while the percentage input still holds native focus and the keyboard is up.src/components/SelectionList/ListItem/SplitListItem.tsx#L92-L97— theuseAutoFocusInputcallback ref is wired only for the selected, editable row, so JS does not re-focus anything on the incoming Amount tab.src/hooks/useAutoFocusInput.ts#L83-L88— even when it does run,shouldSkipAutoFocusDueToExistingFocus()andtryClaim(Priorities.AUTO)gate the programmatic focus.Library-side confirmation in the installed v9.0.4:
node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt:584-610— each RN page is hosted in anAndroidView, andattachChildre-parents the page view ((child.parent as? ViewGroup)?.removeView(child)→container.removeAllViews()→container.addView(child, …)).:646-656—DEFAULT_BEYOND_VIEWPORT_PAGE_COUNT = 1, so a page two positions away leaves the composition and itsAndroidViewis disposed.grep -rn -i "focus\|descendant" node_modules/react-native-pager-view/android/src/main/returns zero matches: the v9 Android implementation never requests focus for the incoming page, nor manages focus containment.Root Cause
EditTexttakes Android focus and the soft keyboard opens.DynamicSplitExpensePagedoes not passshouldDismissKeyboardBeforeTabSwitch,handleTabPressreturns early (OnyxTabNavigator.tsx:299-302) and the jump happens immediately, withkeyboardDismissMode="none"keeping the keyboard up.ViewPager2/RecyclerViewViewGroupwith ordinary Android view-focus semantics and stayed attached withinoffscreenPageLimit. Under v9, the page change runs through Compose:attachChilddetaches and re-parents the page views, andViewGroup.removeViewon a subtree that holds focus makes Android clear that focus.The result is exactly the reported state: the keyboard is up, but no input is visibly focused, so
onFocus→focusHandler→onInputFocusnever fires and the JS-side focused styling (hideFocusedState={false}) and scroll-into-view never run, even though keystrokes still land in the amount field.Flagged as unverified: I could not run an Android build here, so the precise native detail of which view ends up holding the input connection after the re-parent (step 4) is Medium confidence. The causation — that the
react-native-pager-viewbump in #100149 is the only possible source of this regression — is High confidence, since it is the sole delta in this code path between production and staging.Suggested fix (roll forward)
One prop, in
src/pages/iou/DynamicSplitExpensePage.tsx:640:This prop exists for exactly this case — see its doc comment at
OnyxTabNavigator.tsx#L73-L78("Opt in from tab screens that hold a text input next to tabs…") — it already ships in production, andDistanceRequestStartPage.tsx#L181is the existing precedent. With it, the keyboard is fully dismissed before the jump, so the user never lands on the Amount tab with a keyboard up over an unfocused input; tapping the amount input then focuses it normally.Fallback: if that cannot be validated on an Android device inside the deploy window, revert #100149 instead. It is a clean 3-file revert (
package.json,package-lock.json,ios/Podfile.lock), but note it reintroduces #78089 and has a companion Mobile-Expensify PR linked from #100149 that needs to be handled alongside it.💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for
Expensify/App:UI-4Dismiss the keyboard before switching tabs when a tab screen holds a text inputOnyxTabNavigatorwhose tab screens render a text input (TextInput,MoneyRequestAmountInput,PercentageForm,AmountForm) but that does not passshouldDismissKeyboardBeforeTabSwitch; add the prop so the tab only changes once the keyboard is fully hidden.keyboardDismissMode="none", a synchronous tab jump leaves the soft keyboard up over the outgoing tab's focused input. Whether focus survives is then up to the native pager's view-recycling semantics, which is why areact-native-pager-viewmajor bump (fix: Android - Split - Percentage input has to be focused twice to be able to edit it #100149) turned this latent gap into a deploy blocker (this issue). Currently 6 of the 7OnyxTabNavigatorcall sites omit the prop, includingIOURequestStartPage.tsx#L298whose Manual tab holds the amount input.grep -rl "OnyxTabNavigator" src/pagesthat do not also matchshouldDismissKeyboardBeforeTabSwitch, then check whether anyTopTab.Screensubtree renders a text input.DynamicSplitExpensePage.tsx#L640-L644DistanceRequestStartPage.tsx#L176-L182The full rule file, with the ❌/✅ code inline in App's coding-standards format, would go in
Expensify/Appitself (.claude/skills/app-coding-standards/rules/ui-4-*.mdplus aSKILL.mdindex bullet). If it's worth adding, a maintainer can open it there as a draft PR.22 remaining items
melvin-bot commentedon Oct 5, 2026
@TaduJR Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
melvin-bot commentedon Oct 7, 2026
@TaduJR Eep! 4 days overdue now. Issues have feelings too...
TaduJR commentedon Oct 9, 2026
Not Overdue. Upstream merged.
Waiting for release.
melvin-bot commentedon Oct 12, 2026
@TaduJR Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
KisaneNeko commentedon Oct 13, 2026
Fix is now available in RN Pager View v9.0.8
melvin-bot commentedon Oct 13, 2026
📣 @KisaneNeko! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Format:
melvin-bot commentedon Oct 14, 2026
@TaduJR Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Oct 16, 2026
@TaduJR Still overdue 6 days?! Let's take care of this!
TaduJR commentedon Oct 20, 2026
Not Overdue
carlosmiceli commentedon Oct 21, 2026
@TaduJR what's the hold at the moment?
TaduJR commentedon Oct 21, 2026
iOS have the same problem as android raised a pr here callstack/react-native-pager-view#1193
Getting reviewed by @troZee