diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 78e7cf9c8f6a..f2bf02f84937 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5125,6 +5125,7 @@ const CONST = { }, SELECTION_LIST_WITH_MODAL_TEST_ID: 'selectionListWithModalMenuItem', + ICON_TEST_ID: 'Icon', IMAGE_TEST_ID: 'Image', IMAGE_SVG_TEST_ID: 'ImageSVG', VIDEO_PLAYER_TEST_ID: 'VideoPlayer', diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 533e5615cf8e..0792aaa65d34 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -103,6 +103,25 @@ function ButtonWithDropdownMenu({ } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical, popoverHorizontalOffsetType]); + const handleSingleOptionPress = useCallback( + (event: GestureResponderEvent | KeyboardEvent | undefined) => { + const option = options.at(0); + if (!option) { + return; + } + + if (option.onSelected) { + option.onSelected(); + } else { + onOptionSelected?.(option); + onPress(event, option.value); + } + + onSubItemSelected?.(option, 0, event); + }, + [options, onPress, onOptionSelected, onSubItemSelected], + ); + useKeyboardShortcut( CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, (e) => { @@ -115,10 +134,7 @@ function ButtonWithDropdownMenu({ onPress(e, selectedItem.value); } } else { - const option = options.at(0); - if (option?.value) { - onPress(e, option.value); - } + handleSingleOptionPress(e); } }, { @@ -205,10 +221,7 @@ function ButtonWithDropdownMenu({ disabledStyle={disabledStyle} isLoading={isLoading} text={selectedItem?.text} - onPress={(event) => { - const option = options.at(0); - return option ? onPress(event, option.value) : undefined; - }} + onPress={handleSingleOptionPress} large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE} medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL} @@ -219,6 +232,7 @@ function ButtonWithDropdownMenu({ icon={shouldUseOptionIcon && !shouldShowButtonRightIcon ? options.at(0)?.icon : icon} iconRight={shouldShowButtonRightIcon ? options.at(0)?.icon : undefined} shouldShowRightIcon={shouldShowButtonRightIcon} + testID={testID} /> )} {(shouldAlwaysShowDropdownMenu || options.length > 1) && !!popoverAnchorPosition && ( diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 28b4dd07e725..789f4d2052ff 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -134,7 +134,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS {}} - shouldAlwaysShowDropdownMenu + shouldUseOptionIcon customText={translate('common.more')} options={secondaryActions} isSplitButton={false} diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 54c81194052b..892e839058f0 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -337,7 +337,7 @@ function PolicyDistanceRatesPage({ {}} - shouldAlwaysShowDropdownMenu + shouldUseOptionIcon customText={translate('common.more')} options={secondaryActions} isSplitButton={false} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 08eade74100b..82873f286e9a 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -335,7 +335,7 @@ function WorkspaceTaxesPage({ {}} - shouldAlwaysShowDropdownMenu + shouldUseOptionIcon customText={translate('common.more')} options={secondaryActions} isSplitButton={false} diff --git a/tests/ui/components/ButtonWithDropdownMenuTest.tsx b/tests/ui/components/ButtonWithDropdownMenuTest.tsx new file mode 100644 index 000000000000..ab676b694e8e --- /dev/null +++ b/tests/ui/components/ButtonWithDropdownMenuTest.tsx @@ -0,0 +1,68 @@ +import {fireEvent, render, screen} from '@testing-library/react-native'; +import React from 'react'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import * as Expensicons from '@components/Icon/Expensicons'; +import CONST from '@src/CONST'; + +describe('ButtonWithDropdownMenu (single option)', () => { + const mockOnSelected = jest.fn(); + const mockOnOptionSelected = jest.fn(); + const mockOnSubItemSelected = jest.fn(); + const mockOnPress = jest.fn(); + const option = { + value: 'test', + text: 'Test Option', + icon: Expensicons.Checkbox, + onSelected: mockOnSelected, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders a button (not a dropdown) when only one option is present', () => { + render( + , + ); + expect(screen.getByText('Test Option')).toBeTruthy(); + }); + + it('calls all relevant callbacks when the button is pressed', () => { + render( + , + ); + fireEvent.press(screen.getByText('Test Option')); + expect(mockOnSelected).toHaveBeenCalled(); + expect(mockOnOptionSelected).not.toHaveBeenCalled(); // onSelected takes precedence + expect(mockOnSubItemSelected).toHaveBeenCalledWith(expect.objectContaining(option), 0, undefined); + expect(mockOnPress).not.toHaveBeenCalled(); // onPress should not be called when onSelected exists to prevent double execution + }); + + it('renders the icon from the option along with the text', () => { + render( + , + ); + const iconNodes = screen.getAllByTestId(CONST.ICON_TEST_ID); + expect(iconNodes.length).toBeGreaterThan(0); + expect(screen.getByText('Test Option')).toBeTruthy(); + }); +});