From 66bbdb1fcc6778a518b47b7698e0d34c279db316 Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Wed, 20 Nov 2024 19:35:22 +0200 Subject: [PATCH 1/7] Added component and CSS for Switch --- src/components/app-switch/AppSwitch.styles.ts | 71 +++++++++++++++++++ src/components/app-switch/AppSwitch.tsx | 35 +++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/components/app-switch/AppSwitch.styles.ts create mode 100644 src/components/app-switch/AppSwitch.tsx diff --git a/src/components/app-switch/AppSwitch.styles.ts b/src/components/app-switch/AppSwitch.styles.ts new file mode 100644 index 000000000..6ae4b161f --- /dev/null +++ b/src/components/app-switch/AppSwitch.styles.ts @@ -0,0 +1,71 @@ +import { SizeEnum } from '~/types' + +const trackMixin = (width: number, borderWidth: number) => ({ + position: 'absolute', + backgroundColor: '#F7F7F7 !important', + border: `${borderWidth}px solid`, + borderColor: 'primary.200', + borderRadius: `${width / 3}px`, + opacity: 1, + transition: 'background-color 0.3s ease, border-color 0.3s ease', + '&.Mui-disabled': { + color: 'primary.200' + } +}) + +const thumbMixin = (diameter: number) => ({ + width: `${diameter}px`, + height: `${diameter}px`, + boxShadow: 'none', + transition: 'transform 0.3s ease, background-color 0.3s ease' +}) + +const switchBaseMixin = (rWidth: number, tWidth: number) => ({ + position: 'relative', + color: 'primary.400', + padding: 0, + left: `${rWidth / 10}px`, + '&.Mui-checked': { + color: 'primary.800', + left: `${rWidth - tWidth - rWidth / 10 - 20}px` //source forces +20px on transformX (literally 2 days spent to investigate it) https://github.com/mui/material-ui/blob/v6.1.8/packages/mui-material/src/Switch/Switch.js + }, + '&.Mui-disabled': { + color: 'primary.200' + } +}) + +const rootMixin = (width: number, height: number) => ({ + display: 'flex', + alignItems: 'center', + width: `${width}px`, + height: `${height}px`, + overflow: 'visible', + padding: 0, + margin: '5px', + '&:hover': { + '& .MuiSwitch-track': { + borderColor: 'primary.500' + } + } +}) +const getMixins = ( + trackWidth: number, + trackHeight: number, + thumbDiameter: number, + trackBorderWidth: number +): object => { + return { + '&': rootMixin(trackWidth, trackHeight), + '& .MuiSwitch-thumb': thumbMixin(thumbDiameter), + '& .MuiSwitch-track': trackMixin(trackWidth, trackBorderWidth), + '& .MuiSwitch-switchBase': switchBaseMixin( + trackWidth + trackBorderWidth * 2, + thumbDiameter + ) + } +} +export const styles: Record = { + [SizeEnum.Small.toString()]: getMixins(45, 21, 15, 1), + [SizeEnum.Medium.toString()]: getMixins(60, 28, 20, 2), + [SizeEnum.Large.toString()]: getMixins(75, 35, 25, 3) +} diff --git a/src/components/app-switch/AppSwitch.tsx b/src/components/app-switch/AppSwitch.tsx new file mode 100644 index 000000000..0903768b9 --- /dev/null +++ b/src/components/app-switch/AppSwitch.tsx @@ -0,0 +1,35 @@ +import { SizeEnum } from '~/types' +import Switch, { SwitchProps } from '@mui/material/Switch' +import { styles } from './AppSwitch.styles' +import { FormControlLabel } from '@mui/material' +interface AppSwitchProps extends Omit { + labelPosition?: 'start' | 'end' | 'top' | 'bottom' + size?: SizeEnum + label: string + loading?: boolean +} +export const AppSwitch = ({ + labelPosition, + size = SizeEnum.Medium, + label, + loading, + disabled, + ...props +}: AppSwitchProps) => { + const sizeStyle = styles[size] + return ( + + } + label={label} + labelPlacement={labelPosition} + sx={{ + display: 'flex', + alignItems: 'center', + gap: '10px', + overflow: 'visible' + }} + /> + ) +} From ce7810719216c88549774f89facd6d659251eb25 Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Thu, 21 Nov 2024 13:11:18 +0200 Subject: [PATCH 2/7] Added unit tests and added AppSwitch to storybook --- src/components/app-switch/AppSwitch.styles.ts | 10 +- src/components/app-switch/AppSwitch.tsx | 19 ++- src/stories/AppSwitch.stories.jsx | 110 ++++++++++++++++++ .../components/app-switch/AppSwitch.spec.jsx | 94 +++++++++++++++ 4 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 src/stories/AppSwitch.stories.jsx create mode 100644 tests/unit/components/app-switch/AppSwitch.spec.jsx diff --git a/src/components/app-switch/AppSwitch.styles.ts b/src/components/app-switch/AppSwitch.styles.ts index 6ae4b161f..4e0626230 100644 --- a/src/components/app-switch/AppSwitch.styles.ts +++ b/src/components/app-switch/AppSwitch.styles.ts @@ -64,8 +64,16 @@ const getMixins = ( ) } } -export const styles: Record = { +export const switchStyles: Record = { [SizeEnum.Small.toString()]: getMixins(45, 21, 15, 1), [SizeEnum.Medium.toString()]: getMixins(60, 28, 20, 2), [SizeEnum.Large.toString()]: getMixins(75, 35, 25, 3) } +export const formLabelStyles = { + formLabelBox: { + display: 'flex', + alignItems: 'center', + gap: '10px', + overflow: 'visible' + } +} diff --git a/src/components/app-switch/AppSwitch.tsx b/src/components/app-switch/AppSwitch.tsx index 0903768b9..97ffc8b03 100644 --- a/src/components/app-switch/AppSwitch.tsx +++ b/src/components/app-switch/AppSwitch.tsx @@ -1,35 +1,30 @@ import { SizeEnum } from '~/types' import Switch, { SwitchProps } from '@mui/material/Switch' -import { styles } from './AppSwitch.styles' +import { switchStyles, formLabelStyles } from './AppSwitch.styles' import { FormControlLabel } from '@mui/material' interface AppSwitchProps extends Omit { labelPosition?: 'start' | 'end' | 'top' | 'bottom' size?: SizeEnum - label: string + label?: string loading?: boolean } export const AppSwitch = ({ - labelPosition, - size = SizeEnum.Medium, + labelPosition = 'end', + size = SizeEnum.Small, label, loading, disabled, ...props }: AppSwitchProps) => { - const sizeStyle = styles[size] + const sizeStyle = switchStyles[size] return ( } - label={label} + label={label ? label : ''} labelPlacement={labelPosition} - sx={{ - display: 'flex', - alignItems: 'center', - gap: '10px', - overflow: 'visible' - }} + sx={formLabelStyles.formLabelBox} /> ) } diff --git a/src/stories/AppSwitch.stories.jsx b/src/stories/AppSwitch.stories.jsx new file mode 100644 index 000000000..a31e649b6 --- /dev/null +++ b/src/stories/AppSwitch.stories.jsx @@ -0,0 +1,110 @@ +import { AppSwitch } from '~/components/app-switch/AppSwitch' +import { SizeEnum } from '~/types' + +export default { + title: 'Components/AppSwitch', + component: AppSwitch, + argTypes: { + label: { + control: 'text', + description: 'Label for the switch', + defaultValue: '' + }, + labelPosition: { + control: { type: 'radio' }, + options: ['start', 'end', 'top', 'bottom'], + description: 'Position of the label relative to the switch', + defaultValue: 'end' + }, + size: { + control: { type: 'radio' }, + options: [SizeEnum.Small, SizeEnum.Medium, SizeEnum.Large], + description: 'Size of the switch', + defaultValue: SizeEnum.Medium + }, + loading: { + control: 'boolean', + description: 'Disables the switch and displays loading state', + defaultValue: false + }, + disabled: { + control: 'boolean', + description: 'Disables the switch', + defaultValue: false + } + } +} + +const Template = (args) => + +export const Default = Template.bind({}) +Default.args = { + size: SizeEnum.Medium, + disabled: false, + loading: false +} + +export const DefaultWithLabel = Template.bind({}) +DefaultWithLabel.args = { + label: 'Default Switch with Label', + size: SizeEnum.Medium, + labelPosition: 'end', + disabled: false, + loading: false +} + +export const Disabled = Template.bind({}) +Disabled.args = { + label: 'Disabled Switch', + size: SizeEnum.Medium, + labelPosition: 'end', + disabled: true +} + +export const Loading = Template.bind({}) +Loading.args = { + label: 'Loading Switch', + size: SizeEnum.Medium, + labelPosition: 'end', + loading: true +} + +export const LargeSize = Template.bind({}) +LargeSize.args = { + label: 'Large Switch', + size: SizeEnum.Large, + labelPosition: 'end', + disabled: false +} + +export const SmallSize = Template.bind({}) +SmallSize.args = { + label: 'Small Switch', + size: SizeEnum.Small, + labelPosition: 'end', + disabled: false +} + +export const TopPosition = Template.bind({}) +TopPosition.args = { + label: 'Top Position', + size: SizeEnum.Medium, + labelPosition: 'top', + disabled: false +} + +export const StartPosition = Template.bind({}) +StartPosition.args = { + label: 'Start Position', + size: SizeEnum.Medium, + labelPosition: 'start', + disabled: false +} + +export const BottomPosition = Template.bind({}) +BottomPosition.args = { + label: 'Bottom Position', + size: SizeEnum.Medium, + labelPosition: 'bottom', + disabled: false +} diff --git a/tests/unit/components/app-switch/AppSwitch.spec.jsx b/tests/unit/components/app-switch/AppSwitch.spec.jsx new file mode 100644 index 000000000..abe7ac671 --- /dev/null +++ b/tests/unit/components/app-switch/AppSwitch.spec.jsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react' +import { fireEvent } from '@testing-library/react' +import {AppSwitch} from '~/components/app-switch/AppSwitch' +import {SizeEnum} from '~/types' + +describe('AppSwitch Component', () => { + it('renders correctly with default props', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeInTheDocument() + }) + + it('triggers `onChange` when toggled', () => { + const handleChange = vi.fn() + render() + + const switchEl = screen.getByRole('checkbox') + fireEvent.click(switchEl) + + expect(handleChange).toHaveBeenCalledTimes(1) + }) + + it('displays the label when provided', () => { + render() + + const labelEl = screen.getByText('Test Label') + expect(labelEl).toBeInTheDocument() + }) + + it('applies the correct Large size style', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeInTheDocument() + }) + + it('applies the correct Medium size style', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeInTheDocument() + }) + + it('applies the correct Small size style', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeInTheDocument() + }) + + it('is disabled when loading is true', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeDisabled() + }) + + it('is disabled when disabled prop is true', () => { + render() + + const switchEl = screen.getByRole('checkbox') + expect(switchEl).toBeDisabled() + }) + + it('renders the label in the correct end position', () => { + render() + + const labelEl = screen.getByText('End Label') + expect(labelEl).toBeInTheDocument() + }) + + it('renders the label in the correct start position', () => { + render() + + const labelEl = screen.getByText('Start Label') + expect(labelEl).toBeInTheDocument() + }) + + it('renders the label in the correct top position', () => { + render() + + const labelEl = screen.getByText('Top Label') + expect(labelEl).toBeInTheDocument() + }) + + it('renders the label in the correct bottom position', () => { + render() + + const labelEl = screen.getByText('Bottom Label') + expect(labelEl).toBeInTheDocument() + }) + +}) From 12a2fca5d8fe2f1d248e35efb0c4662e140ece81 Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Thu, 21 Nov 2024 13:36:48 +0200 Subject: [PATCH 3/7] SonarFix --- src/components/app-switch/AppSwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/app-switch/AppSwitch.tsx b/src/components/app-switch/AppSwitch.tsx index 97ffc8b03..2e9e9f841 100644 --- a/src/components/app-switch/AppSwitch.tsx +++ b/src/components/app-switch/AppSwitch.tsx @@ -22,7 +22,7 @@ export const AppSwitch = ({ control={ } - label={label ? label : ''} + label={label || ''} labelPlacement={labelPosition} sx={formLabelStyles.formLabelBox} /> From 5283aa37bcbe19ce4ce065b5546f13ce14a777e6 Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Thu, 21 Nov 2024 14:01:25 +0200 Subject: [PATCH 4/7] SonarFix --- src/components/app-switch/AppSwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/app-switch/AppSwitch.tsx b/src/components/app-switch/AppSwitch.tsx index 2e9e9f841..48d7ab23b 100644 --- a/src/components/app-switch/AppSwitch.tsx +++ b/src/components/app-switch/AppSwitch.tsx @@ -22,7 +22,7 @@ export const AppSwitch = ({ control={ } - label={label || ''} + label={label ?? ''} labelPlacement={labelPosition} sx={formLabelStyles.formLabelBox} /> From ed867f231f15a949866d744a305ec75ad53b97c4 Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Thu, 21 Nov 2024 15:42:08 +0200 Subject: [PATCH 5/7] changed default size to medium --- src/components/app-switch/AppSwitch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/app-switch/AppSwitch.tsx b/src/components/app-switch/AppSwitch.tsx index 48d7ab23b..57fbe35bc 100644 --- a/src/components/app-switch/AppSwitch.tsx +++ b/src/components/app-switch/AppSwitch.tsx @@ -10,7 +10,7 @@ interface AppSwitchProps extends Omit { } export const AppSwitch = ({ labelPosition = 'end', - size = SizeEnum.Small, + size = SizeEnum.Medium, label, loading, disabled, From 96ff3f3929c76cbba61e7fa8ce4bfac49f85a87e Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Thu, 21 Nov 2024 22:23:12 +0200 Subject: [PATCH 6/7] fixed opacity for checked --- src/components/app-switch/AppSwitch.styles.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/app-switch/AppSwitch.styles.ts b/src/components/app-switch/AppSwitch.styles.ts index 4e0626230..c644bc942 100644 --- a/src/components/app-switch/AppSwitch.styles.ts +++ b/src/components/app-switch/AppSwitch.styles.ts @@ -1,15 +1,15 @@ import { SizeEnum } from '~/types' - +import palette from '~/styles/app-theme/app.pallete' const trackMixin = (width: number, borderWidth: number) => ({ position: 'absolute', - backgroundColor: '#F7F7F7 !important', + backgroundColor: `${palette.backgroundColor} !important`, border: `${borderWidth}px solid`, borderColor: 'primary.200', borderRadius: `${width / 3}px`, - opacity: 1, + opacity: '1 !important', transition: 'background-color 0.3s ease, border-color 0.3s ease', - '&.Mui-disabled': { - color: 'primary.200' + '&.Mui-checked': { + opacity: '1 !important' } }) @@ -30,7 +30,7 @@ const switchBaseMixin = (rWidth: number, tWidth: number) => ({ left: `${rWidth - tWidth - rWidth / 10 - 20}px` //source forces +20px on transformX (literally 2 days spent to investigate it) https://github.com/mui/material-ui/blob/v6.1.8/packages/mui-material/src/Switch/Switch.js }, '&.Mui-disabled': { - color: 'primary.200' + color: 'primary.100' } }) @@ -42,10 +42,22 @@ const rootMixin = (width: number, height: number) => ({ overflow: 'visible', padding: 0, margin: '5px', - '&:hover': { + '&.Mui-checked': { + opacity: '1 !important' + }, + '&:not(.Mui-disabled):hover': { '& .MuiSwitch-track': { borderColor: 'primary.500' } + }, + '&.Mui-disabled:hover': { + pointerEvents: 'none', + '& .MuiSwitch-track': { + borderColor: 'inherit !important' + } + }, + '& .MuiSwitch-track.Mui-disabled:hover': { + pointerEvents: 'none' } }) const getMixins = ( From 2bb64e48929b5371dd7e982436ea680e837ce7ab Mon Sep 17 00:00:00 2001 From: Zhayvoronok Kateryna <1gayv425@gmail.com> Date: Fri, 22 Nov 2024 13:00:09 +0200 Subject: [PATCH 7/7] fixed opacity problem when checked --- src/components/app-switch/AppSwitch.styles.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/app-switch/AppSwitch.styles.ts b/src/components/app-switch/AppSwitch.styles.ts index c644bc942..0621526fc 100644 --- a/src/components/app-switch/AppSwitch.styles.ts +++ b/src/components/app-switch/AppSwitch.styles.ts @@ -7,10 +7,7 @@ const trackMixin = (width: number, borderWidth: number) => ({ borderColor: 'primary.200', borderRadius: `${width / 3}px`, opacity: '1 !important', - transition: 'background-color 0.3s ease, border-color 0.3s ease', - '&.Mui-checked': { - opacity: '1 !important' - } + transition: 'background-color 0.3s ease, border-color 0.3s ease' }) const thumbMixin = (diameter: number) => ({ @@ -42,22 +39,21 @@ const rootMixin = (width: number, height: number) => ({ overflow: 'visible', padding: 0, margin: '5px', - '&.Mui-checked': { - opacity: '1 !important' - }, - '&:not(.Mui-disabled):hover': { + // for some reason, the normal hover overrides the settings when disabled + '&:hover:not(.Mui-disabled)': { '& .MuiSwitch-track': { borderColor: 'primary.500' } }, - '&.Mui-disabled:hover': { - pointerEvents: 'none', + '&.Mui-disabled': { '& .MuiSwitch-track': { - borderColor: 'inherit !important' + borderColor: 'primary.100 !important' + }, + '&:hover': { + '& .MuiSwitch-track': { + borderColor: 'primary.100 !important' + } } - }, - '& .MuiSwitch-track.Mui-disabled:hover': { - pointerEvents: 'none' } }) const getMixins = (