From ecc0cfe2d3313fcc7cb8d5f314957db8024082a1 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Thu, 12 Mar 2026 23:35:18 +0800 Subject: [PATCH 1/3] Fix: Exports list is not scrollable when export options exceed viewport height --- src/components/MoneyReportHeader.tsx | 6 ++++++ .../MoneyRequestReportActionsList.tsx | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index a220e97371ec..40eb54a4a58f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1932,6 +1932,9 @@ function MoneyReportHeader({ }, [originalSelectedTransactionsOptions, showDeleteModal, dismissedRejectUseExplanation, isDelegateAccessRestricted, showDelegateNoAccessModal]); const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID; + const shouldPopoverUseScrollView = + selectedTransactionsOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || + selectedTransactionsOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); if (isMobileSelectionModeEnabled && shouldUseNarrowLayout) { // If mobile selection mode is enabled but only one or no transactions remain, turn it off @@ -2023,6 +2026,8 @@ function MoneyReportHeader({ })} isSplitButton={false} shouldAlwaysShowDropdownMenu + shouldPopoverUseScrollView={shouldPopoverUseScrollView} + wrapperStyle={styles.w100} /> )} @@ -2040,6 +2045,7 @@ function MoneyReportHeader({ })} isSplitButton={false} shouldAlwaysShowDropdownMenu + shouldPopoverUseScrollView={shouldPopoverUseScrollView} wrapperStyle={styles.w100} /> diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 9d36a095ee32..0f5556c2f31d 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -306,6 +306,10 @@ function MoneyRequestReportActionsList({ }); }, [originalSelectedTransactionsOptions, dismissedRejectUseExplanation, isDelegateAccessRestricted, showDelegateNoAccessModal]); + const shouldPopoverUseScrollView = + selectedTransactionsOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || + selectedTransactionsOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); + const dismissRejectModalBasedOnAction = useCallback(() => { if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK) { dismissRejectUseExplanation(); @@ -800,6 +804,7 @@ function MoneyRequestReportActionsList({ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})} isSplitButton={false} shouldAlwaysShowDropdownMenu + shouldPopoverUseScrollView={shouldPopoverUseScrollView} wrapperStyle={[styles.w100, styles.ph5]} /> From 87eef763324009c81ce000321c9e2a626276fbca Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 13 Mar 2026 22:44:36 +0800 Subject: [PATCH 2/3] extract shouldPopoverUseScrollView into shared utility --- src/components/MoneyReportHeader.tsx | 9 ++++----- .../MoneyRequestReportActionsList.tsx | 7 +++---- src/components/Search/SearchBulkActionsButton.tsx | 8 ++++---- src/components/SettlementButton/index.tsx | 6 +++--- src/libs/shouldPopoverUseScrollView.ts | 8 ++++++++ 5 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 src/libs/shouldPopoverUseScrollView.ts diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 425563dcf1ff..02efd10e0d8b 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -97,6 +97,7 @@ import { rejectMoneyRequestReason, shouldBlockSubmitDueToStrictPolicyRules, } from '@libs/ReportUtils'; +import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import { @@ -1932,9 +1933,7 @@ function MoneyReportHeader({ }, [originalSelectedTransactionsOptions, showDeleteModal, dismissedRejectUseExplanation, isDelegateAccessRestricted, showDelegateNoAccessModal]); const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID; - const shouldPopoverUseScrollView = - selectedTransactionsOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || - selectedTransactionsOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); + const popoverUseScrollView = shouldPopoverUseScrollView(selectedTransactionsOptions); if (isMobileSelectionModeEnabled && shouldUseNarrowLayout) { // If mobile selection mode is enabled but only one or no transactions remain, turn it off @@ -2026,7 +2025,7 @@ function MoneyReportHeader({ })} isSplitButton={false} shouldAlwaysShowDropdownMenu - shouldPopoverUseScrollView={shouldPopoverUseScrollView} + shouldPopoverUseScrollView={popoverUseScrollView} wrapperStyle={styles.w100} /> @@ -2045,7 +2044,7 @@ function MoneyReportHeader({ })} isSplitButton={false} shouldAlwaysShowDropdownMenu - shouldPopoverUseScrollView={shouldPopoverUseScrollView} + shouldPopoverUseScrollView={popoverUseScrollView} wrapperStyle={styles.w100} /> diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 0f5556c2f31d..aa645e24839a 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -59,6 +59,7 @@ import { wasMessageReceivedWhileOffline, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction, chatIncludesChronosWithID, getOriginalReportID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils'; +import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView'; import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {isTransactionPendingDelete} from '@libs/TransactionUtils'; @@ -306,9 +307,7 @@ function MoneyRequestReportActionsList({ }); }, [originalSelectedTransactionsOptions, dismissedRejectUseExplanation, isDelegateAccessRestricted, showDelegateNoAccessModal]); - const shouldPopoverUseScrollView = - selectedTransactionsOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || - selectedTransactionsOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); + const popoverUseScrollView = shouldPopoverUseScrollView(selectedTransactionsOptions); const dismissRejectModalBasedOnAction = useCallback(() => { if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK) { @@ -804,7 +803,7 @@ function MoneyRequestReportActionsList({ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})} isSplitButton={false} shouldAlwaysShowDropdownMenu - shouldPopoverUseScrollView={shouldPopoverUseScrollView} + shouldPopoverUseScrollView={popoverUseScrollView} wrapperStyle={[styles.w100, styles.ph5]} /> diff --git a/src/components/Search/SearchBulkActionsButton.tsx b/src/components/Search/SearchBulkActionsButton.tsx index ce3905b9d49e..f418270be5e6 100644 --- a/src/components/Search/SearchBulkActionsButton.tsx +++ b/src/components/Search/SearchBulkActionsButton.tsx @@ -20,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {handleBulkPayItemSelected} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; import {isExpenseReport} from '@libs/ReportUtils'; +import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -76,8 +77,7 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) { const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); const isExpenseReportType = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; - const shouldPopoverUseScrollView = - headerButtonsOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || headerButtonsOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); + const popoverUseScrollView = shouldPopoverUseScrollView(headerButtonsOptions); const selectedItemsCount = useMemo(() => { if (!selectedTransactions) { @@ -123,7 +123,7 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) { shouldAlwaysShowDropdownMenu isDisabled={headerButtonsOptions.length === 0} onPress={() => null} - shouldPopoverUseScrollView={shouldPopoverUseScrollView} + shouldPopoverUseScrollView={popoverUseScrollView} onSubItemSelected={(subItem) => handleBulkPayItemSelected({ item: subItem, @@ -163,7 +163,7 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) { buttonSize={CONST.DROPDOWN_BUTTON_SIZE.SMALL} customText={selectionButtonText} options={headerButtonsOptions} - shouldPopoverUseScrollView={shouldPopoverUseScrollView} + shouldPopoverUseScrollView={popoverUseScrollView} onSubItemSelected={(subItem) => handleBulkPayItemSelected({ item: subItem, diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 0c01b08c9a0b..42cfa976eac8 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -38,6 +38,7 @@ import { isIOUReport, } from '@libs/ReportUtils'; import {handleUnvalidatedUserNavigation, useSettlementButtonPaymentMethods} from '@libs/SettlementButtonUtils'; +import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {approveMoneyRequest} from '@userActions/IOU'; @@ -577,8 +578,7 @@ function SettlementButton({ const shouldUseSplitButton = hasPreferredPaymentMethod || !!lastPaymentPolicy || ((isExpenseReport || isInvoiceReport) && hasIntentToPay); const shouldLimitWidth = shouldUseShortForm && shouldUseSplitButton && !paymentButtonOptions.length; - const shouldPopoverUseScrollView = - paymentButtonOptions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || paymentButtonOptions.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); + const popoverUseScrollView = shouldPopoverUseScrollView(paymentButtonOptions); return ( 5 ? styles.settlementButtonListContainer : {}} wrapperStyle={[wrapperStyle, shouldLimitWidth ? styles.settlementButtonShortFormWidth : {}]} disabledStyle={disabledStyle} diff --git a/src/libs/shouldPopoverUseScrollView.ts b/src/libs/shouldPopoverUseScrollView.ts new file mode 100644 index 000000000000..ccce7e66ec26 --- /dev/null +++ b/src/libs/shouldPopoverUseScrollView.ts @@ -0,0 +1,8 @@ +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import CONST from '@src/CONST'; + +function shouldPopoverUseScrollView(options: Array>): boolean { + return options.length >= CONST.DROPDOWN_SCROLL_THRESHOLD || options.some((option) => (option.subMenuItems?.length ?? 0) >= CONST.DROPDOWN_SCROLL_THRESHOLD); +} + +export default shouldPopoverUseScrollView; From 95b0b2a60d30d95b6ecfb1a825738cb7c23da3aa Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 13 Mar 2026 22:45:06 +0800 Subject: [PATCH 3/3] add shouldPopoverUseScrollViewTest unit test --- tests/unit/shouldPopoverUseScrollViewTest.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/unit/shouldPopoverUseScrollViewTest.ts diff --git a/tests/unit/shouldPopoverUseScrollViewTest.ts b/tests/unit/shouldPopoverUseScrollViewTest.ts new file mode 100644 index 000000000000..af091a4711a1 --- /dev/null +++ b/tests/unit/shouldPopoverUseScrollViewTest.ts @@ -0,0 +1,39 @@ +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView'; +import CONST from '@src/CONST'; + +describe('shouldPopoverUseScrollView', () => { + const createOption = (value: string, subMenuItems?: Array<{value: string; text: string}>): DropdownOption => ({ + value, + text: value, + ...(subMenuItems && {subMenuItems: subMenuItems.map((item) => ({...item, key: item.value}))}), + }); + + it('returns false when there are few top-level options and no large submenus', () => { + const options = [createOption('a'), createOption('b'), createOption('c')]; + expect(shouldPopoverUseScrollView(options)).toBe(false); + }); + + it('returns true when there are 5 or more top-level options', () => { + const options = Array.from({length: CONST.DROPDOWN_SCROLL_THRESHOLD}, (_, i) => createOption(`option-${i}`)); + expect(shouldPopoverUseScrollView(options)).toBe(true); + }); + + it('returns true when any option has 5 or more submenu items', () => { + const subMenuItems = Array.from({length: CONST.DROPDOWN_SCROLL_THRESHOLD}, (_, i) => ({ + value: `sub-${i}`, + text: `Sub ${i}`, + })); + const options = [createOption('parent', subMenuItems)]; + expect(shouldPopoverUseScrollView(options)).toBe(true); + }); + + it('returns false when submenu has fewer than threshold items', () => { + const subMenuItems = [ + {value: 'sub-1', text: 'Sub 1'}, + {value: 'sub-2', text: 'Sub 2'}, + ]; + const options = [createOption('parent', subMenuItems)]; + expect(shouldPopoverUseScrollView(options)).toBe(false); + }); +});