Skip to content

[$250] Android - Spend - "Apply" button is overlapped by keyboard in exposed filter. #105145

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


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:

  1. Open the Expensify app.
  2. Navigate to "Spend" > "Expenses"
  3. Tap on "FIlters" icon > "Withdrawal ID"
  4. Enter any ID > "Confirm" > "Apply Filters"
  5. Tap on exposed filter.
  6. Tap on text input to open keyboard.
  7. 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.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7297588_1793067821661.Withdrawal.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7297588

Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 27, 2026
  2. applause-bot commented on Oct 27, 2026

    @applause-bot
    Author

    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

  3. MelvinBot commented on Oct 27, 2026

    @MelvinBot
    Contributor

    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 TextFilterPopup inside a bottom-docked FilterPopupButton sheet. Two problems stack up:

    1. 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).
    2. 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.
    • This is not a regression from Add the Withdrawal ID custom column to expense report searches. That PR only changes columns and sorting, and the issue also reproduces in production. Recent commits to FilterPopupButton don't touch keyboard handling.
    • MerchantFilterPopup uses 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

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 27, 2026
  5. j3ffffff commented on Oct 27, 2026

    @j3ffffff
  6. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @brunovjk (External)

  7. 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
  8. melvin-bot commented on Oct 27, 2026

    @melvin-bot
  9. camesenin commented on Oct 27, 2026

    @camesenin
  10. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. apoorv-xs commented on Oct 27, 2026

    @apoorv-xs
  12. huhuli083-creator commented on Oct 27, 2026

    @huhuli083-creator

    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?

    1. The exposed filter popups (TextFilterPopup, etc.) are rendered inside <PopoverWithMeasuredContent> via src/components/Search/FilterDropdowns/FilterPopupButton.tsx (lines 84–99):

      <PopoverWithMeasuredContent
          shouldHandleNavigationBack
          anchorRef={buttonRef}
          anchorPosition={popoverAnchorPosition}
          anchorAlignment={anchorAlignment}
          onClose={closePopup}
          isVisible={isPopupVisible}
          ...
    2. 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:

      statusBarTranslucent = true,
      navigationBarTranslucent = true,

      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>.

    3. 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:
        const keyboardY = screenHeight - keyboard.height.value;
        return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
        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:

    --- a/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx
    +++ b/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx
    @@ -55,7 +55,7 @@ function PopoverWithMeasuredContentBase({
         shouldSwitchPositionIfOverflow = false,
         shouldHandleNavigationBack = false,
    -    statusBarTranslucent = false,
    +    statusBarTranslucent = true,
         avoidKeyboard = true,
         ...props
     }: PopoverWithMeasuredContentProps) {

    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.
  13. 45 remaining items

  14. melvin-bot commented on Nov 6, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  15. github-actions commented on Nov 6, 2026

    @github-actions
    Contributor

    ⚠️ @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).

  16. github-actions commented on Nov 6, 2026

    @github-actions
    Contributor

    ⚠️ @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.

  17. Burapa-J commented on Nov 6, 2026

    @Burapa-J

    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:

    1. 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):

      const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({
          addBottomSafeAreaPadding: isSmallScreenWidth && !isKeyboardActive,
          addOfflineIndicatorBottomSafeAreaPadding: false,
      });

      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.

    2. 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.

    3. 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?

    1. 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:
        <KeyboardAvoidingView
            behavior="padding"
            keyboardVerticalOffset={getPlatform() === CONST.PLATFORM.ANDROID ? -insets.top : 0}
            pointerEvents="box-none"
            style={[style, {margin: 0}]}
        >
      • 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.
    2. 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:

      <ScrollView
          style={[styles.pv4, style]}
          keyboardShouldPersistTaps="handled"
          showsVerticalScrollIndicator={false}
      >
          ...
      </ScrollView>

    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>.
  18. jeffrey-graham commented on Nov 6, 2026

    @jeffrey-graham
  19. melvin-bot commented on Nov 6, 2026

    @melvin-bot

    📣 @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:

    1. Make sure you've read and understood the contributing guidelines.
    2. 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.
    3. 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.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  20. bonnhatnguyen commented on Nov 7, 2026

    @bonnhatnguyen

    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?

    1. 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}).
      • react-native-keyboard-controller's KeyboardAvoidingView calculates its bottom padding as:
        Math.max(frame.value.y + frame.value.height - (screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset), 0)
      • 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.
    2. 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:

    1. Determine whether the filter sheet should pad itself for the keyboard on narrow non-tablet screens:
      const isTabletDevice = isTablet();
      const shouldPadForKeyboard = isSmallScreenWidth && !isTabletDevice;
    2. Replace useKeyboardState + useBottomSafeSafeAreaPaddingStyle with useSafeAreaPaddings(true) and useReanimatedKeyboardAnimation() from react-native-keyboard-controller:
      const {paddingBottom: safeAreaPaddingBottom} = useSafeAreaPaddings(true);
      const {height: keyboardOffset} = useReanimatedKeyboardAnimation();
      const keyboardRoomStyle = useAnimatedStyle(() => ({
          paddingBottom: Math.max(safeAreaPaddingBottom, -keyboardOffset.get()),
      }));
    3. 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.
  21. yuvrajangadsingh commented on Nov 7, 2026

    @yuvrajangadsingh
    Contributor

    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 ReanimatedModal wraps the whole sheet in keyboard-controller's padding KeyboardAvoidingView.

    <PopoverWithMeasuredContent
    anchorRef={triggerRef}
    avoidKeyboard
    isVisible={isOverlayVisible}
    onClose={toggleOverlay}
    onModalShow={onModalShow}
    anchorPosition={popoverTriggerPosition}
    anchorAlignment={popoverAnchorAlignment}
    restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
    shouldEnableNewFocusManagement
    shouldMeasureAnchorPositionFromTop={false}
    outerStyle={{...StyleUtils.getOuterModalStyle(windowHeight, viewportOffsetTop), ...containerStyles}}
    // This must be false because we dont want the modal to close if we open the RHP for selections
    // such as date years
    shouldCloseWhenBrowserNavigationChanged={false}
    innerContainerStyle={{...containerStyles, ...styles.p0}}
    popoverDimensions={{
    width: actualPopoverWidth,
    height: CONST.POPOVER_DROPDOWN_MIN_HEIGHT,
    }}
    shouldSkipRemeasurement
    shouldDisplayBelowModals
    shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode={false}
    enableEdgeToEdgeBottomSafeAreaPadding
    >
    <View style={[containerStyles, bottomSafeAreaPaddingStyle]}>{popoverContent}</View>

    if (isSmallScreenWidth) {
    return (
    <Modal
    {...props}
    type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED}
    animationIn="slideInUp"
    animationOut="slideOutDown"
    shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode={shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode}
    // The anchor is the launcher fallback for triggers that blur themselves before opening.
    launcherRef={props.anchorRef}
    />
    );

    {avoidKeyboard ? (
    <KeyboardAvoidingView
    behavior="padding"
    pointerEvents="box-none"
    style={[style, {margin: 0}]}
    >
    {isVisibleState && containerView}
    </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.

    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.

    <KeyboardProvider
    statusBarTranslucent
    navigationBarTranslucent
    >

    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() 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.

    @@ -392,7 +400,18 @@ public class ReactModalHostView(context: ThemedReactContext) :
    // Navigation bar cannot be translucent without status bar being translucent too
    if (navigationBarTranslucent) {
    - dialogWindow.enableEdgeToEdge()
    + // Do not derive the bar icons from the UI mode here: it would override the activity's
    + // icons, and updateProperties() re-runs on every prop update while the dialog is open
    + // without going through show(), which is why updateSystemAppearance() is called again
    + // at the end of this method.
    + dialogWindow.enableEdgeToEdge(applyNavigationBarIconAppearanceFromUiMode = false)

    @@ -343,6 +344,13 @@ public class ReactModalHostView(context: ThemedReactContext) :
    if (currentActivity?.isFinishing == false) {
    newDialog.show()
    updateSystemAppearance()
    + // The activity and dialog insets can settle after attach, so sync the bar icons again.
    + window.decorView.post {
    + // The modal can dismiss or recreate its dialog before this runs.
    + if (dialog === newDialog) {
    + updateSystemAppearance()
    + }
    + }
    window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
    (context as ThemedReactContext).onExtraWindowCreate(window)

    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 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.

    # Use this property to enable edge-to-edge display support.
    # This allows your app to draw behind system bars for an immersive UI.
    # Note: Only works with ReactActivity and should not be used with custom Activity.
    edgeToEdgeEnabled=true

    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.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.

    <View style={[styles.pv4, style]}>
    {onBackButtonPress ? (
    <HeaderWithBackButton
    shouldDisplayHelpButton={false}
    style={[styles.h10, styles.pv1, styles.mb2]}
    subtitle={label}
    onBackButtonPress={onBackButtonPress}
    />
    ) : (
    shouldDisplayLabel && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1, styles.mb2]}>{label}</Text>
    )}
    {children}
    {shouldShowActionButtons && (
    <ActionButtons
    containerStyle={[styles.flexRow, styles.gap2, styles.ph5, styles.mt2]}
    onReset={onReset}
    onApply={onApply}
    applySentryLabel={applySentryLabel}
    resetSentryLabel={resetSentryLabel}
    shouldSubmitOnEnter={shouldSubmitOnEnter}
    />
    )}
    </View>

    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.

    avoidKeyboard={isSearchEnabled}

    // A list with a search input makes room for the keyboard inside the sheet, frame by frame with it, in both orientations:
    // flipping `avoidKeyboard` on rotation would swap the modal's wrapper and remount the sheet, replaying its entry animation
    // and dropping the input's focus. On mobile web the keyboard shrinks the window instead, which the sheet's height follows.
    const shouldPadForKeyboard = isSmallScreenWidth && isSearchable && !isTabletDevice;

    avoidKeyboard: isSmallScreenWidth && !shouldPadForKeyboard,

    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 as ThemedReactContext).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()
          }

    @@ -343,6 +344,13 @@ public class ReactModalHostView(context: ThemedReactContext) :
    if (currentActivity?.isFinishing == false) {
    newDialog.show()
    updateSystemAppearance()
    + // The activity and dialog insets can settle after attach, so sync the bar icons again.
    + window.decorView.post {
    + // The modal can dismiss or recreate its dialog before this runs.
    + if (dialog === newDialog) {
    + updateSystemAppearance()
    + }
    + }
    window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
    (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.

    const {isKeyboardActive} = useKeyboardState();
    // The style is applied inside the popover, which never renders an offline indicator, so don't reserve space for one
    const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({
    addBottomSafeAreaPadding: isSmallScreenWidth && !isKeyboardActive,
    addOfflineIndicatorBottomSafeAreaPadding: false,
    });

    {!keyboardStateContextValue?.isKeyboardActive && <NavigationBar />}

    • 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.

    ### [react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch](react-native+0.86.0+019+fix-modal-transparent-navigation-bar.patch)
    - 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.
    - Upstream PR/issue: 🛑
    - E/App issue: [#69005](https://github.com/Expensify/App/issues/69005), [#103778](https://github.com/Expensify/App/issues/103778)
    - PR introducing patch: [#69004](https://github.com/Expensify/App/pull/69004) (introduced), [#104689](https://github.com/Expensify/App/pull/104689) (`APP-M66` guard)

    • 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.

    // The popover avoids the keyboard by padding its bottom, so while the keyboard is open only the room above it is available.
    const {maxHeight: popoverMaxHeight} = styles.getPopoverMaxHeight(windowHeight, isInLandscapeMode);
    const maxHeight = isKeyboardActive ? Math.min(popoverMaxHeight, windowHeight - keyboardActiveHeight - safeAreaTop - BOTTOM_SHEET_TOP_PADDING) : popoverMaxHeight;

    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.

    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

    • 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.
  22. RajGahoi commented on Nov 7, 2026

    @RajGahoi

    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):

    1. The container wrapping the exposed filter input and action button is not wrapped in a properly configured KeyboardAvoidingView or ScrollView with keyboardShouldPersistTaps="handled".
    2. 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?

    1. 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.
    2. 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:

    1. Verify that when the withdrawal ID input receives focus and keyboard is toggled active, the action button remains within viewport scroll bounds.
    2. Verify that on Android platforms, typing in the exposed filter input does not hide or clip the submit action.
  23. jeffrey-graham commented on Nov 7, 2026

    @jeffrey-graham
  24. melvin-bot commented on Nov 7, 2026

    @melvin-bot

    📣 @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:

    1. Make sure you've read and understood the contributing guidelines.
    2. 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.
    3. 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.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  25. RajGahoi commented on Nov 7, 2026

    @RajGahoi
  26. melvin-bot commented on Nov 7, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  27. github-actions commented on Nov 7, 2026

    @github-actions
    Contributor

    ⚠️ @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.

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

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions