Skip to content

Commit 78c9739

Browse files
authored
Feature/fwf-4404 button dropdown added (#522)
* initial commit for buttonDropdown * style updates and test case added * code updations and style changes * sonar issues fixed * div changed to btn * console removed * changes in data test id * test case updated
1 parent 851c608 commit 78c9739

File tree

6 files changed

+298
-4
lines changed

6 files changed

+298
-4
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import { ButtonDropdown } from "../components/CustomComponents/ButtonDropdown";
3+
4+
// Mock the StyleServices and translations
5+
jest.mock("@formsflow/service", () => ({
6+
StyleServices: {
7+
getCSSVariable: jest.fn().mockReturnValue("#000000"),
8+
},
9+
}));
10+
11+
jest.mock("react-i18next", () => ({
12+
useTranslation: () => ({
13+
t: (str: string) => str,
14+
}),
15+
}));
16+
17+
describe("ButtonDropdown Component", () => {
18+
const mockDropdownItems = [
19+
{
20+
content: "Item 1",
21+
onClick: jest.fn(),
22+
type: "type1",
23+
dataTestId: "item1-test",
24+
},
25+
{
26+
content: "Item 2",
27+
onClick: jest.fn(),
28+
type: "type2",
29+
dataTestId: "item2-test",
30+
},
31+
];
32+
33+
const defaultProps = {
34+
variant: "primary",
35+
label: "Test Button",
36+
dropdownType: "DROPDOWN_ONLY",
37+
dropdownItems: mockDropdownItems,
38+
dataTestId: "test-dropdown",
39+
};
40+
41+
it("renders basic dropdown button correctly", () => {
42+
render(<ButtonDropdown {...defaultProps} />);
43+
44+
expect(screen.getByText("Test Button")).toBeInTheDocument();
45+
expect(screen.getByTestId("test-dropdown")).toBeInTheDocument();
46+
});
47+
48+
it("handles dropdown item clicks correctly", () => {
49+
render(<ButtonDropdown {...defaultProps} />);
50+
51+
// Open dropdown using the toggle button with a specific test ID
52+
const toggleButton = screen.getByTestId("test-dropdown-toggle");
53+
fireEvent.click(toggleButton);
54+
55+
// Find and click the dropdown item
56+
const dropdownItem = screen.getByText("Item 1");
57+
fireEvent.click(dropdownItem);
58+
59+
expect(mockDropdownItems[0].onClick).toHaveBeenCalledWith("type1");
60+
});
61+
62+
it("renders extra action icon when dropdownType is DROPDOWN_WITH_EXTRA_ACTION", () => {
63+
const extraActionProps = {
64+
...defaultProps,
65+
dropdownType: "DROPDOWN_WITH_EXTRA_ACTION",
66+
extraActionIcon: <div data-testid="extra-icon">Icon</div>,
67+
extraActionOnClick: jest.fn(),
68+
};
69+
70+
render(<ButtonDropdown {...extraActionProps} />);
71+
72+
const extraIcon = screen.getByTestId("extra-icon");
73+
expect(extraIcon).toBeInTheDocument();
74+
75+
fireEvent.click(extraIcon);
76+
expect(extraActionProps.extraActionOnClick).toHaveBeenCalled();
77+
});
78+
79+
it("applies custom size prop correctly", () => {
80+
render(<ButtonDropdown {...defaultProps} size="sm" />);
81+
82+
const button = screen.getByTestId("test-dropdown");
83+
expect(button).toHaveClass("btn-sm");
84+
});
85+
86+
it("applies custom className correctly", () => {
87+
render(<ButtonDropdown {...defaultProps} className="custom-class" />);
88+
89+
const dropdown = screen
90+
.getByTestId("test-dropdown")
91+
.closest(".custom-btn-width");
92+
expect(dropdown).toHaveClass("custom-class");
93+
});
94+
95+
it("updates menu style on window resize", () => {
96+
render(<ButtonDropdown {...defaultProps} />);
97+
98+
// Trigger window resize
99+
global.dispatchEvent(new Event("resize"));
100+
101+
// Verify the dropdown is still rendered
102+
expect(screen.getByTestId("test-dropdown")).toBeInTheDocument();
103+
});
104+
});

forms-flow-components/src/components/CustomComponents/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const CustomButton: React.FC<CustomButtonProps> = ({
9494
data-testid={dataTestId}
9595
aria-label={ariaLabel}
9696
name={name}
97-
className={classNameForButton}
97+
className={`${classNameForButton} justify-content-start`}
9898
>
9999
{t(label)}
100100
</Button>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { useRef, useEffect, useState } from "react";
2+
import Button from "react-bootstrap/Button";
3+
import ButtonGroup from "react-bootstrap/ButtonGroup";
4+
import Dropdown from "react-bootstrap/Dropdown";
5+
import { ChevronIcon } from "../SvgIcons/index";
6+
import { useTranslation } from "react-i18next";
7+
import { StyleServices } from "@formsflow/service";
8+
9+
interface DropdownItem {
10+
content?: React.ReactNode;
11+
onClick: (type?: string) => void;
12+
type?: string;
13+
dataTestId?: string;
14+
ariaLabel?: string;
15+
}
16+
17+
interface ButtonDropdownProps {
18+
variant: string;
19+
size?: "sm" | "md" | "lg" ;
20+
defaultLabel: string;
21+
label: string;
22+
name?: string,
23+
className?: string;
24+
dropdownType: "DROPDOWN_ONLY" | "DROPDOWN_WITH_EXTRA_ACTION";
25+
dropdownItems?: DropdownItem[];
26+
extraActionIcon?: React.ReactNode;
27+
extraActionOnClick?:() => void;
28+
dataTestId?: string;
29+
ariaLabel?: string;
30+
}
31+
32+
export const ButtonDropdown: React.FC<ButtonDropdownProps> = ({
33+
dropdownType,
34+
variant,
35+
size,
36+
defaultLabel,
37+
label,
38+
dropdownItems = [],
39+
extraActionIcon = false,
40+
extraActionOnClick,
41+
className = "",
42+
dataTestId = "",
43+
ariaLabel = "",
44+
name = "",
45+
}) => {
46+
47+
const buttonRef = useRef<HTMLDivElement>(null);
48+
const toggleRef = useRef<HTMLButtonElement>(null);
49+
const [menuStyle, setMenuStyle] = useState<React.CSSProperties>({});
50+
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
51+
const { t } = useTranslation();
52+
const primaryBtnBgColor = StyleServices.getCSSVariable('--primary-btn-bg-color');
53+
const secondaryBtnBgColor = StyleServices.getCSSVariable('--secondary-btn-bg-color');
54+
// Check if we should use the default label
55+
const isUsingDefaultLabel = !label || label === "";
56+
// Display label if provided, otherwise use defaultLabel
57+
const displayLabel = isUsingDefaultLabel ? defaultLabel : label;
58+
// Define the label style as a separate variable
59+
const labelStyle: React.CSSProperties = isUsingDefaultLabel
60+
? { fontStyle: 'italic' }
61+
: {};
62+
const updateMenuStyle = () => {
63+
if (buttonRef.current && toggleRef.current) {
64+
const buttonWidth = buttonRef.current.getBoundingClientRect().width;
65+
const toggleWidth = toggleRef.current.getBoundingClientRect().width;
66+
const totalWidth = buttonWidth + toggleWidth - 1;
67+
setMenuStyle({
68+
minWidth: `${totalWidth}px`,
69+
border: "2px solid var(--primary-btn-bg-color)",
70+
borderTopLeftRadius: "0",
71+
borderTopRightRadius: "0",
72+
padding: "0",
73+
});
74+
}
75+
};
76+
77+
useEffect(() => {
78+
updateMenuStyle();
79+
window.addEventListener("resize", updateMenuStyle);
80+
return () => window.removeEventListener("resize", updateMenuStyle);
81+
}, []);
82+
83+
const getExtraActionStyles = (variant: string) => {
84+
const backgroundColors: Record<string, string | undefined> = {
85+
primary: primaryBtnBgColor,
86+
secondary: secondaryBtnBgColor,
87+
};
88+
89+
return {
90+
extraActionClass: "extra-action-icon",
91+
backgroundColor: backgroundColors[variant] || undefined,
92+
};
93+
};
94+
95+
const { extraActionClass, backgroundColor } = getExtraActionStyles(variant);
96+
97+
return (
98+
<Dropdown
99+
as={ButtonGroup}
100+
className={`${className} custom-btn-width`}
101+
onToggle={(isOpen) => setDropdownOpen(isOpen)}
102+
data-testid={`${dataTestId}-container`}
103+
>
104+
<div ref={buttonRef} className="label-extra-action">
105+
<Button
106+
variant={variant}
107+
size={size !== "md" ? size : undefined}
108+
data-testid={dataTestId}
109+
aria-label={ariaLabel}
110+
name={name}
111+
className="button-dropdown"
112+
>
113+
<span style={labelStyle}>
114+
{t(displayLabel)}
115+
</span>
116+
</Button>
117+
{dropdownType === "DROPDOWN_WITH_EXTRA_ACTION" && extraActionIcon && (
118+
<Button
119+
onClick={extraActionOnClick}
120+
className={`${extraActionClass} border-0`}
121+
style={{ backgroundColor }}
122+
data-testid={`${dataTestId}-extra-action`}
123+
aria-label={`${t(displayLabel)} extra action`}
124+
>
125+
{extraActionIcon}
126+
</Button>
127+
)}
128+
</div>
129+
<Dropdown.Toggle
130+
ref={toggleRef}
131+
split
132+
variant={variant}
133+
id="dropdown-split-basic"
134+
className={`default-arrow ${dropdownOpen ? "collapsed" : ""} button-dropdown-toggle`}
135+
data-testid={`${dataTestId}-toggle`}
136+
aria-label={`${t(displayLabel)} dropdown toggle`}
137+
>
138+
<ChevronIcon color="white" />
139+
</Dropdown.Toggle>
140+
141+
<Dropdown.Menu style={menuStyle}>
142+
{dropdownItems.map((item) => (
143+
<Dropdown.Item
144+
key={item.type}
145+
onClick={() => item.onClick(item.type)}
146+
data-testid={item.dataTestId}
147+
aria-label={item.ariaLabel}
148+
>
149+
{item.content}
150+
</Dropdown.Item>
151+
))}
152+
</Dropdown.Menu>
153+
</Dropdown>
154+
);
155+
}
156+

forms-flow-components/src/components/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ export * from "./CustomComponents/ShowPremiumIcon";
2424
export * from "./CustomComponents/MultiSelect";
2525
export * from "./CustomComponents/DropdownMultiselect";
2626
export * from "./CustomComponents/FormSubmissionHistoryModal";
27+
export * from "./CustomComponents/ButtonDropdown";
2728
export * from "./CustomComponents/DragandDropSort";
28-
export * from "./CustomComponents/DateFilter";
29+
export * from "./CustomComponents/DateFilter";

forms-flow-theme/scss/_button.scss

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,34 @@ $danger-color: var(--ff-danger);
326326
border-bottom-right-radius: 0 !important;
327327
}
328328
}
329+
.label-extra-action{
330+
display: flex !important;
331+
flex-grow: 1 !important;
332+
}
333+
334+
.button-dropdown {
335+
border-top-left-radius: var(--radius-lg) !important;
336+
border-bottom-left-radius: var(--radius-lg) !important;
337+
padding: var(--spacer-050) var(--spacer-075) !important;
338+
justify-content: start !important;
339+
width: 100%;
340+
}
341+
342+
.button-dropdown-toggle {
343+
padding: var(--spacer-050) var(--spacer-125) var(--spacer-050) var(--spacer-075) !important;
344+
flex-grow: 0 !important;
345+
}
346+
347+
.extra-action-icon {
348+
padding: var(--spacer-050) var(--spacer-075) !important;
349+
display: flex;
350+
cursor: pointer;
351+
opacity: 0.8;
352+
}
353+
.dropdown-item {
354+
padding: var(--spacer-050) var(--spacer-100) !important;
355+
margin: 0.25rem !important;
356+
}
329357
}
330358

331359
.btn-group {
@@ -350,6 +378,10 @@ $danger-color: var(--ff-danger);
350378
width: 15rem !important;
351379
}
352380

381+
.custom-btn-width{
382+
width: 15rem !important;
383+
}
384+
353385
.default-arrow {
354386
&::after {
355387
display: none !important;
@@ -555,3 +587,6 @@ $danger-color: var(--ff-danger);
555587
text-align: inherit;
556588
align-items: center;
557589
}
590+
591+
592+

forms-flow-theme/scss/_variables.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ $white-color: var(--ff-white);
5555
border-radius: var(--radius-sm);
5656
background: $primary-color !important;
5757
color: $white-color !important;
58-
margin-left: 6px !important;
59-
margin-right: 6px !important;
6058
filter: none;
6159
width: auto !important;
6260
}

0 commit comments

Comments
 (0)