Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
30 changes: 22 additions & 8 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ function ButtonWithDropdownMenu<IValueType>({
}
}, [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) => {
Expand All @@ -115,10 +134,7 @@ function ButtonWithDropdownMenu<IValueType>({
onPress(e, selectedItem.value);
}
} else {
const option = options.at(0);
if (option?.value) {
onPress(e, option.value);
}
handleSingleOptionPress(e);
}
},
{
Expand Down Expand Up @@ -205,10 +221,7 @@ function ButtonWithDropdownMenu<IValueType>({
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}
Expand All @@ -219,6 +232,7 @@ function ButtonWithDropdownMenu<IValueType>({
icon={shouldUseOptionIcon && !shouldShowButtonRightIcon ? options.at(0)?.icon : icon}
iconRight={shouldShowButtonRightIcon ? options.at(0)?.icon : undefined}
shouldShowRightIcon={shouldShowButtonRightIcon}
testID={testID}
/>
)}
{(shouldAlwaysShowDropdownMenu || options.length > 1) && !!popoverAnchorPosition && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
shouldAlwaysShowDropdownMenu
shouldUseOptionIcon
customText={translate('common.more')}
options={secondaryActions}
isSplitButton={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ function PolicyDistanceRatesPage({
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
shouldAlwaysShowDropdownMenu
shouldUseOptionIcon
customText={translate('common.more')}
options={secondaryActions}
isSplitButton={false}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ function WorkspaceTaxesPage({
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
shouldAlwaysShowDropdownMenu
shouldUseOptionIcon
customText={translate('common.more')}
options={secondaryActions}
isSplitButton={false}
Expand Down
68 changes: 68 additions & 0 deletions tests/ui/components/ButtonWithDropdownMenuTest.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ButtonWithDropdownMenu
options={[option]}
onPress={mockOnPress}
onOptionSelected={mockOnOptionSelected}
onSubItemSelected={mockOnSubItemSelected}
shouldUseOptionIcon
/>,
);
expect(screen.getByText('Test Option')).toBeTruthy();
});

it('calls all relevant callbacks when the button is pressed', () => {
render(
<ButtonWithDropdownMenu
options={[option]}
onPress={mockOnPress}
onOptionSelected={mockOnOptionSelected}
onSubItemSelected={mockOnSubItemSelected}
shouldUseOptionIcon
/>,
);
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(
<ButtonWithDropdownMenu
options={[option]}
onPress={mockOnPress}
onOptionSelected={mockOnOptionSelected}
onSubItemSelected={mockOnSubItemSelected}
shouldUseOptionIcon
testID={CONST.ICON_TEST_ID}
/>,
);
const iconNodes = screen.getAllByTestId(CONST.ICON_TEST_ID);
expect(iconNodes.length).toBeGreaterThan(0);
expect(screen.getByText('Test Option')).toBeTruthy();
});
});
Loading