Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added reusable Switch Component according to design systems #2834

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/components/app-switch/AppSwitch.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { SizeEnum } from '~/types'
import palette from '~/styles/app-theme/app.pallete'
const trackMixin = (width: number, borderWidth: number) => ({
position: 'absolute',
backgroundColor: `${palette.backgroundColor} !important`,
border: `${borderWidth}px solid`,
borderColor: 'primary.200',
borderRadius: `${width / 3}px`,
opacity: '1 !important',
transition: 'background-color 0.3s ease, border-color 0.3s ease'
})

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.100'
}
})

const rootMixin = (width: number, height: number) => ({
display: 'flex',
alignItems: 'center',
width: `${width}px`,
height: `${height}px`,
overflow: 'visible',
padding: 0,
margin: '5px',
// for some reason, the normal hover overrides the settings when disabled
'&:hover:not(.Mui-disabled)': {
'& .MuiSwitch-track': {
borderColor: 'primary.500'
}
},
'&.Mui-disabled': {
'& .MuiSwitch-track': {
borderColor: 'primary.100 !important'
},
'&:hover': {
'& .MuiSwitch-track': {
borderColor: 'primary.100 !important'
}
}
}
})
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 switchStyles: Record<string, object> = {
[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'
}
}
30 changes: 30 additions & 0 deletions src/components/app-switch/AppSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { SizeEnum } from '~/types'
import Switch, { SwitchProps } from '@mui/material/Switch'
import { switchStyles, formLabelStyles } from './AppSwitch.styles'
import { FormControlLabel } from '@mui/material'
interface AppSwitchProps extends Omit<SwitchProps, 'size'> {
labelPosition?: 'start' | 'end' | 'top' | 'bottom'
size?: SizeEnum
label?: string
loading?: boolean
}
export const AppSwitch = ({
labelPosition = 'end',
size = SizeEnum.Medium,
label,
loading,
disabled,
...props
}: AppSwitchProps) => {
const sizeStyle = switchStyles[size]
return (
<FormControlLabel
control={
<Switch disabled={loading || disabled} sx={sizeStyle} {...props} />
}
label={label ?? ''}
labelPlacement={labelPosition}
sx={formLabelStyles.formLabelBox}
/>
)
}
110 changes: 110 additions & 0 deletions src/stories/AppSwitch.stories.jsx
Original file line number Diff line number Diff line change
@@ -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) => <AppSwitch {...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
}
94 changes: 94 additions & 0 deletions tests/unit/components/app-switch/AppSwitch.spec.jsx
Original file line number Diff line number Diff line change
@@ -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(<AppSwitch />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeInTheDocument()
})

it('triggers `onChange` when toggled', () => {
const handleChange = vi.fn()
render(<AppSwitch onChange={handleChange} />)

const switchEl = screen.getByRole('checkbox')
fireEvent.click(switchEl)

expect(handleChange).toHaveBeenCalledTimes(1)
})

it('displays the label when provided', () => {
render(<AppSwitch label="Test Label" />)

const labelEl = screen.getByText('Test Label')
expect(labelEl).toBeInTheDocument()
})

it('applies the correct Large size style', () => {
render(<AppSwitch size={SizeEnum.Large} />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeInTheDocument()
})

it('applies the correct Medium size style', () => {
render(<AppSwitch size={SizeEnum.Medium} />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeInTheDocument()
})

it('applies the correct Small size style', () => {
render(<AppSwitch size={SizeEnum.Small} />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeInTheDocument()
})

it('is disabled when loading is true', () => {
render(<AppSwitch loading />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeDisabled()
})

it('is disabled when disabled prop is true', () => {
render(<AppSwitch disabled />)

const switchEl = screen.getByRole('checkbox')
expect(switchEl).toBeDisabled()
})

it('renders the label in the correct end position', () => {
render(<AppSwitch label="End Label" labelPosition="end" />)

const labelEl = screen.getByText('End Label')
expect(labelEl).toBeInTheDocument()
})

it('renders the label in the correct start position', () => {
render(<AppSwitch label="Start Label" labelPosition="start" />)

const labelEl = screen.getByText('Start Label')
expect(labelEl).toBeInTheDocument()
})

it('renders the label in the correct top position', () => {
render(<AppSwitch label="Top Label" labelPosition="top" />)

const labelEl = screen.getByText('Top Label')
expect(labelEl).toBeInTheDocument()
})

it('renders the label in the correct bottom position', () => {
render(<AppSwitch label="Bottom Label" labelPosition="bottom" />)

const labelEl = screen.getByText('Bottom Label')
expect(labelEl).toBeInTheDocument()
})

})
Loading