Skip to content

Commit 0228618

Browse files
committed
feat: enhance Switch component with size and loading state support
1 parent 6bbe654 commit 0228618

File tree

4 files changed

+99
-52
lines changed

4 files changed

+99
-52
lines changed

src/Shared/Components/Switch/Switch.component.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { AriaAttributes, HTMLAttributes } from 'react'
22
import { AnimatePresence, motion } from 'framer-motion'
33

44
import { Tooltip } from '@Common/Tooltip'
5+
import { ComponentSizeType } from '@Shared/constants'
56

67
import { Icon } from '../Icon'
8+
import { INDETERMINATE_ICON_WIDTH_MAP, LOADING_COLOR_MAP, SQUARE_ICON_DIMENSION_MAP } from './constants'
79
import { SwitchProps } from './types'
810
import { getSwitchContainerClass, getSwitchIconColor, getSwitchThumbClass, getSwitchTrackColor } from './utils'
911

@@ -19,45 +21,46 @@ const Switch = ({
1921
iconColor,
2022
iconName,
2123
indeterminate = false,
22-
handleChange,
24+
size = ComponentSizeType.medium,
25+
onChange,
2326
}: SwitchProps) => {
24-
const getAriaChecked = (): AriaAttributes['aria-checked'] => {
27+
const getAriaCheckedValue = (): AriaAttributes['aria-checked'] => {
2528
if (!isChecked) {
2629
return false
2730
}
2831

2932
return indeterminate ? 'mixed' : true
3033
}
3134

32-
const ariaChecked = getAriaChecked()
35+
const ariaCheckedValue = getAriaCheckedValue()
3336

34-
const showIndeterminateIcon = ariaChecked === 'mixed'
37+
const showIndeterminateIcon = ariaCheckedValue === 'mixed'
3538
const role: HTMLAttributes<HTMLButtonElement>['role'] = showIndeterminateIcon ? 'checkbox' : 'switch'
3639

3740
const renderContent = () => {
3841
if (isLoading) {
39-
return <Icon name="ic-circle-loader" color={null} />
42+
return <Icon name="ic-circle-loader" color={LOADING_COLOR_MAP[variant]} />
4043
}
4144

4245
return (
4346
<motion.span
44-
className={`p-1 flex flex-grow-1 ${shape === 'rounded' ? 'br-12' : 'br-4'} ${isChecked ? 'right' : 'left'}`}
47+
className={`flex flex-grow-1 ${shape === 'rounded' ? 'p-2 br-12' : 'p-1 br-4'} ${isChecked ? 'right' : 'left'}`}
4548
layout
4649
animate={{
4750
backgroundColor: getSwitchTrackColor({ shape, variant, isChecked }),
4851
}}
4952
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
5053
>
5154
<motion.span
52-
className={getSwitchThumbClass({ indeterminate, shape, isChecked })}
55+
className={getSwitchThumbClass({ shape, size, showIndeterminateIcon })}
5356
layout
5457
transition={{ type: 'spring', stiffness: 500, damping: 35 }}
5558
>
5659
<AnimatePresence>
5760
{showIndeterminateIcon ? (
5861
<motion.span
5962
key="dash"
60-
className="w-8 h-2 br-4 dc__no-shrink bg__white"
63+
className={`${INDETERMINATE_ICON_WIDTH_MAP[size]} h-2 br-4 dc__no-shrink bg__white`}
6164
initial={{ scale: 0, opacity: 0 }}
6265
animate={{ scale: 1, opacity: 1 }}
6366
exit={{ scale: 0, opacity: 0 }}
@@ -66,7 +69,7 @@ const Switch = ({
6669
iconName && (
6770
<motion.span
6871
key="icon"
69-
className="flex icon-dim-12 dc__fill-available-space dc__no-shrink"
72+
className={`${SQUARE_ICON_DIMENSION_MAP[size]} flex dc__fill-available-space dc__no-shrink`}
7073
initial={{ scale: 0.8, opacity: 0 }}
7174
animate={{ scale: 1, opacity: 1 }}
7275
exit={{ scale: 0.8, opacity: 0 }}
@@ -78,7 +81,6 @@ const Switch = ({
7881
iconColor,
7982
variant,
8083
})}
81-
size={12}
8284
/>
8385
</motion.span>
8486
)
@@ -92,16 +94,16 @@ const Switch = ({
9294
// TODO: Can add hidden input for accessibility in case name [for forms] is given
9395
return (
9496
<Tooltip alwaysShowTippyOnHover={!!tooltipContent} content={tooltipContent}>
95-
<div className={`${getSwitchContainerClass({ shape })} flex dc__no-shrink`}>
97+
<div className={`${getSwitchContainerClass({ shape, size })} flex dc__no-shrink py-2`}>
9698
<button
9799
type="button"
98100
role={role}
99-
aria-checked={ariaChecked}
101+
aria-checked={ariaCheckedValue}
100102
aria-label={isLoading ? 'Loading...' : ariaLabel}
101103
data-testid={dataTestId}
102104
disabled={isDisabled || isLoading}
103105
className={`p-0-imp h-100 flex flex-grow-1 dc__transparent ${isDisabled ? 'dc__disabled' : ''} dc__fill-available-space`}
104-
onClick={handleChange}
106+
onClick={onChange}
105107
>
106108
{renderContent()}
107109
</button>
Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
1+
import { ComponentSizeType } from '@Shared/constants'
2+
import { IconBaseColorType } from '@Shared/types'
3+
14
import { SwitchProps } from './types'
25

3-
export const SWITCH_VARIANTS: Record<SwitchProps['variant'], null> = {
6+
export const SWITCH_VARIANTS: Readonly<Record<SwitchProps['variant'], null>> = {
47
theme: null,
58
positive: null,
69
}
710

8-
export const SWITCH_SHAPES: Record<SwitchProps['shape'], null> = {
11+
export const SWITCH_SHAPES: Readonly<Record<SwitchProps['shape'], null>> = {
912
rounded: null,
1013
square: null,
1114
}
15+
16+
export const ROUNDED_SWITCH_SIZE_MAP: Readonly<Record<SwitchProps['size'], string>> = {
17+
[ComponentSizeType.medium]: 'w-32',
18+
[ComponentSizeType.small]: 'w-24',
19+
}
20+
21+
export const SQUARE_SWITCH_SIZE_MAP: typeof ROUNDED_SWITCH_SIZE_MAP = {
22+
[ComponentSizeType.medium]: 'w-28',
23+
[ComponentSizeType.small]: 'w-24',
24+
}
25+
26+
export const SWITCH_HEIGHT_MAP: Readonly<Record<SwitchProps['size'], string>> = {
27+
[ComponentSizeType.medium]: 'h-24',
28+
[ComponentSizeType.small]: 'h-20',
29+
}
30+
31+
export const LOADING_COLOR_MAP: Record<SwitchProps['variant'], IconBaseColorType> = {
32+
theme: 'B500',
33+
positive: 'G500',
34+
}
35+
36+
export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record<SwitchProps['variant'], `var(--${IconBaseColorType})`> = {
37+
theme: 'var(--B500)',
38+
positive: 'var(--G500)',
39+
}
40+
41+
export const SQUARE_SWITCH_TRACK_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_COLOR_MAP = {
42+
theme: 'var(--B300)',
43+
positive: 'var(--G300)',
44+
}
45+
46+
export const ROUNDED_SWITCH_THUMB_SIZE_MAP: Record<SwitchProps['size'], string> = {
47+
[ComponentSizeType.medium]: 'icon-dim-16',
48+
[ComponentSizeType.small]: 'icon-dim-12',
49+
}
50+
51+
export const INDETERMINATE_ICON_WIDTH_MAP: Record<SwitchProps['size'], string> = {
52+
[ComponentSizeType.medium]: 'w-12',
53+
[ComponentSizeType.small]: 'w-10',
54+
}
55+
56+
export const SQUARE_ICON_DIMENSION_MAP: Record<SwitchProps['size'], string> = {
57+
[ComponentSizeType.medium]: 'icon-dim-12',
58+
[ComponentSizeType.small]: 'icon-dim-8',
59+
}

src/Shared/Components/Switch/types.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ComponentSizeType } from '@Shared/constants'
12
import { IconBaseColorType } from '@Shared/types'
23

34
import { IconName } from '../Icon'
@@ -14,7 +15,7 @@ import { IconName } from '../Icon'
1415
* - `iconName` specifies the name of the icon to display.
1516
* - `iconColor` allows customization of the icon's color in the active state.
1617
* - `indeterminate` indicates whether the switch is in an indeterminate state, typically used for checkboxes to represent a mixed state.
17-
* If `indeterminate` is true, the switch will not be fully checked or unchecked, and its role will change to `checkbox`.
18+
* If `indeterminate` is true, the switch will not be fully checked or unchecked.
1819
*/
1920
type SwitchShapeProps =
2021
| {
@@ -32,7 +33,14 @@ type SwitchShapeProps =
3233
* Icon color is not applicable for the `rounded` shape.
3334
*/
3435
iconColor?: never
35-
indeterminate?: never
36+
/**
37+
* Indicates whether the switch is in an indeterminate state.
38+
* This state is typically used for checkboxes to indicate a mixed state.
39+
* If true, the switch will not be fully checked or unchecked. Due this state alone we are keeping role as `checkbox` instead of `switch`.
40+
* This property is not applicable for the `square` shape.
41+
* @default false
42+
*/
43+
indeterminate?: boolean
3644
}
3745
| {
3846
/**
@@ -49,14 +57,7 @@ type SwitchShapeProps =
4957
* The color of the icon. If provided, this will override the default color in the active state.
5058
*/
5159
iconColor?: IconBaseColorType
52-
/**
53-
* Indicates whether the switch is in an indeterminate state.
54-
* This state is typically used for checkboxes to indicate a mixed state.
55-
* If true, the switch will not be fully checked or unchecked. We will change the role to checkbox in this case since the indeterminate state is not applicable for the switch.
56-
* This property is not applicable for the `rounded` shape.
57-
* @default false
58-
*/
59-
indeterminate?: boolean
60+
indeterminate?: never
6061
}
6162

6263
/**
@@ -80,7 +81,13 @@ export type SwitchProps = {
8081
*/
8182
variant?: 'theme' | 'positive'
8283

83-
handleChange: () => void
84+
/**
85+
* The size of the switch.
86+
* @default `ComponentSizeType.medium`
87+
*/
88+
size?: Extract<ComponentSizeType, ComponentSizeType.medium | ComponentSizeType.small>
89+
90+
onChange: () => void
8491

8592
/**
8693
* Indicates whether the switch is disabled.

src/Shared/Components/Switch/utils.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { IconBaseColorType } from '@Shared/types'
22

3+
import {
4+
ROUNDED_SWITCH_SIZE_MAP,
5+
ROUNDED_SWITCH_THUMB_SIZE_MAP,
6+
ROUNDED_SWITCH_TRACK_COLOR_MAP,
7+
SQUARE_SWITCH_SIZE_MAP,
8+
SQUARE_SWITCH_TRACK_COLOR_MAP,
9+
SWITCH_HEIGHT_MAP,
10+
} from './constants'
311
import { SwitchProps } from './types'
412

5-
// On intro of size there will changes in almost all methods
6-
export const getSwitchContainerClass = ({ shape }: Required<Pick<SwitchProps, 'shape'>>): string => {
7-
if (shape === 'rounded') {
8-
return 'py-3 h-24 w-20'
9-
}
10-
11-
return 'w-28 h-18'
12-
}
13+
export const getSwitchContainerClass = ({ shape, size }: Required<Pick<SwitchProps, 'shape' | 'size'>>): string =>
14+
`${SWITCH_HEIGHT_MAP[size]} ${shape === 'rounded' ? ROUNDED_SWITCH_SIZE_MAP[size] : SQUARE_SWITCH_SIZE_MAP[size]}`
1315

1416
export const getSwitchTrackColor = ({
1517
shape,
@@ -20,31 +22,19 @@ export const getSwitchTrackColor = ({
2022
return 'var(--N200)'
2123
}
2224

23-
if (shape === 'rounded') {
24-
if (variant === 'theme') {
25-
return 'var(--B500)'
26-
}
27-
28-
return 'var(--G500)'
29-
}
30-
31-
if (variant === 'theme') {
32-
return 'var(--B300)'
33-
}
34-
35-
return 'var(--G300)'
25+
return shape === 'rounded' ? ROUNDED_SWITCH_TRACK_COLOR_MAP[variant] : SQUARE_SWITCH_TRACK_COLOR_MAP[variant]
3626
}
3727

3828
export const getSwitchThumbClass = ({
39-
indeterminate,
4029
shape,
41-
isChecked,
42-
}: Pick<SwitchProps, 'indeterminate' | 'shape' | 'isChecked'>) => {
43-
if (isChecked && indeterminate) {
30+
size,
31+
showIndeterminateIcon,
32+
}: Pick<SwitchProps, 'shape' | 'size'> & { showIndeterminateIcon: boolean }) => {
33+
if (showIndeterminateIcon) {
4434
return 'w-100 h-100 flex'
4535
}
4636

47-
return `flex p-2 ${shape === 'rounded' ? 'dc__border-radius-50-per icon-dim-10' : 'br-3'} bg__white`
37+
return `flex p-3 ${shape === 'rounded' ? `dc__border-radius-50-per ${ROUNDED_SWITCH_THUMB_SIZE_MAP[size]}` : 'br-3'} bg__white`
4838
}
4939

5040
export const getSwitchIconColor = ({

0 commit comments

Comments
 (0)