Skip to content

Commit 8c8da26

Browse files
authored
Merge pull request #833 from rintumerin-aot/feature/FWF-5209-ui-toggle---switch
Feature/fwf 5209 UI toggle switch
2 parents 51a5270 + 71cb339 commit 8c8da26

File tree

8 files changed

+227
-5
lines changed

8 files changed

+227
-5
lines changed

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-
PromptModal,
62+
Switch,
63+
PromptModal
6364
}: any;
6465
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useState, useRef } from "react";
2+
import { SwitchTickIcon, SwitchCrossIcon } from "../SvgIcons";
3+
import { StyleServices } from "@formsflow/service";
4+
5+
interface SwitchProps {
6+
checked?: boolean;
7+
disabled?: boolean;
8+
withIcon?: boolean;
9+
onChange?: (checked: boolean) => void;
10+
id?: string;
11+
className?: string;
12+
label?: string;
13+
type?: string; // default|primary|binary
14+
}
15+
16+
export const Switch: React.FC<SwitchProps> = ({
17+
checked = false,
18+
disabled = false,
19+
withIcon = false,
20+
onChange,
21+
id,
22+
className = "",
23+
label,
24+
type = "default"
25+
}) => {
26+
const [isChecked, setIsChecked] = useState(checked);
27+
const [isFocused, setIsFocused] = useState(false);
28+
const switchRef = useRef<HTMLButtonElement>(null);
29+
30+
// Use CSS variables for colors
31+
const colorSuccess = StyleServices.getCSSVariable('--ff-success'); // for #00C49A
32+
const colorPrimaryLight = StyleServices.getCSSVariable('--ff-primary-light'); // for #B8ABFF
33+
const colorDanger = StyleServices.getCSSVariable('--ff-danger'); // for #E57373
34+
const colorGrayLight = StyleServices.getCSSVariable('--ff-gray-light'); // for #E5E5E5
35+
36+
const renderIcon = () => {
37+
let fillColor = colorSuccess;
38+
39+
if (isChecked) {
40+
if (type.toLowerCase() === 'primary') fillColor = colorPrimaryLight;
41+
return (
42+
<span className="custom-switch-icon">
43+
<SwitchTickIcon fillColor={fillColor} />
44+
</span>
45+
);
46+
} else {
47+
if (type.toLowerCase() === 'binary') fillColor = colorDanger;
48+
else fillColor = colorGrayLight;
49+
return (
50+
<span className="custom-switch-icon">
51+
<SwitchCrossIcon fillColor={fillColor} />
52+
</span>
53+
);
54+
}
55+
};
56+
57+
const renderClass = () => {
58+
let switchClass = 'custom-switch';
59+
60+
if (isChecked) {
61+
if (type.toLowerCase() === 'primary') switchClass += ' custom-switch-on-primary';
62+
else switchClass += ' custom-switch-on';
63+
} else {
64+
if (type.toLowerCase() === 'binary') {
65+
switchClass += ' custom-switch-off-binary';
66+
} else {
67+
switchClass += ' custom-switch-off';
68+
}
69+
}
70+
71+
if (isFocused) switchClass += ' custom-switch-focused';
72+
if (disabled) switchClass += ' custom-switch-disabled';
73+
74+
return switchClass;
75+
}
76+
77+
const handleToggle = () => {
78+
if (disabled) return;
79+
setIsChecked((prev) => {
80+
const newChecked = !prev;
81+
onChange?.(newChecked);
82+
return newChecked;
83+
});
84+
};
85+
86+
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
87+
if (disabled) return;
88+
if (e.key === " " || e.key === "Spacebar") {
89+
e.preventDefault();
90+
handleToggle();
91+
}
92+
};
93+
94+
const handleFocus = () => setIsFocused(true);
95+
const handleBlur = () => setIsFocused(false);
96+
97+
return (
98+
<div className={`custom-switch-wrapper ${className}`}>
99+
{label && (
100+
<label htmlFor={id} className="custom-switch-label">
101+
{label}
102+
</label>
103+
)}
104+
<button
105+
type="button"
106+
id={id ? `${id}-label` : undefined}
107+
ref={switchRef}
108+
role="switch"
109+
aria-checked={isChecked}
110+
aria-disabled={disabled}
111+
aria-labelledby={label && id ? `${id}-label` : undefined}
112+
aria-label={!label ? label ?? 'Toggle' : undefined}
113+
tabIndex={disabled ? -1 : 0}
114+
className={renderClass()}
115+
onClick={handleToggle}
116+
onKeyDown={handleKeyDown}
117+
onFocus={handleFocus}
118+
onBlur={handleBlur}
119+
disabled={disabled}
120+
>
121+
<span className="custom-switch-slider">
122+
{withIcon && renderIcon()}
123+
</span>
124+
</button>
125+
</div>
126+
);
127+
};

forms-flow-components/src/components/SvgIcons/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ export const TickIcon = ({ color = baseColor, ...props }) => (
554554
strokeWidth="2"
555555
strokeLinecap="round"
556556
strokeLinejoin="round"
557+
{...props}
557558
/>
558559
</svg>
559560
);
@@ -905,6 +906,19 @@ export const CalenderRightIcon = ({ color = baseColor, ...props }) => (
905906
</svg>
906907
);
907908

909+
export const SwitchTickIcon = ({fillColor = baseColor, ...props}) => (
910+
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="9" viewBox="0 0 11 9" fill="none">
911+
<path d="M3.8 8.01667L0 4.21667L0.95 3.26667L3.8 6.11667L9.91667 0L10.8667 0.95L3.8 8.01667Z" fill={fillColor}/>
912+
</svg>
913+
);
914+
915+
export const SwitchCrossIcon = ({fillColor = baseColor, ...props}) => (
916+
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="none">
917+
<path d="M0.933333 9.33333L0 8.4L3.73333 4.66667L0 0.933333L0.933333 0L4.66667 3.73333L8.4 0L9.33333 0.933333L5.6 4.66667L9.33333 8.4L8.4 9.33333L4.66667 5.6L0.933333 9.33333Z"
918+
fill={fillColor}/>
919+
</svg>
920+
);
921+
908922
export const VerticalLineIcon = ({ color = baseColor, ...props }) => (
909923
<svg
910924
xmlns="http://www.w3.org/2000/svg"
@@ -918,7 +932,6 @@ export const VerticalLineIcon = ({ color = baseColor, ...props }) => (
918932
</svg>
919933
);
920934

921-
922935
export const CloseIcon = ({
923936
dataTestId = "close-icon",
924937
color = baseColor,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ export * from "./CustomComponents/SubmissionHistoryWithViewButton";
3939
export * from "./CustomComponents/VariableModal";
4040
export * from "./CustomComponents/FormComponent";
4141
export * from "./CustomComponents/StepperComponent";
42-
export * from "./CustomComponents/CustomButton";
42+
export * from "./CustomComponents/CustomButton";
43+
export * from "./CustomComponents/Switch";
4344
export * from "./CustomComponents/SelectDropdown";
4445
export * from "./CustomComponents/CustomDropdownButton";
4546
export * from "./CustomComponents/PromptModal";
4647
export * from "./CustomComponents/CustomTextInput";
4748
export * from "./CustomComponents/CustomTextArea";
48-
export * from "./CustomComponents/FileUploadArea";
49-
49+
export * from "./CustomComponents/FileUploadArea";

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ declare module "@formsflow/components" {
8585
ReusableResizableTable,
8686
BackToPrevIcon,
8787
StepperComponent,
88+
Switch,
8889
PromptModal
8990
}: any;
9091
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@ declare module "@formsflow/components" {
6262
SubmissionHistoryWithViewButton,
6363
VariableModal,
6464
StepperComponent,
65+
Switch
6566
}: any;
6667
}

forms-flow-theme/scss/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
@import "./v8-scss/search";
4545
@import "./v8-scss/button";
4646
@import "./v8-scss/dateRangePicker";
47+
@import "./v8-scss/switch";
4748
@import "./v8-scss/selectDropdown";
4849
@import "./v8-scss/dropdownButton";
4950
@import "./v8-scss/textInput";
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
.custom-switch-wrapper {
2+
display: inline-flex;
3+
align-items: center;
4+
gap: 0.5rem;
5+
}
6+
.custom-switch-label {
7+
user-select: none;
8+
font-size: 1rem;
9+
}
10+
.custom-switch {
11+
position: relative;
12+
width: 2.75rem;
13+
height: 1.5rem;
14+
background: #ccc;
15+
border-radius: 0.75rem;
16+
cursor: pointer;
17+
padding: 0;
18+
display: inline-flex;
19+
align-items: center;
20+
outline: none;
21+
box-shadow: none;
22+
border: none;
23+
}
24+
.custom-switch-on {
25+
background: #00C49A;
26+
}
27+
.custom-switch-on:hover:not(.custom-switch-disabled) {
28+
box-shadow: 0 0 0 2px #007B61;
29+
}
30+
.custom-switch-on-primary{
31+
background: #B8ABFF;
32+
box-shadow: none;
33+
}
34+
.custom-switch-off {
35+
background: #E5E5E5;
36+
}
37+
.custom-switch-off-binary{
38+
background: #E57373;
39+
}
40+
.custom-switch-on-primary:hover:not(.custom-switch-disabled) {
41+
box-shadow: 0 0 0 1px #7A6FB6;
42+
}
43+
.custom-switch-off:hover:not(.custom-switch-disabled) {
44+
box-shadow: 0 0 0 1px #C5C5C5;
45+
}
46+
.custom-switch-off-binary:hover:not(.custom-switch-disabled) {
47+
box-shadow: 0 0 0 1px #934A4A;
48+
}
49+
.custom-switch-focused:not(.custom-switch-disabled) {
50+
box-shadow: none;
51+
}
52+
.custom-switch-disabled {
53+
cursor: not-allowed;
54+
border: 1px solid #E5E5E5;
55+
background: #E5E5E580;
56+
}
57+
.custom-switch-slider {
58+
position: absolute;
59+
left: 0.125rem;
60+
top: 0.125rem;
61+
width: 1.25rem;
62+
height: 1.25rem;
63+
background: #fff;
64+
border-radius: 50%;
65+
transition: left 0.2s;
66+
box-shadow: 0 0.0625rem 0.1875rem rgba(0,0,0,0.2); /* 0 1px 3px */
67+
display: flex;
68+
align-items: center;
69+
justify-content: center;
70+
}
71+
.custom-switch-on,.custom-switch-on-primary{
72+
.custom-switch-slider {
73+
left: 1.375rem;
74+
}
75+
}
76+
.custom-switch-tick {
77+
display: block;
78+
}

0 commit comments

Comments
 (0)