Skip to content

Commit 71cb339

Browse files
authored
Merge branch 'v8-develop' into feature/FWF-5209-ui-toggle---switch
2 parents 9ba6594 + 51a5270 commit 71cb339

File tree

67 files changed

+6167
-1327
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+6167
-1327
lines changed

forms-flow-admin/src/components/users/users.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,10 @@ const Users = React.memo((props: any) => {
378378
className="overflow-hidden">
379379
<Modal.Header>
380380
<Modal.Title></Modal.Title>
381-
</Modal.Header>
381+
<div className="icon-close" onClick={closeSuccessModal} data-testid="user-add-success-close">
382+
<CloseIcon dataTestId="action-success-modal-close" />
383+
</div>
384+
</Modal.Header>
382385
<Modal.Body className="modal-md d-flex align-items-center justify-content-center">
383386
<div className="p-3 text-center">
384387
<div className="d-flex flex-column align-items-center">

forms-flow-admin/src/declarations.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ declare module "@formsflow/components" {
5959
DeleteIcon,
6060
ConfirmModal,
6161
CustomInfo,
62-
Switch
62+
Switch,
63+
PromptModal
6364
}: any;
6465
}

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: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import React, { useState, useCallback, useMemo, forwardRef, memo } from "react";
2+
import Dropdown from "react-bootstrap/Dropdown";
3+
import ButtonGroup from "react-bootstrap/ButtonGroup";
4+
import { ChevronIcon } from "../SvgIcons/index";
5+
6+
/**
7+
* Dropdown item descriptor for `V8CustomDropdownButton`.
8+
*/
9+
export interface DropdownItemConfig {
10+
/** Text label or translation key for the item */
11+
label: string;
12+
/** Value associated with this item */
13+
value?: string;
14+
/** Called when this item is clicked */
15+
onClick?: () => void;
16+
/** Test ID for automated testing */
17+
dataTestId?: string;
18+
/** Accessible label for screen readers */
19+
ariaLabel?: string;
20+
}
21+
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 */
29+
label?: string;
30+
/** Array of dropdown menu items */
31+
dropdownItems: DropdownItemConfig[];
32+
/** Visual style variant */
33+
variant?: "primary" | "secondary";
34+
/** Disables the entire dropdown button */
35+
disabled?: boolean;
36+
/** Additional CSS classes */
37+
className?: string;
38+
/** Test ID for automated testing */
39+
dataTestId?: string;
40+
/** Accessible label for screen readers */
41+
ariaLabel?: string;
42+
/** Dropdown menu alignment */
43+
menuPosition?: "left" | "right";
44+
/** Called when the label is clicked (separate from dropdown) */
45+
onLabelClick?: () => void;
46+
}
47+
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>(({
70+
label = "Edit",
71+
dropdownItems,
72+
variant = "primary",
73+
disabled = false,
74+
className = "",
75+
dataTestId = "v8-dropdown",
76+
ariaLabel = "Custom dropdown",
77+
menuPosition = "left",
78+
onLabelClick,
79+
...restProps
80+
}, ref) => {
81+
// State management
82+
const [open, setOpen] = useState(false);
83+
const [selectedValue, setSelectedValue] = useState<string | null>(null);
84+
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) => {
90+
setSelectedValue(item.value || item.label);
91+
item.onClick?.();
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]);
130+
131+
return (
132+
<Dropdown
133+
as={ButtonGroup}
134+
show={open}
135+
onToggle={handleDropdownToggle}
136+
className={containerClassName}
137+
ref={ref}
138+
{...(dataTestId ? { "data-testid": dataTestId } : {})}
139+
{...restProps}
140+
>
141+
<Dropdown.Toggle
142+
variant={variant}
143+
disabled={disabled}
144+
className={toggleClassName}
145+
aria-haspopup="listbox"
146+
aria-expanded={open}
147+
{...(ariaLabel ? { "aria-label": ariaLabel } : {})}
148+
{...(dataTestId ? { "data-testid": `${dataTestId}-toggle` } : {})}
149+
>
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+
>
159+
<span className="dropdown-label">{label}</span>
160+
</div>
161+
162+
{/* Visual divider */}
163+
<span className="v8-dropdown-divider" aria-hidden="true" />
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+
>
174+
<span className="chevron-icon">
175+
<ChevronIcon />
176+
</span>
177+
</div>
178+
</Dropdown.Toggle>
179+
180+
{/* Dropdown menu */}
181+
<Dropdown.Menu
182+
className="v8-dropdown-menu"
183+
role="listbox"
184+
{...(dataTestId ? { "data-testid": `${dataTestId}-menu` } : {})}
185+
>
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+
})}
207+
</Dropdown.Menu>
208+
</Dropdown>
209+
);
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 };
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { FC } from "react";
2+
import { useTranslation } from "react-i18next";
3+
4+
interface CustomTextAreaProps {
5+
value: string;
6+
setValue: (value: string) => void;
7+
placeholder?: string;
8+
dataTestId: string;
9+
disabled?: boolean;
10+
ariaLabel?: string;
11+
rows?: number;
12+
maxLength?: number;
13+
}
14+
15+
export const CustomTextArea: FC<CustomTextAreaProps> = ({
16+
value,
17+
setValue,
18+
placeholder = "Enter text",
19+
dataTestId,
20+
disabled = false,
21+
ariaLabel,
22+
rows = 4,
23+
maxLength,
24+
}) => {
25+
const { t } = useTranslation();
26+
const inputId = `${dataTestId}-textarea`; // unique id per instance
27+
const containerClass = `text-area-container${disabled ? " text-area-disabled" : ""}`;
28+
29+
return (
30+
<div className={containerClass}>
31+
{/* Hidden label for accessibility */}
32+
{/* <label htmlFor={inputId} className="sr-only">
33+
{ariaLabel || t(placeholder)}
34+
</label> */}
35+
36+
<textarea
37+
id={inputId}
38+
name={dataTestId} // ensures no warnings + supports form submit
39+
className="text-area"
40+
onChange={(e) => setValue(e.target.value)}
41+
placeholder={t(placeholder)}
42+
data-testid={dataTestId}
43+
aria-label={ariaLabel || placeholder}
44+
value={value}
45+
role="textbox"
46+
draggable={false}
47+
disabled={disabled}
48+
rows={rows}
49+
{...(maxLength ? { maxLength } : {})}
50+
/>
51+
</div>
52+
);
53+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { FC } from "react";
2+
import { useTranslation } from "react-i18next";
3+
4+
interface CustomTextInputProps {
5+
value: string;
6+
setValue: (value: string) => void;
7+
placeholder?: string;
8+
dataTestId: string;
9+
disabled?: boolean;
10+
ariaLabel?: string;
11+
}
12+
13+
export const CustomTextInput: FC<CustomTextInputProps> = ({
14+
value,
15+
setValue,
16+
placeholder = "Enter text",
17+
dataTestId,
18+
disabled = false,
19+
ariaLabel,
20+
}) => {
21+
console.log(value);
22+
const { t } = useTranslation();
23+
const inputId = `${dataTestId}-input`; // unique id
24+
25+
return (
26+
<div className="text-input-container">
27+
{/* Hidden label for accessibility */}
28+
{/* <label htmlFor={inputId} className="sr-only">
29+
{ariaLabel || t(placeholder)}
30+
</label> */}
31+
32+
<input
33+
id={inputId}
34+
className="text-input"
35+
type="text"
36+
onChange={(e) => setValue(e.target.value)}
37+
placeholder={t(placeholder)}
38+
data-testid={dataTestId}
39+
aria-label={ariaLabel || t(placeholder)}
40+
value={value}
41+
disabled={disabled}
42+
/>
43+
</div>
44+
);
45+
};

0 commit comments

Comments
 (0)