Skip to content

Commit 51a5270

Browse files
authored
Merge pull request #838 from vinaayakh-aot/feature/storybook-integration
Feature/storybook integration
2 parents 48bf7ac + e2121da commit 51a5270

32 files changed

+3790
-1330
lines changed

forms-flow-components/.storybook/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module.exports = {
2727
alias: {
2828
...(config.resolve && config.resolve.alias ? config.resolve.alias : {}),
2929
bootstrap: bootstrapPath,
30+
'@formsflow/service': resolve(__dirname, '../src/mocks/formsflow-service.js'),
3031
},
3132
extensions: Array.from(new Set([...(config.resolve?.extensions || []), '.ts', '.tsx'])),
3233
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React, { useEffect, useState } from "react";
22
import { AngleRightIcon, AngleLeftIcon, PencilIcon } from "../SvgIcons";
33
import { useTranslation } from "react-i18next";
4-
import { ButtonDropdown, FormInput } from "@formsflow/components";
4+
import { ButtonDropdown } from "./ButtonDropdown";
5+
import { FormInput } from "./FormInput";
56
import { CustomButton } from "./Button";
67
import { CustomInfo } from "./CustomInfo";
78

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import { useTranslation } from "react-i18next";
66
*
77
* Usage:
88
* <CustomButton variant="primary" label="submit" onClick={...} loading={...} icon={<Icon />} />
9+
* <CustomButton variant="secondary" label="cancel" onClick={...} />
910
* <CustomButton icon={<Icon />} iconOnly ariaLabel="Search" />
1011
*/
1112

1213
type ButtonVariant = "primary" | "secondary";
13-
type ButtonSize = "small" | "medium" | "large";
14+
type ButtonSize = "small" | "medium" | "large"; // Size prop not implemented correctly.CSS missing for this.
1415
type ButtonType = "button" | "submit" | "reset";
1516

1617
interface CustomButtonProps extends Omit<React.ComponentPropsWithoutRef<"button">, 'onClick' | 'disabled' | 'type'> {
Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,72 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useCallback, useMemo, forwardRef, memo } from "react";
22
import Dropdown from "react-bootstrap/Dropdown";
33
import ButtonGroup from "react-bootstrap/ButtonGroup";
44
import { ChevronIcon } from "../SvgIcons/index";
55

6-
interface DropdownItemConfig {
6+
/**
7+
* Dropdown item descriptor for `V8CustomDropdownButton`.
8+
*/
9+
export interface DropdownItemConfig {
10+
/** Text label or translation key for the item */
711
label: string;
12+
/** Value associated with this item */
813
value?: string;
14+
/** Called when this item is clicked */
915
onClick?: () => void;
16+
/** Test ID for automated testing */
1017
dataTestId?: string;
18+
/** Accessible label for screen readers */
1119
ariaLabel?: string;
1220
}
1321

14-
interface V8CustomDropdownButtonProps {
22+
/**
23+
* Props for `V8CustomDropdownButton` component.
24+
* Optimized, accessible dropdown button with separate label and dropdown actions.
25+
*/
26+
export interface V8CustomDropdownButtonProps
27+
extends Omit<React.ComponentPropsWithoutRef<"div">, "onClick"> {
28+
/** Button label text */
1529
label?: string;
30+
/** Array of dropdown menu items */
1631
dropdownItems: DropdownItemConfig[];
32+
/** Visual style variant */
1733
variant?: "primary" | "secondary";
34+
/** Disables the entire dropdown button */
1835
disabled?: boolean;
36+
/** Additional CSS classes */
1937
className?: string;
38+
/** Test ID for automated testing */
2039
dataTestId?: string;
40+
/** Accessible label for screen readers */
2141
ariaLabel?: string;
22-
menuPosition?: "left" | "right"; // controls dropdown menu alignment
42+
/** Dropdown menu alignment */
43+
menuPosition?: "left" | "right";
44+
/** Called when the label is clicked (separate from dropdown) */
45+
onLabelClick?: () => void;
2346
}
2447

25-
export const V8CustomDropdownButton: React.FC<V8CustomDropdownButtonProps> = ({
48+
/**
49+
* Utility function to build className string
50+
*/
51+
const buildClassNames = (...classes: (string | boolean | undefined)[]): string => {
52+
return classes.filter(Boolean).join(" ");
53+
};
54+
55+
/**
56+
* V8CustomDropdownButton: Accessible, memoized dropdown button with separate label and dropdown actions.
57+
*
58+
* Usage:
59+
* <V8CustomDropdownButton
60+
* label="Actions"
61+
* dropdownItems={[
62+
* { label: 'Edit', value: 'edit', onClick: handleEdit },
63+
* { label: 'Delete', value: 'delete', onClick: handleDelete }
64+
* ]}
65+
* onLabelClick={handlePrimaryAction}
66+
* variant="primary"
67+
* />
68+
*/
69+
const V8CustomDropdownButtonComponent = forwardRef<HTMLDivElement, V8CustomDropdownButtonProps>(({
2670
label = "Edit",
2771
dropdownItems,
2872
variant = "primary",
@@ -31,65 +75,145 @@ export const V8CustomDropdownButton: React.FC<V8CustomDropdownButtonProps> = ({
3175
dataTestId = "v8-dropdown",
3276
ariaLabel = "Custom dropdown",
3377
menuPosition = "left",
34-
}) => {
78+
onLabelClick,
79+
...restProps
80+
}, ref) => {
81+
// State management
3582
const [open, setOpen] = useState(false);
3683
const [selectedValue, setSelectedValue] = useState<string | null>(null);
3784

38-
const handleItemClick = (item: DropdownItemConfig) => {
85+
// Memoized dropdown items to prevent unnecessary re-renders
86+
const memoizedDropdownItems = useMemo(() => dropdownItems, [dropdownItems]);
87+
88+
// Memoized click handlers for better performance
89+
const handleItemClick = useCallback((item: DropdownItemConfig) => {
3990
setSelectedValue(item.value || item.label);
4091
item.onClick?.();
41-
setOpen(false); // close after selecting
42-
};
92+
setOpen(false); // Close dropdown after selection
93+
}, []);
94+
95+
const handleLabelClick = useCallback((e: React.MouseEvent) => {
96+
e.preventDefault();
97+
e.stopPropagation();
98+
if (!disabled && onLabelClick) {
99+
onLabelClick();
100+
}
101+
}, [disabled, onLabelClick]);
102+
103+
const handleDropdownIconClick = useCallback((e: React.MouseEvent) => {
104+
e.preventDefault();
105+
e.stopPropagation();
106+
if (!disabled) {
107+
setOpen(!open);
108+
}
109+
}, [disabled, open]);
110+
111+
// Memoized dropdown toggle handler
112+
const handleDropdownToggle = useCallback((isOpen: boolean) => {
113+
if (!disabled) {
114+
setOpen(isOpen);
115+
}
116+
}, [disabled]);
117+
118+
// Memoized container className
119+
const containerClassName = useMemo(() => buildClassNames(
120+
"v8-custom-dropdown",
121+
`menu-${menuPosition}`,
122+
className
123+
), [menuPosition, className]);
124+
125+
// Memoized toggle button className
126+
const toggleClassName = useMemo(() => buildClassNames(
127+
"v8-dropdown-toggle",
128+
open && "open"
129+
), [open]);
43130

44131
return (
45132
<Dropdown
46133
as={ButtonGroup}
47134
show={open}
48-
onToggle={(isOpen) => setOpen(isOpen)}
49-
className={`v8-custom-dropdown menu-${menuPosition} ${className}`}
135+
onToggle={handleDropdownToggle}
136+
className={containerClassName}
137+
ref={ref}
50138
{...(dataTestId ? { "data-testid": dataTestId } : {})}
139+
{...restProps}
51140
>
52141
<Dropdown.Toggle
53142
variant={variant}
54143
disabled={disabled}
55-
className={`v8-dropdown-toggle ${open ? "open" : ""}`}
144+
className={toggleClassName}
56145
aria-haspopup="listbox"
57146
aria-expanded={open}
58147
{...(ariaLabel ? { "aria-label": ariaLabel } : {})}
59148
{...(dataTestId ? { "data-testid": `${dataTestId}-toggle` } : {})}
60149
>
61-
<div className="label-div">
150+
{/* Label section - triggers separate action */}
151+
<div
152+
className="label-div"
153+
onClick={handleLabelClick}
154+
data-testid={`${dataTestId}-label`}
155+
role="button"
156+
tabIndex={disabled ? -1 : 0}
157+
aria-label={`${label} action`}
158+
>
62159
<span className="dropdown-label">{label}</span>
63160
</div>
161+
162+
{/* Visual divider */}
64163
<span className="v8-dropdown-divider" aria-hidden="true" />
65-
<div className="dropdown-icon">
164+
165+
{/* Dropdown icon section - toggles menu */}
166+
<div
167+
className="dropdown-icon"
168+
onClick={handleDropdownIconClick}
169+
data-testid={`${dataTestId}-icon`}
170+
role="button"
171+
tabIndex={disabled ? -1 : 0}
172+
aria-label="Toggle dropdown menu"
173+
>
66174
<span className="chevron-icon">
67175
<ChevronIcon />
68176
</span>
69177
</div>
70178
</Dropdown.Toggle>
71179

180+
{/* Dropdown menu */}
72181
<Dropdown.Menu
73182
className="v8-dropdown-menu"
74183
role="listbox"
75184
{...(dataTestId ? { "data-testid": `${dataTestId}-menu` } : {})}
76185
>
77-
{dropdownItems.map((item) => (
78-
<Dropdown.Item
79-
key={item.value || item.label}
80-
onClick={() => handleItemClick(item)}
81-
className={`v8-dropdown-item ${
82-
selectedValue === (item.value || item.label) ? "selected" : ""
83-
}`}
84-
role="option"
85-
aria-selected={selectedValue === (item.value || item.label)}
86-
{...(item.ariaLabel ? { "aria-label": item.ariaLabel } : {})}
87-
{...(item.dataTestId ? { "data-testid": item.dataTestId } : {})}
88-
>
89-
{item.label}
90-
</Dropdown.Item>
91-
))}
186+
{memoizedDropdownItems.map((item, index) => {
187+
const itemKey = item.value || item.label || index;
188+
const isSelected = selectedValue === (item.value || item.label);
189+
190+
return (
191+
<Dropdown.Item
192+
key={itemKey}
193+
onClick={() => handleItemClick(item)}
194+
className={buildClassNames(
195+
"v8-dropdown-item",
196+
isSelected && "selected"
197+
)}
198+
role="option"
199+
aria-selected={isSelected}
200+
{...(item.ariaLabel ? { "aria-label": item.ariaLabel } : {})}
201+
{...(item.dataTestId ? { "data-testid": item.dataTestId } : {})}
202+
>
203+
{item.label}
204+
</Dropdown.Item>
205+
);
206+
})}
92207
</Dropdown.Menu>
93208
</Dropdown>
94209
);
95-
};
210+
});
211+
212+
// Set display name for better debugging
213+
V8CustomDropdownButtonComponent.displayName = "V8CustomDropdownButton";
214+
215+
// Export memoized component for performance optimization
216+
export const V8CustomDropdownButton = memo(V8CustomDropdownButtonComponent);
217+
218+
// Export types for consumers
219+
export type { V8CustomDropdownButtonProps };

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export const CustomTextArea: FC<CustomTextAreaProps> = ({
2929
return (
3030
<div className={containerClass}>
3131
{/* Hidden label for accessibility */}
32-
<label htmlFor={inputId} className="sr-only">
32+
{/* <label htmlFor={inputId} className="sr-only">
3333
{ariaLabel || t(placeholder)}
34-
</label>
34+
</label> */}
3535

3636
<textarea
3737
id={inputId}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export const CustomTextInput: FC<CustomTextInputProps> = ({
2525
return (
2626
<div className="text-input-container">
2727
{/* Hidden label for accessibility */}
28-
<label htmlFor={inputId} className="sr-only">
28+
{/* <label htmlFor={inputId} className="sr-only">
2929
{ariaLabel || t(placeholder)}
30-
</label>
30+
</label> */}
3131

3232
<input
3333
id={inputId}

0 commit comments

Comments
 (0)