diff --git a/src/__testing__/ActionButton.test.tsx b/src/__testing__/ActionButton.test.tsx
new file mode 100644
index 000000000..0a802b0e7
--- /dev/null
+++ b/src/__testing__/ActionButton.test.tsx
@@ -0,0 +1,142 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { ActionButton, Option } from '../custom/ActionButton';
+
+describe('ActionButton Component', () => {
+ const mockOptions: Option[] = [
+ {
+ label: 'Validate',
+ icon: V,
+ onClick: jest.fn()
+ },
+ {
+ label: 'Dry Run',
+ icon: D,
+ onClick: jest.fn()
+ },
+ {
+ label: 'Deploy',
+ icon: Dep,
+ onClick: jest.fn(),
+ disabled: true
+ },
+ {
+ label: 'Hidden Option',
+ icon: H,
+ onClick: jest.fn(),
+ show: false
+ },
+ {
+ label: 'Divider',
+ icon: null,
+ onClick: jest.fn(),
+ isDivider: true
+ },
+ {
+ label: 'Undeploy',
+ icon: U,
+ onClick: jest.fn()
+ }
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders with default label "Action" when no label is passed', () => {
+ render();
+ expect(screen.getByRole('button', { name: /Action/i })).not.toBeNull();
+ });
+
+ it('renders with custom label when provided', () => {
+ render();
+ expect(screen.getByRole('button', { name: /^Actions$/i })).not.toBeNull();
+ });
+
+ it('executes defaultActionClick when primary button is clicked and callback provided', () => {
+ const handleDefaultClick = jest.fn();
+ render(
+
+ );
+
+ const mainButton = screen.getByRole('button', { name: /^Actions$/i });
+ fireEvent.click(mainButton);
+ expect(handleDefaultClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('toggles dropdown menu when primary button is clicked and defaultActionClick is not provided', () => {
+ render();
+
+ expect(screen.queryByRole('menu')).toBeNull();
+ const mainButton = screen.getByRole('button', { name: /^Actions$/i });
+ fireEvent.click(mainButton);
+
+ expect(screen.getByRole('menu')).not.toBeNull();
+ expect(screen.getByText('Validate')).not.toBeNull();
+ });
+
+ it('toggles dropdown menu when the dropdown arrow button is clicked', () => {
+ render();
+
+ expect(screen.queryByRole('menu')).toBeNull();
+
+ const buttons = screen.getAllByRole('button');
+ const dropdownArrowButton = buttons[1];
+
+ // Open menu
+ fireEvent.click(dropdownArrowButton);
+ expect(screen.getByRole('menu')).not.toBeNull();
+ expect(screen.getByText('Validate')).not.toBeNull();
+ expect(screen.getByText('Dry Run')).not.toBeNull();
+
+ // Toggle menu closed
+ fireEvent.click(dropdownArrowButton);
+ expect(screen.queryByRole('menu')).toBeNull();
+ });
+
+ it('calls option onClick handler and closes menu when an option is clicked', () => {
+ render();
+
+ const buttons = screen.getAllByRole('button');
+ const dropdownArrowButton = buttons[1];
+ fireEvent.click(dropdownArrowButton);
+
+ const validateItem = screen.getByText('Validate');
+ fireEvent.click(validateItem);
+
+ expect(mockOptions[0].onClick).toHaveBeenCalledTimes(1);
+ expect(screen.queryByRole('menu')).toBeNull();
+ });
+
+ it('does not invoke onClick for disabled options', () => {
+ render();
+
+ const buttons = screen.getAllByRole('button');
+ fireEvent.click(buttons[1]);
+
+ const deployItem = screen.getByText('Deploy');
+ fireEvent.click(deployItem);
+
+ expect(mockOptions[2].onClick).not.toHaveBeenCalled();
+ });
+
+ it('does not render options with show set to false', () => {
+ render();
+
+ const buttons = screen.getAllByRole('button');
+ fireEvent.click(buttons[1]);
+
+ expect(screen.queryByText('Hidden Option')).toBeNull();
+ });
+
+ it('disables primary button when defaultActionDisabled is true', () => {
+ render();
+
+ const mainButton = screen.getByRole('button', { name: /^Actions$/i });
+ expect(mainButton.hasAttribute('disabled')).toBe(true);
+ });
+});
diff --git a/src/custom/ActionButton/ActionButton.tsx b/src/custom/ActionButton/ActionButton.tsx
index 791f72e97..fc5bd04c4 100644
--- a/src/custom/ActionButton/ActionButton.tsx
+++ b/src/custom/ActionButton/ActionButton.tsx
@@ -10,55 +10,77 @@ import {
Popper
} from '../../base';
import { DropDownIcon } from '../../icons';
+
export interface Option {
icon: React.ReactNode;
label: string;
onClick: (event: React.MouseEvent, index: number) => void;
isDivider?: boolean;
show?: boolean;
+ disabled?: boolean;
}
export interface ActionButtonProps {
- defaultActionClick: () => void;
+ defaultActionClick?: () => void;
defaultActionDisabled?: boolean;
options: Option[];
- label: string;
+ label?: string;
+ placement?: 'bottom-start' | 'bottom' | 'bottom-end' | 'top-start' | 'top' | 'top-end';
}
export default function ActionButton({
defaultActionClick,
defaultActionDisabled = false,
options,
- label
+ label = 'Action',
+ placement = 'bottom-start'
}: ActionButtonProps): JSX.Element {
const [open, setOpen] = React.useState(false);
- const [anchorEl, setAnchorEl] = React.useState(null);
+ const anchorRef = React.useRef(null);
+
const handleMenuItemClick = () => {
setOpen(false);
};
const handleToggle = (event: React.MouseEvent) => {
event.stopPropagation();
- setAnchorEl(event.currentTarget);
setOpen((prevOpen) => !prevOpen);
};
- const handleClose = () => {
- setAnchorEl(null);
+ const handleClose = (event: MouseEvent | TouchEvent) => {
+ if (anchorRef.current && anchorRef.current.contains(event.target as Node)) {
+ return;
+ }
setOpen(false);
};
+ const handleMainClick = (event: React.MouseEvent) => {
+ if (defaultActionClick) {
+ defaultActionClick();
+ } else {
+ handleToggle(event);
+ }
+ };
+
return (
-
@@ -67,8 +89,9 @@ export default function ActionButton({
zIndex: 1
}}
open={open}
- anchorEl={anchorEl}
+ anchorEl={anchorRef.current}
role={undefined}
+ placement={placement}
>
@@ -77,11 +100,15 @@ export default function ActionButton({
.filter((option) => option?.show !== false)
.map((option, index) =>
option.isDivider ? (
-
+
) : (