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
[$250] Android - Spend - "Apply" button is overlapped by keyboard in exposed filter. #105145
Version Number: 9.5.21-0 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#104671 Email or phone of affected tester (no customers):ibellicotest+3215@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Motorola MotoG60 / Android 12 (Hybrid app) App Component: Search
Action Performed:
Open the Expensify app.
Navigate to "Spend" > "Expenses"
Tap on "FIlters" icon > "Withdrawal ID"
Enter any ID > "Confirm" > "Apply Filters"
Tap on exposed filter.
Tap on text input to open keyboard.
Note that "Apply" button is overlapped by keyboard and user can´t scroll down to it.
Expected Result:
"Apply" button should be completelly visible while editing withdrawal ID via exposed filter.
Actual Result:
"Apply" button is overlapped by keyboard while editing withdrawal ID via exposed filter and user is unable to scroll down to it.
On small screens, every text filter chip (Withdrawal ID, Description, Title, Report ID, and others) opens TextFilterPopup inside a bottom-docked FilterPopupButton sheet. Two problems stack up:
Keyboard room comes only from the modal-level avoidKeyboard. That flag wraps the sheet in a padding KeyboardAvoidingView (ReanimatedModal/index.tsx:244). On this Android device it adds too little padding, or none, so the keyboard covers "Apply". The selection-list sheets stopped using avoidKeyboard for inputs for this reason. They pad the content with useReanimatedKeyboardAnimation instead (useSelectionListPopoverLayout.ts:90-105).
There is no scroll fallback.BasePopup is a plain View with the input and then ActionButtons. FilterPopupButton also turns off the modal's only ScrollView wrapper (line 160). Once "Apply" is covered, you can't scroll to it.
What changes do you think we should make in order to solve the problem?
In FilterPopupButton, on small screens, follow the useSelectionListPopoverLayout pattern:
Stop passing avoidKeyboard to PopoverWithMeasuredContent.
Make the content wrapper (line 163) an Animated.View. Set its paddingBottom to max(safeAreaBottom, -keyboardHeight) from useReanimatedKeyboardAnimation. The sheet then rises with the keyboard and keeps "Apply" visible.
This fixes all text filter chips at once, since they share this wrapper.
What alternative solutions did you explore? (Optional)
Wrap BasePopup's children and ActionButtons in a ScrollView with keyboardShouldPersistTaps="handled". This makes "Apply" reachable but doesn't keep it in view. It is a good safety net to pair with the main fix.
Set automaticOffset on the shared KeyboardAvoidingView in ReanimatedModal. This changes every modal that uses avoidKeyboard, so it needs much broader QA.
Investigation notes
Confidence: medium. The component chain and the missing scroll fallback are confirmed in code. The native reason the padding falls short is not confirmed. One likely cause is a mismatch between the Dialog window height and the screen height (for example, the 3-button nav bar on HybridApp). This run could only test on web, so it couldn't reproduce the Android keyboard. Check the fix on a physical Android device with 3-button navigation.
avoidKeyboard reaches Android intact: PopoverWithMeasuredContent → Popover/index.native (bottom-docked) → Modal/index.android → BaseModal → ReanimatedModal. The avoidKeyboard={false} in Modal/index.tsx applies only on web.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/105145) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
changed the title [-]Android - Spend - "Apply" button is overlapped by keyboard in exposed filter.[/-][+][$175] Android - Spend - "Apply" button is overlapped by keyboard in exposed filter.[/+]on Oct 27, 2026
Please re-state the problem that we are trying to solve in this issue.
On Android native, when opening an exposed filter popup in Spend (such as the Merchant, Description, Category, or Title filter popup) and focusing the text input, the soft keyboard overlaps the bottom "Apply" button instead of pushing the popup cleanly above the keyboard.
What is the root cause of that problem?
The exposed filter popups (TextFilterPopup, etc.) are rendered inside <PopoverWithMeasuredContent> via src/components/Search/FilterDropdowns/FilterPopupButton.tsx (lines 84–99):
Across the entire codebase, <KeyboardProvider> (src/components/KeyboardProvider/index.tsx lines 7–9), <BaseModal> (src/components/Modal/BaseModal.tsx lines 117–118), and <ReanimatedModal> (src/components/Modal/ReanimatedModal/index.tsx lines 55–56) all default to:
However, src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx (line 58) is the only modal wrapper in the codebase that still defaults statusBarTranslucent to false:
statusBarTranslucent=false,
and passes statusBarTranslucent={statusBarTranslucent} down to <Popover> (line 237) -> <Modal> (BaseModal.tsx line 322) -> <ReanimatedModal> -> React Native's <Modal>.
Why statusBarTranslucent = false (combined with navigationBarTranslucent = true) causes the exact keyboard overlap on Android:
In React Native's Android ReactModalHostView.kt, navigationBarTranslucent = true enables edge-to-edge on the modal Dialog window, while statusBarTranslucent = false sets fitsSystemWindows = true on the modal's contentView (FrameLayout).
Because fitsSystemWindows = true is set on contentView, Android insets the modal's root view group from the top by statusBarHeight (e.g. 40dp / 80px on Pixel 7 Pro) and from the bottom by navigationBarHeight.
Inside ReanimatedModal (src/components/Modal/ReanimatedModal/index.tsx lines 254–260), when avoidKeyboard is true, the modal content is wrapped in react-native-keyboard-controller's <KeyboardAvoidingView behavior="padding">.
In react-native-keyboard-controller's KeyboardAvoidingView (src/components/KeyboardAvoidingView/index.tsx lines 98–107), relativeKeyboardHeight() computes:
Here, screenHeight comes from the Activity's full edge-to-edge window (where statusBarTranslucent = true), while frame.value comes from onLayout inside the modal's dialogRootViewGroup (frame.value.y = 0, frame.value.height = screenHeight - statusBarHeight - navigationBarHeight).
Because frame.value.y is 0 relative to dialogRootViewGroup instead of reflecting the statusBarHeight top offset applied by fitsSystemWindows = true, frame.value.y + frame.value.height under-reports the view's bottom screen coordinate by statusBarHeight.
As a result, KeyboardAvoidingView applies paddingBottom that is short by statusBarHeight, leaving the bottom-docked "Apply" button overlapped by the top of the Android keyboard!
What changes do you think we should make in order to solve the problem?
In src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx (line 58), change the default value of statusBarTranslucent from false to true so PopoverWithMeasuredContent is consistent with BaseModal, ReanimatedModal, and KeyboardProvider:
With statusBarTranslucent = true, the modal's coordinate space on Android matches the main window and KeyboardProvider, so KeyboardAvoidingView calculates the exact relativeKeyboardHeight and pushes the "Apply" button completely above the keyboard.
What alternative solutions did you explore? (Optional)
Passing statusBarTranslucent only in FilterPopupButton.tsx was considered, but every other modal in the app already defaults statusBarTranslucent = true in BaseModal.tsx and ReanimatedModal/index.tsx. Updating the default in PopoverWithMeasuredContentBase.tsx fixes all PopoverWithMeasuredContent popups with inputs on Android consistently.
⚠️@jmquero92 Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).
⚠️@jmquero92 Thanks for your interest. To be considered for this job, please post a proposal following the proposal template (note the mandatory sections). Comments claiming the job without one are not reviewed.
Please re-state the problem that we are trying to solve in this issue.
On Android (HybridApp and native mobile), when tapping a text input inside an exposed filter popover in Spend > Expenses (such as Withdrawal ID, Description, or Merchant), the soft keyboard opens and overlaps the Apply and Reset buttons at the bottom of the popover, and the user cannot scroll the popover to reach them.
What is the root cause of that problem?
There are three cooperating causes across the filter popover and modal keyboard-avoidance chain:
FilterPopupButton Strips Its Own Bottom Safe-Area Padding as Soon as the Keyboard Opens (src/components/Search/FilterDropdowns/FilterPopupButton.tsx:75-78, :166-168 & src/components/Modal/BaseModal.tsx:298-305):
FilterPopupButton passes enableEdgeToEdgeBottomSafeAreaPadding into PopoverWithMeasuredContent (FilterPopupButton.tsx:166). In BaseModal.tsx:298-305, enableEdgeToEdgeBottomSafeAreaPadding !== undefined enables edge-to-edge mode (isUsingEdgeToEdgeMode = true), which disables the modal container's own bottom safe-area padding (shouldAddBottomSafeAreaPadding: false). However, when the user focuses the filter TextInput, isKeyboardActive flips to true, causing FilterPopupButton to drop addBottomSafeAreaPadding to false immediately while the Android window/Dialog inset still requires clearance at the bottom of the sheet.
ReanimatedModal Measures KeyboardAvoidingView Inside an Android Dialog Window Without Accounting for the Translucent Status Bar Offset (src/components/Modal/ReanimatedModal/index.tsx:243-250): ReanimatedModal renders inside React Native's <Modal statusBarTranslucent>, which creates a separate Android Dialog window whose top coordinate starts below the status bar in getWindowVisibleDisplayFrame, while react-native-keyboard-controller's KeyboardAvoidingView (behavior="padding") computes keyboard overlap against the root activity window dimensions. Because keyboardVerticalOffset is not passed, the bottom padding applied by KeyboardAvoidingView is short by the Android status bar height (~24–32dp), leaving the bottom of the Apply button covered by the top of the IME.
BasePopup Uses a Fixed Non-Scrollable <View> Container (src/components/Search/FilterDropdowns/BasePopup.tsx:47-70): BasePopup renders its header, filter input children, and ActionButtons (Reset / Apply) inside a plain <View style={[styles.pv4, style]}>, and BaseModal only wraps bottom-docked modals in a ScrollView when in landscape mode (shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode). On smaller portrait screens or with larger font scaling, even when KeyboardAvoidingView lifts the bottom-docked popover, the user cannot scroll inside BasePopup to reach the Apply button.
What changes do you think we should make in order to solve the problem?
Preserve Correct Bottom Clearance in src/components/Modal/ReanimatedModal/index.tsx and src/components/Search/FilterDropdowns/FilterPopupButton.tsx:
In ReanimatedModal/index.tsx:243-250, pass the Android translucent status-bar offset (or top safe-area inset) to KeyboardAvoidingView when rendering inside a native Android modal dialog so behavior="padding" shifts the bottom-docked modal above the full height of the keyboard without arbitrary magic padding:
Alternatively, if keeping KeyboardAvoidingView unchanged for other modals, keep addBottomSafeAreaPadding: isSmallScreenWidth in FilterPopupButton.tsx:75-78 so the popover does not collapse its bottom safe-area padding when isKeyboardActive becomes true.
Make BasePopup Scrollable with keyboardShouldPersistTaps="handled" (src/components/Search/FilterDropdowns/BasePopup.tsx:47-70):
Wrap the inner content of BasePopup in ScrollView (from @components/ScrollView) with keyboardShouldPersistTaps="handled" so that on compact Android viewports, split-screen mode, or large font sizes, the user can always scroll to and tap Apply / Reset while the keyboard is open without dismissing the keyboard first:
What alternative solutions did you explore? (Optional)
Adding arbitrary fixed bottom padding to FilterPopupButton or BaseModal (MelvinBot's approach): As @brunovjk noted, adding hardcoded padding causes double-compensation when the keyboard is closed or on devices with 3-button vs gesture navigation, and still fails on short viewports if BasePopup is not scrollable.
Only patching react-native-keyboard-controller or KeyboardAvoidingView without updating BasePopup.tsx: Fixes the ~24–32dp status-bar inset gap on standard portrait phones, but still leaves the Apply button unreachable on smaller Android screens, display-zoomed devices, or multi-field filter popovers because BasePopup is a non-scrollable <View>.
📣 @jeffrey-graham! 📣
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>
Please re-state the problem that we are trying to solve in this issue.
On Android (narrow layout / HybridApp), when opening an exposed filter chip in Spend > Expenses (such as Withdrawal ID, Description, Title, Report ID, or Merchant) and focusing the text input, the soft keyboard overlaps the bottom Apply button and the user cannot scroll down to reach it.
What is the root cause of that problem?
Coordinate-system mismatch in ReanimatedModal's KeyboardAvoidingView on Android (src/components/Search/FilterDropdowns/FilterPopupButton.tsx:145, src/components/Modal/ReanimatedModal/index.tsx:243-250):
FilterPopupButton passes avoidKeyboard unconditionally to PopoverWithMeasuredContent, which on narrow screens (isSmallScreenWidth) renders a BOTTOM_DOCKED modal in ReanimatedModal wrapped in KeyboardAvoidingView (behavior="padding", automaticOffset={false}).
Because React Native's <Modal> on Android renders inside a separate native Dialog window that is inset below the status bar (on pre-Android-15 / HybridApp), frame (from Dialog-local onLayout) has frame.y = 0 and frame.height = screenHeight - statusBarHeight - navBarHeight, whereas screenHeight and keyboard.heightWhenOpened are measured in the host Activity's full-screen coordinates.
As a result, frame.y + frame.height under-reports the container's bottom screen Y by statusBarHeight (in the reported 720x1640 video: 0 + 1522 - (1640 - 560) = 442px of padding instead of the 520px required to reach the keyboard top at y = 1080px from the Dialog bottom at y = 1600px). The sheet stops statusBarHeight (~78px) short of the keyboard top, leaving the Apply button covered by the keyboard.
FilterPopupButton strips bottomSafeAreaPaddingStyle as soon as isKeyboardActive becomes true (src/components/Search/FilterDropdowns/FilterPopupButton.tsx:75-78):
FilterPopupButton passes enableEdgeToEdgeBottomSafeAreaPadding (line 166), which disables BaseModal's own bottom safe-area padding (isUsingEdgeToEdgeMode = true in BaseModal.tsx:322-328), and instead applies addBottomSafeAreaPadding: isSmallScreenWidth && !isKeyboardActive on its inner content <View>.
When the input is focused, isKeyboardActive flips to true and immediately removes the bottom safe-area padding (causing a mid-animation drop and leaving Apply under the system navigation bar when a hardware keyboard is used).
What changes do you think we should make in order to solve the problem?
Follow the exact pattern established in src/hooks/useSelectionListPopoverLayout.ts:53-105 inside src/components/Search/FilterDropdowns/FilterPopupButton.tsx:
Determine whether the filter sheet should pad itself for the keyboard on narrow non-tablet screens:
Replace useKeyboardState + useBottomSafeSafeAreaPaddingStyle with useSafeAreaPaddings(true) and useReanimatedKeyboardAnimation() from react-native-keyboard-controller:
Pass avoidKeyboard={!shouldPadForKeyboard} to PopoverWithMeasuredContent (keeping avoidKeyboard enabled on wide/tablet layouts while disabling ReanimatedModal's KeyboardAvoidingView on narrow phone sheets) and render the content wrapper as <Animated.View style={[containerStyles, shouldPadForKeyboard ? keyboardRoomStyle : StyleUtils.getPaddingBottom(isSmallScreenWidth ? safeAreaPaddingBottom : 0)]}>.
Why this prevents any double compensation:
No KeyboardAvoidingView + manual padding stacking: When shouldPadForKeyboard is true, avoidKeyboard={!shouldPadForKeyboard} is false, so ReanimatedModal/index.tsx:243 skips <KeyboardAvoidingView> entirely and renders <FocusTrapForModal> instead. Only keyboardRoomStyle pads the sheet for the keyboard.
No BaseModal bottom or top safe-area stacking:FilterPopupButton passes enableEdgeToEdgeBottomSafeAreaPadding, so BaseModal.tsx:322-328 sets isUsingEdgeToEdgeMode = true (shouldAddBottomSafeAreaPadding = false), and BaseModal.tsx:255 gates modalOverlapsWithTopSafeArea on avoidKeyboard.
No safe-area + keyboard height addition:Math.max(safeAreaPaddingBottom, -keyboardOffset.get()) takes the larger of safeAreaPaddingBottom and -keyboardOffset.get(), never their sum. When the soft keyboard is closed (or a hardware keyboard has height 0), safeAreaPaddingBottom keeps Apply above the navigation bar; as the soft keyboard animates open, -keyboardOffset.get() smoothly takes over on the UI thread and lifts Apply completely above the keyboard.
What alternative solutions did you explore? (Optional)
Enabling automaticOffset on KeyboardAvoidingView in ReanimatedModal:automaticOffset measures the view's window Y asynchronously via native viewPositionInWindow, which affects all avoidKeyboard modals across the app and can over-lift in Android split-screen mode where the lower pane's screen Y is non-zero.
Wrapping TextFilterPopup / BasePopup in a ScrollView: A 1-field filter popup is short enough to fit entirely above the keyboard; making the user scroll to find Apply degrades UX compared to lifting the sheet cleanly above the keyboard.
Please re-state the problem that we are trying to solve in this issue.
As part of #104671, On the Android HybridApp, tapping an applied text chip on Spend (Withdrawal ID here, Description and Report ID behave the same) opens a bottom sheet, and focusing its input lifts the sheet with the keyboard but not far enough: Apply ends up behind the keyboard and the sheet can't be scrolled to it.
What is the root cause of that problem?
On a small screen the chip's popup is a bottom-docked Modal that asks the modal to avoid the keyboard, so ReanimatedModal wraps the whole sheet in keyboard-controller's padding KeyboardAvoidingView.
That view pads by frame.y + frame.height - (screenHeight - keyboardHeight). The two inputs come from two different Android windows. screenHeight is the height of the activity's content view, reported by a listener the library attaches once per app, and frame is the view's own onLayout inside the Dialog window that a React Native Modal opens. The formula is only right when the dialog is laid out the same way as the activity.
The activity is laid out edge-to-edge: the App's keyboard provider runs with both bars translucent, keyboard-controller sets the activity window to not fit system windows, and the keyboard height it reports is the full IME inset, navigation bar included.
The dialog starts out edge-to-edge too. The App's own modal patch has updateProperties() call enableEdgeToEdge() whenever the modal wants a translucent navigation bar, which this one does. Then the dialog is shown and React Native hands the new window to StatusBarModule, which copies the activity's status bar visibility onto it. That runs statusBarShow(), and with the edge-to-edge feature flag off that function fits the window to the system windows again on Android 11 and up. From here on the dialog's content sits below the status bar and above the navigation bar.
The flag comes from the build. The standalone app turns it on with the edgeToEdgeEnabled gradle property, which the generated entry point forwards, and React Native forces it on for a target SDK 35 app on Android 15 and up. The HybridApp is built from Mobile-Expensify, which this checkout doesn't contain, but this flag-off path is the only one I found in this code that leaves the dialog shorter than the activity, and it is why the standalone build and Android 15 don't reproduce the bug.
With screen height S, status bar s, navigation bar n and keyboard k: the dialog content is S - s - n tall, frame.y is 0 and screenHeight is S, so the padding is (S - s - n) - (S - k) = k - s - n. The sheet's bottom edge sits n above the screen bottom, so it needs k - n to meet the keyboard. The lift is short by exactly s, the status bar, which is the sheet rising and then stopping short in the video. Nothing can scroll Apply into view because BasePopup is a plain View and the popup turns the modal's scroll wrapper off.
Every avoidKeyboard bottom sheet goes through the same wrapper, so the emoji picker, a PopoverMenu with search, the describe-rule sheet and the submit-to sheet get the same short lift on that build. The searchable selection lists are the only ones that sidestep it, by padding inside the sheet and turning avoidKeyboard off.
What changes do you think we should make in order to solve the problem?
Lay the dialog out edge-to-edge again right after the status bar sync, in the App's existing React Native modal patch, so the window the keyboard view measures has the same shape as the activity it compares against. In ReactModalHostView.showOrUpdate(), after the window is handed to the status bar module:
(context asThemedReactContext).onExtraWindowCreate(window)
// StatusBarModule.onExtraWindowCreate() shows the status bar on the new window, and with the// edge-to-edge feature flag off that fits the dialog under the system bars again, undoing the// enableEdgeToEdge() that updateProperties() applied above. Lay the dialog out edge-to-edge again.if (navigationBarTranslucent &&!isEdgeToEdgeFeatureFlagOn) {
updateProperties()
}
(context as ThemedReactContext).onExtraWindowCreate(window)
Why this fixes it: the dialog content is the full screen again, so frame.y + frame.height equals screenHeight and the padding comes out as exactly the keyboard height, for this sheet and for the other four. Nothing is added on top, so there is no double compensation: the sheet still gives up its safe-area padding when the keyboard comes up, and the navigation bar overlay still draws behind the three buttons while it is down.
updateProperties() is the same call that ran before show(). It is already used on a showing dialog for prop updates, and it ends with updateSystemAppearance(), so the bar colours and icons stay in sync with the activity the way the patch intends.
the guard keeps the branch dead on any build with the flag on (standalone, Android 15 with target SDK 35), so those builds, iOS and web don't change. isEdgeToEdgeFeatureFlagOn is already imported in that file.
ship it as a new numbered patch next to 019 with a details.md entry, so the APP-M66 history in 019 stays readable. It is worth an upstream issue too, the entry can link it.
- Reason: This patch fixes an issue where it is not possible to enable a transparent navigation bar on Android, and keeps the modal's system bar icons in sync with the activity. The `dialog === newDialog` check in the call that `showOrUpdate()` posts fixes the fatal `IllegalStateException: dialog must exist when we call updateProperties` behind `APP-M66`, which happens when the modal is dismissed before that call runs. Keep this check when regenerating the patch.
on the HybridApp this also makes modals look like they do on the standalone build: the backdrop covers the status bar and the sheet rests on the screen bottom instead of padding for a navigation bar the dialog doesn't cover.
the status bar module's setHidden would fit the dialog again later. Nothing in the App's source calls it.
FilterPopupButton, BasePopup and TextFilterPopup stay as they are, and the merchant popup's keyboard-aware max height keeps working because the room above the keyboard is unchanged.
Tests: Not run yet, this is what I'd do with the PR. Jest can't exercise a native patch, so the check is on device:
build the standalone app with edgeToEdgeEnabled=false, which takes the same flag-off path as the HybridApp, on an Android 12 to 14 emulator, and run the issue steps for Withdrawal ID, Description, Report ID and Merchant: Apply sits flush above the keyboard, and the sheet no longer rests above the navigation bar
same build: the emoji picker search and the submit-to sheet lift to the keyboard
a flag-on build and iOS: nothing moves
the existing modal and filter popup Jest tests keep passing
What alternative solutions did you explore? (Optional)
Pad inside the sheet from useReanimatedKeyboardAnimation with avoidKeyboard off, like the selection lists do. It does bring Apply above the keyboard here, but it leaves the dialog fitted under the bars: the sheet's edge already sits a navigation bar above the screen bottom, so a keyboard-height pad lifts it that much too far, and the emoji picker, PopoverMenu search, describe-rule and submit-to sheets keep the short lift. The dialog is the thing that is wrong, so that is where I'd rather fix it.
automaticOffset on the modal's keyboard view. Exact in full screen, but it measures with getLocationOnScreen, so in the lower split-screen pane the pane's own offset is added to the padding, and the position lands in a promise after the first layout.
A keyboardVerticalOffset sized to the status bar: that encodes the wrong inset instead of removing it, and guesses on devices where the two differ.
A scroll view or keyboard-aware max height in TextFilterPopup: the sheet already fits above the keyboard, it is placed too low, so neither engages.
Turning the edge-to-edge flag on in the HybridApp build would make this branch unnecessary, but that is a Mobile-Expensify change that touches every screen of the host app, not something for this issue.
The hardware keyboard video in the thread is a different mechanism: the sheet drops its safe-area padding on isKeyboardActive while the reported keyboard is shorter than the inset. This patch doesn't touch that line. I'd handle it as a follow-up unless @brunovjk wants it in the same PR.
Please re-state the problem that we are trying to solve in this issue.
In the Android Expensify app, when navigating to Spend > Expenses > Filters > Withdrawal ID, entering an ID, and tapping on the exposed filter pill to edit the text input, the virtual soft keyboard pops up and overlaps the bottom "Apply" button. The user cannot scroll down past the keyboard to reach the button.
What is the root cause of that problem?
In the exposed filter modal / screen component (inside the filter view hierarchy):
The container wrapping the exposed filter input and action button is not wrapped in a properly configured KeyboardAvoidingView or ScrollView with keyboardShouldPersistTaps="handled".
On Android, when windowSoftInputMode is adjustResize (or adjustPan), the flex layout does not calculate bottom padding offset for the keyboard within modal sheets, causing the fixed-position bottom button container to be occluded beneath the soft input pane.
What changes do you think we should make in order to solve the problem?
In the exposed filter screen / component:
Wrap the content in ScreenWrapper with includeSafeAreaPaddingBottom={true} or ensure the inner form uses KeyboardAvoidingView with behavior={Platform.OS === 'ios' ? 'padding' : undefined} and keyboardVerticalOffset.
Wrap the filter input and options in a ScrollView with contentContainerStyle having adequate bottom spacing (styles.pb5 / styles.mb5) so that scrolling remains active while the keyboard is visible.
Ensure the "Apply" button is part of FixedFooter or placed within the scrollable content container so that on Android devices with compact viewports (such as Moto G60), scrolling upwards brings the button into view above the keyboard.
What specific scenarios should we cover in automated tests?
In unit/component tests:
Verify that when the withdrawal ID input receives focus and keyboard is toggled active, the action button remains within viewport scroll bounds.
Verify that on Android platforms, typing in the exposed filter input does not hide or clip the submit action.
📣 @ronanchnlps-bit! 📣
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>
⚠️@RajGahoi Thanks for your interest. To be considered for this job, please post a proposal following the proposal template (note the mandatory sections). Comments claiming the job without one are not reviewed.
Activity
applause-bot commentedon Oct 27, 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/5641d273a629159887b0e9f777cf54c0e74e54c456366f0b042cdebb6b14df5a
MelvinBot commentedon Oct 27, 2026
Proposal
What is the root cause of that problem?
On small screens, every text filter chip (Withdrawal ID, Description, Title, Report ID, and others) opens
TextFilterPopupinside a bottom-dockedFilterPopupButtonsheet. Two problems stack up:avoidKeyboard. That flag wraps the sheet in a paddingKeyboardAvoidingView(ReanimatedModal/index.tsx:244). On this Android device it adds too little padding, or none, so the keyboard covers "Apply". The selection-list sheets stopped usingavoidKeyboardfor inputs for this reason. They pad the content withuseReanimatedKeyboardAnimationinstead (useSelectionListPopoverLayout.ts:90-105).BasePopupis a plainViewwith the input and thenActionButtons.FilterPopupButtonalso turns off the modal's onlyScrollViewwrapper (line 160). Once "Apply" is covered, you can't scroll to it.What changes do you think we should make in order to solve the problem?
In
FilterPopupButton, on small screens, follow theuseSelectionListPopoverLayoutpattern:avoidKeyboardtoPopoverWithMeasuredContent.Animated.View. Set itspaddingBottomtomax(safeAreaBottom, -keyboardHeight)fromuseReanimatedKeyboardAnimation. The sheet then rises with the keyboard and keeps "Apply" visible.This fixes all text filter chips at once, since they share this wrapper.
What alternative solutions did you explore? (Optional)
BasePopup's children andActionButtonsin aScrollViewwithkeyboardShouldPersistTaps="handled". This makes "Apply" reachable but doesn't keep it in view. It is a good safety net to pair with the main fix.automaticOffseton the sharedKeyboardAvoidingViewinReanimatedModal. This changes every modal that usesavoidKeyboard, so it needs much broader QA.Investigation notes
avoidKeyboardreaches Android intact:PopoverWithMeasuredContent→Popover/index.native(bottom-docked) →Modal/index.android→BaseModal→ReanimatedModal. TheavoidKeyboard={false}inModal/index.tsxapplies only on web.FilterPopupButtondon't touch keyboard handling.MerchantFilterPopupuses the same layout, so the Merchant chip is likely affected too.Possibly related: Android & iOS - Spend - Unable to scroll down Describe your search page in landscape, "Describe your search" box hidden behind keyboard when put cursor on it in landscape
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/105145)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
j3ffffff commentedon Oct 27, 2026
melvin-bot commentedon Oct 27, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @brunovjk (
External)[-]Android - Spend - "Apply" button is overlapped by keyboard in exposed filter.[/-][+][$175] Android - Spend - "Apply" button is overlapped by keyboard in exposed filter.[/+]melvin-bot commentedon Oct 27, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022115102803454012561
camesenin commentedon Oct 27, 2026
melvin-bot commentedon Oct 27, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
apoorv-xs commentedon Oct 27, 2026
huhuli083-creator commentedon Oct 27, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android native, when opening an exposed filter popup in Spend (such as the Merchant, Description, Category, or Title filter popup) and focusing the text input, the soft keyboard overlaps the bottom "Apply" button instead of pushing the popup cleanly above the keyboard.
What is the root cause of that problem?
The exposed filter popups (
TextFilterPopup, etc.) are rendered inside<PopoverWithMeasuredContent>viasrc/components/Search/FilterDropdowns/FilterPopupButton.tsx(lines 84–99):Across the entire codebase,
<KeyboardProvider>(src/components/KeyboardProvider/index.tsxlines 7–9),<BaseModal>(src/components/Modal/BaseModal.tsxlines 117–118), and<ReanimatedModal>(src/components/Modal/ReanimatedModal/index.tsxlines 55–56) all default to:However,
src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx(line 58) is the only modal wrapper in the codebase that still defaultsstatusBarTranslucenttofalse:and passes
statusBarTranslucent={statusBarTranslucent}down to<Popover>(line 237) -><Modal>(BaseModal.tsxline 322) -><ReanimatedModal>-> React Native's<Modal>.Why
statusBarTranslucent = false(combined withnavigationBarTranslucent = true) causes the exact keyboard overlap on Android:ReactModalHostView.kt,navigationBarTranslucent = trueenables edge-to-edge on the modalDialogwindow, whilestatusBarTranslucent = falsesetsfitsSystemWindows = trueon the modal'scontentView(FrameLayout).fitsSystemWindows = trueis set oncontentView, Android insets the modal's root view group from the top bystatusBarHeight(e.g.40dp/80pxon Pixel 7 Pro) and from the bottom bynavigationBarHeight.ReanimatedModal(src/components/Modal/ReanimatedModal/index.tsxlines 254–260), whenavoidKeyboardistrue, the modal content is wrapped inreact-native-keyboard-controller's<KeyboardAvoidingView behavior="padding">.react-native-keyboard-controller'sKeyboardAvoidingView(src/components/KeyboardAvoidingView/index.tsxlines 98–107),relativeKeyboardHeight()computes:screenHeightcomes from the Activity's full edge-to-edge window (wherestatusBarTranslucent = true), whileframe.valuecomes fromonLayoutinside the modal'sdialogRootViewGroup(frame.value.y = 0,frame.value.height = screenHeight - statusBarHeight - navigationBarHeight).frame.value.yis0relative todialogRootViewGroupinstead of reflecting thestatusBarHeighttop offset applied byfitsSystemWindows = true,frame.value.y + frame.value.heightunder-reports the view's bottom screen coordinate bystatusBarHeight.KeyboardAvoidingViewappliespaddingBottomthat is short bystatusBarHeight, leaving the bottom-docked"Apply"button overlapped by the top of the Android keyboard!What changes do you think we should make in order to solve the problem?
In
src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx(line 58), change the default value ofstatusBarTranslucentfromfalsetotruesoPopoverWithMeasuredContentis consistent withBaseModal,ReanimatedModal, andKeyboardProvider:With
statusBarTranslucent = true, the modal's coordinate space on Android matches the main window andKeyboardProvider, soKeyboardAvoidingViewcalculates the exactrelativeKeyboardHeightand pushes the"Apply"button completely above the keyboard.What alternative solutions did you explore? (Optional)
statusBarTranslucentonly inFilterPopupButton.tsxwas considered, but every other modal in the app already defaultsstatusBarTranslucent = trueinBaseModal.tsxandReanimatedModal/index.tsx. Updating the default inPopoverWithMeasuredContentBase.tsxfixes allPopoverWithMeasuredContentpopups with inputs on Android consistently.45 remaining items
melvin-bot commentedon Nov 6, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
github-actions commentedon Nov 6, 2026
github-actions commentedon Nov 6, 2026
Burapa-J commentedon Nov 6, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android (HybridApp and native mobile), when tapping a text input inside an exposed filter popover in Spend > Expenses (such as Withdrawal ID, Description, or Merchant), the soft keyboard opens and overlaps the Apply and Reset buttons at the bottom of the popover, and the user cannot scroll the popover to reach them.
What is the root cause of that problem?
There are three cooperating causes across the filter popover and modal keyboard-avoidance chain:
FilterPopupButtonStrips Its Own Bottom Safe-Area Padding as Soon as the Keyboard Opens (src/components/Search/FilterDropdowns/FilterPopupButton.tsx:75-78,:166-168&src/components/Modal/BaseModal.tsx:298-305):FilterPopupButtonpassesenableEdgeToEdgeBottomSafeAreaPaddingintoPopoverWithMeasuredContent(FilterPopupButton.tsx:166). InBaseModal.tsx:298-305,enableEdgeToEdgeBottomSafeAreaPadding !== undefinedenables edge-to-edge mode (isUsingEdgeToEdgeMode = true), which disables the modal container's own bottom safe-area padding (shouldAddBottomSafeAreaPadding: false). However, when the user focuses the filterTextInput,isKeyboardActiveflips totrue, causingFilterPopupButtonto dropaddBottomSafeAreaPaddingtofalseimmediately while the Android window/Dialog inset still requires clearance at the bottom of the sheet.ReanimatedModalMeasuresKeyboardAvoidingViewInside an AndroidDialogWindow Without Accounting for the Translucent Status Bar Offset (src/components/Modal/ReanimatedModal/index.tsx:243-250):ReanimatedModalrenders inside React Native's<Modal statusBarTranslucent>, which creates a separate AndroidDialogwindow whose top coordinate starts below the status bar ingetWindowVisibleDisplayFrame, whilereact-native-keyboard-controller'sKeyboardAvoidingView(behavior="padding") computes keyboard overlap against the root activity window dimensions. BecausekeyboardVerticalOffsetis not passed, the bottom padding applied byKeyboardAvoidingViewis short by the Android status bar height (~24–32dp), leaving the bottom of the Apply button covered by the top of the IME.BasePopupUses a Fixed Non-Scrollable<View>Container (src/components/Search/FilterDropdowns/BasePopup.tsx:47-70):BasePopuprenders its header, filter input children, andActionButtons(Reset / Apply) inside a plain<View style={[styles.pv4, style]}>, andBaseModalonly wraps bottom-docked modals in aScrollViewwhen in landscape mode (shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode). On smaller portrait screens or with larger font scaling, even whenKeyboardAvoidingViewlifts the bottom-docked popover, the user cannot scroll insideBasePopupto reach the Apply button.What changes do you think we should make in order to solve the problem?
Preserve Correct Bottom Clearance in
src/components/Modal/ReanimatedModal/index.tsxandsrc/components/Search/FilterDropdowns/FilterPopupButton.tsx:ReanimatedModal/index.tsx:243-250, pass the Android translucent status-bar offset (or top safe-area inset) toKeyboardAvoidingViewwhen rendering inside a native Android modal dialog sobehavior="padding"shifts the bottom-docked modal above the full height of the keyboard without arbitrary magic padding:KeyboardAvoidingViewunchanged for other modals, keepaddBottomSafeAreaPadding: isSmallScreenWidthinFilterPopupButton.tsx:75-78so the popover does not collapse its bottom safe-area padding whenisKeyboardActivebecomestrue.Make
BasePopupScrollable withkeyboardShouldPersistTaps="handled"(src/components/Search/FilterDropdowns/BasePopup.tsx:47-70):Wrap the inner content of
BasePopupinScrollView(from@components/ScrollView) withkeyboardShouldPersistTaps="handled"so that on compact Android viewports, split-screen mode, or large font sizes, the user can always scroll to and tap Apply / Reset while the keyboard is open without dismissing the keyboard first:What alternative solutions did you explore? (Optional)
FilterPopupButtonorBaseModal(MelvinBot's approach): As@brunovjknoted, adding hardcoded padding causes double-compensation when the keyboard is closed or on devices with 3-button vs gesture navigation, and still fails on short viewports ifBasePopupis not scrollable.react-native-keyboard-controllerorKeyboardAvoidingViewwithout updatingBasePopup.tsx: Fixes the ~24–32dp status-bar inset gap on standard portrait phones, but still leaves the Apply button unreachable on smaller Android screens, display-zoomed devices, or multi-field filter popovers becauseBasePopupis a non-scrollable<View>.jeffrey-graham commentedon Nov 6, 2026
melvin-bot commentedon Nov 6, 2026
📣 @jeffrey-graham! 📣
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:
bonnhatnguyen commentedon Nov 7, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android (narrow layout / HybridApp), when opening an exposed filter chip in Spend > Expenses (such as Withdrawal ID, Description, Title, Report ID, or Merchant) and focusing the text input, the soft keyboard overlaps the bottom Apply button and the user cannot scroll down to reach it.
What is the root cause of that problem?
Coordinate-system mismatch in
ReanimatedModal'sKeyboardAvoidingViewon Android (src/components/Search/FilterDropdowns/FilterPopupButton.tsx:145,src/components/Modal/ReanimatedModal/index.tsx:243-250):FilterPopupButtonpassesavoidKeyboardunconditionally toPopoverWithMeasuredContent, which on narrow screens (isSmallScreenWidth) renders aBOTTOM_DOCKEDmodal inReanimatedModalwrapped inKeyboardAvoidingView(behavior="padding",automaticOffset={false}).react-native-keyboard-controller'sKeyboardAvoidingViewcalculates its bottom padding as:Math.max(frame.value.y + frame.value.height - (screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset), 0)<Modal>on Android renders inside a separate nativeDialogwindow that is inset below the status bar (on pre-Android-15 / HybridApp),frame(from Dialog-localonLayout) hasframe.y = 0andframe.height = screenHeight - statusBarHeight - navBarHeight, whereasscreenHeightandkeyboard.heightWhenOpenedare measured in the host Activity's full-screen coordinates.frame.y + frame.heightunder-reports the container's bottom screen Y bystatusBarHeight(in the reported 720x1640 video:0 + 1522 - (1640 - 560) = 442pxof padding instead of the520pxrequired to reach the keyboard top aty = 1080pxfrom the Dialog bottom aty = 1600px). The sheet stopsstatusBarHeight(~78px) short of the keyboard top, leaving the Apply button covered by the keyboard.FilterPopupButtonstripsbottomSafeAreaPaddingStyleas soon asisKeyboardActivebecomestrue(src/components/Search/FilterDropdowns/FilterPopupButton.tsx:75-78):FilterPopupButtonpassesenableEdgeToEdgeBottomSafeAreaPadding(line 166), which disablesBaseModal's own bottom safe-area padding (isUsingEdgeToEdgeMode = trueinBaseModal.tsx:322-328), and instead appliesaddBottomSafeAreaPadding: isSmallScreenWidth && !isKeyboardActiveon its inner content<View>.isKeyboardActiveflips totrueand immediately removes the bottom safe-area padding (causing a mid-animation drop and leaving Apply under the system navigation bar when a hardware keyboard is used).What changes do you think we should make in order to solve the problem?
Follow the exact pattern established in
src/hooks/useSelectionListPopoverLayout.ts:53-105insidesrc/components/Search/FilterDropdowns/FilterPopupButton.tsx:useKeyboardState+useBottomSafeSafeAreaPaddingStylewithuseSafeAreaPaddings(true)anduseReanimatedKeyboardAnimation()fromreact-native-keyboard-controller:avoidKeyboard={!shouldPadForKeyboard}toPopoverWithMeasuredContent(keepingavoidKeyboardenabled on wide/tablet layouts while disablingReanimatedModal'sKeyboardAvoidingViewon narrow phone sheets) and render the content wrapper as<Animated.View style={[containerStyles, shouldPadForKeyboard ? keyboardRoomStyle : StyleUtils.getPaddingBottom(isSmallScreenWidth ? safeAreaPaddingBottom : 0)]}>.Why this prevents any double compensation:
KeyboardAvoidingView+ manual padding stacking: WhenshouldPadForKeyboardistrue,avoidKeyboard={!shouldPadForKeyboard}isfalse, soReanimatedModal/index.tsx:243skips<KeyboardAvoidingView>entirely and renders<FocusTrapForModal>instead. OnlykeyboardRoomStylepads the sheet for the keyboard.BaseModalbottom or top safe-area stacking:FilterPopupButtonpassesenableEdgeToEdgeBottomSafeAreaPadding, soBaseModal.tsx:322-328setsisUsingEdgeToEdgeMode = true(shouldAddBottomSafeAreaPadding = false), andBaseModal.tsx:255gatesmodalOverlapsWithTopSafeAreaonavoidKeyboard.Math.max(safeAreaPaddingBottom, -keyboardOffset.get())takes the larger ofsafeAreaPaddingBottomand-keyboardOffset.get(), never their sum. When the soft keyboard is closed (or a hardware keyboard has height0),safeAreaPaddingBottomkeeps Apply above the navigation bar; as the soft keyboard animates open,-keyboardOffset.get()smoothly takes over on the UI thread and lifts Apply completely above the keyboard.What alternative solutions did you explore? (Optional)
automaticOffsetonKeyboardAvoidingViewinReanimatedModal:automaticOffsetmeasures the view's window Y asynchronously via nativeviewPositionInWindow, which affects allavoidKeyboardmodals across the app and can over-lift in Android split-screen mode where the lower pane's screen Y is non-zero.TextFilterPopup/BasePopupin aScrollView: A 1-field filter popup is short enough to fit entirely above the keyboard; making the user scroll to find Apply degrades UX compared to lifting the sheet cleanly above the keyboard.yuvrajangadsingh commentedon Nov 7, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
As part of #104671, On the Android HybridApp, tapping an applied text chip on Spend (Withdrawal ID here, Description and Report ID behave the same) opens a bottom sheet, and focusing its input lifts the sheet with the keyboard but not far enough: Apply ends up behind the keyboard and the sheet can't be scrolled to it.
What is the root cause of that problem?
On a small screen the chip's popup is a bottom-docked Modal that asks the modal to avoid the keyboard, so
ReanimatedModalwraps the whole sheet in keyboard-controller's paddingKeyboardAvoidingView.App/src/components/Search/FilterDropdowns/FilterPopupButton.tsx
Lines 143 to 168 in b14f359
App/src/components/PopoverWithMeasuredContent/index.tsx
Lines 35 to 46 in b14f359
App/src/components/Modal/ReanimatedModal/index.tsx
Lines 243 to 250 in b14f359
That view pads by
frame.y + frame.height - (screenHeight - keyboardHeight). The two inputs come from two different Android windows.screenHeightis the height of the activity's content view, reported by a listener the library attaches once per app, andframeis the view's ownonLayoutinside the Dialog window that a React Native Modal opens. The formula is only right when the dialog is laid out the same way as the activity.https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/src/components/KeyboardAvoidingView/index.tsx#L102-L109
https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/src/hooks/useWindowDimensions/index.ts#L8-L17
https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/android/src/main/java/com/reactnativekeyboardcontroller/listeners/WindowDimensionListener.kt#L23-L39
The activity is laid out edge-to-edge: the App's keyboard provider runs with both bars translucent, keyboard-controller sets the activity window to not fit system windows, and the keyboard height it reports is the full IME inset, navigation bar included.
App/src/components/KeyboardProvider/index.tsx
Lines 8 to 11 in b14f359
https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt#L142-L151
https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt#L460-L473
The dialog starts out edge-to-edge too. The App's own modal patch has
updateProperties()callenableEdgeToEdge()whenever the modal wants a translucent navigation bar, which this one does. Then the dialog is shown and React Native hands the new window toStatusBarModule, which copies the activity's status bar visibility onto it. That runsstatusBarShow(), and with the edge-to-edge feature flag off that function fits the window to the system windows again on Android 11 and up. From here on the dialog's content sits below the status bar and above the navigation bar.App/patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch
Lines 27 to 36 in b14f359
App/patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch
Lines 13 to 25 in b14f359
https://github.com/facebook/react-native/blob/v0.86.0/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt#L49-L61
https://github.com/facebook/react-native/blob/v0.86.0/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt#L146-L161
The flag comes from the build. The standalone app turns it on with the
edgeToEdgeEnabledgradle property, which the generated entry point forwards, and React Native forces it on for a target SDK 35 app on Android 15 and up. The HybridApp is built from Mobile-Expensify, which this checkout doesn't contain, but this flag-off path is the only one I found in this code that leaves the dialog shorter than the activity, and it is why the standalone build and Android 15 don't reproduce the bug.App/android/gradle.properties
Lines 67 to 70 in b14f359
https://github.com/facebook/react-native/blob/v0.86.0/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt#L100-L102
https://github.com/facebook/react-native/blob/v0.86.0/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt#L45-L69
With screen height S, status bar s, navigation bar n and keyboard k: the dialog content is S - s - n tall,
frame.yis 0 andscreenHeightis S, so the padding is (S - s - n) - (S - k) = k - s - n. The sheet's bottom edge sits n above the screen bottom, so it needs k - n to meet the keyboard. The lift is short by exactly s, the status bar, which is the sheet rising and then stopping short in the video. Nothing can scroll Apply into view becauseBasePopupis a plain View and the popup turns the modal's scroll wrapper off.App/src/components/Search/FilterDropdowns/BasePopup.tsx
Lines 49 to 71 in b14f359
Every
avoidKeyboardbottom sheet goes through the same wrapper, so the emoji picker, a PopoverMenu with search, the describe-rule sheet and the submit-to sheet get the same short lift on that build. The searchable selection lists are the only ones that sidestep it, by padding inside the sheet and turningavoidKeyboardoff.App/src/components/EmojiPicker/EmojiPicker.tsx
Line 284 in b14f359
App/src/components/PopoverMenu/index.tsx
Line 838 in b14f359
App/src/pages/workspace/rules/DescribeRuleModal.tsx
Line 74 in b14f359
App/src/hooks/useReportSubmitToPopover.tsx
Line 271 in b14f359
App/src/hooks/useSelectionListPopoverLayout.ts
Lines 65 to 68 in b14f359
App/src/hooks/useSelectionListPopoverLayout.ts
Line 105 in b14f359
What changes do you think we should make in order to solve the problem?
Lay the dialog out edge-to-edge again right after the status bar sync, in the App's existing React Native modal patch, so the window the keyboard view measures has the same shape as the activity it compares against. In
ReactModalHostView.showOrUpdate(), after the window is handed to the status bar module:App/patches/react-native/react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch
Lines 13 to 25 in b14f359
Why this fixes it: the dialog content is the full screen again, so
frame.y + frame.heightequalsscreenHeightand the padding comes out as exactly the keyboard height, for this sheet and for the other four. Nothing is added on top, so there is no double compensation: the sheet still gives up its safe-area padding when the keyboard comes up, and the navigation bar overlay still draws behind the three buttons while it is down.App/src/components/Search/FilterDropdowns/FilterPopupButton.tsx
Lines 73 to 78 in b14f359
App/src/components/Modal/BaseModal.tsx
Line 451 in b14f359
updateProperties()is the same call that ran beforeshow(). It is already used on a showing dialog for prop updates, and it ends withupdateSystemAppearance(), so the bar colours and icons stay in sync with the activity the way the patch intends.isEdgeToEdgeFeatureFlagOnis already imported in that file.details.mdentry, so the APP-M66 history in 019 stays readable. It is worth an upstream issue too, the entry can link it.App/patches/react-native/details.md
Lines 149 to 154 in b14f359
setHiddenwould fit the dialog again later. Nothing in the App's source calls it.FilterPopupButton,BasePopupandTextFilterPopupstay as they are, and the merchant popup's keyboard-aware max height keeps working because the room above the keyboard is unchanged.App/src/components/Search/FilterDropdowns/MerchantFilterPopup.tsx
Lines 55 to 57 in b14f359
Tests: Not run yet, this is what I'd do with the PR. Jest can't exercise a native patch, so the check is on device:
edgeToEdgeEnabled=false, which takes the same flag-off path as the HybridApp, on an Android 12 to 14 emulator, and run the issue steps for Withdrawal ID, Description, Report ID and Merchant: Apply sits flush above the keyboard, and the sheet no longer rests above the navigation barWhat alternative solutions did you explore? (Optional)
useReanimatedKeyboardAnimationwithavoidKeyboardoff, like the selection lists do. It does bring Apply above the keyboard here, but it leaves the dialog fitted under the bars: the sheet's edge already sits a navigation bar above the screen bottom, so a keyboard-height pad lifts it that much too far, and the emoji picker, PopoverMenu search, describe-rule and submit-to sheets keep the short lift. The dialog is the thing that is wrong, so that is where I'd rather fix it.automaticOffseton the modal's keyboard view. Exact in full screen, but it measures withgetLocationOnScreen, so in the lower split-screen pane the pane's own offset is added to the padding, and the position lands in a promise after the first layout.https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/src/components/KeyboardAvoidingView/index.tsx#L140-L160
https://github.com/kirillzyusko/react-native-keyboard-controller/blob/1.22.4/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt#L56-L60
keyboardVerticalOffsetsized to the status bar: that encodes the wrong inset instead of removing it, and guesses on devices where the two differ.TextFilterPopup: the sheet already fits above the keyboard, it is placed too low, so neither engages.isKeyboardActivewhile the reported keyboard is shorter than the inset. This patch doesn't touch that line. I'd handle it as a follow-up unless @brunovjk wants it in the same PR.RajGahoi commentedon Nov 7, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In the Android Expensify app, when navigating to Spend > Expenses > Filters > Withdrawal ID, entering an ID, and tapping on the exposed filter pill to edit the text input, the virtual soft keyboard pops up and overlaps the bottom "Apply" button. The user cannot scroll down past the keyboard to reach the button.
What is the root cause of that problem?
In the exposed filter modal / screen component (inside the filter view hierarchy):
KeyboardAvoidingVieworScrollViewwithkeyboardShouldPersistTaps="handled".windowSoftInputModeisadjustResize(oradjustPan), the flex layout does not calculate bottom padding offset for the keyboard within modal sheets, causing the fixed-position bottom button container to be occluded beneath the soft input pane.What changes do you think we should make in order to solve the problem?
ScreenWrapperwithincludeSafeAreaPaddingBottom={true}or ensure the inner form usesKeyboardAvoidingViewwithbehavior={Platform.OS === 'ios' ? 'padding' : undefined}andkeyboardVerticalOffset.ScrollViewwithcontentContainerStylehaving adequate bottom spacing (styles.pb5/styles.mb5) so that scrolling remains active while the keyboard is visible.FixedFooteror placed within the scrollable content container so that on Android devices with compact viewports (such as Moto G60), scrolling upwards brings the button into view above the keyboard.What specific scenarios should we cover in automated tests?
In unit/component tests:
jeffrey-graham commentedon Nov 7, 2026
melvin-bot commentedon Nov 7, 2026
📣 @ronanchnlps-bit! 📣
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:
RajGahoi commentedon Nov 7, 2026
melvin-bot commentedon Nov 7, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
github-actions commentedon Nov 7, 2026