Skip to content

Commit 07f5fb8

Browse files
Merge pull request #736 from devtron-labs/feat/switch
feat: switch
2 parents 0fc09d7 + 4f03159 commit 07f5fb8

File tree

9 files changed

+446
-3
lines changed

9 files changed

+446
-3
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.14.1-pre-1",
3+
"version": "1.14.1-pre-2",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { AriaAttributes, useRef } from 'react'
2+
import { AnimatePresence, motion } from 'framer-motion'
3+
4+
import { Tooltip } from '@Common/Tooltip'
5+
import { ComponentSizeType } from '@Shared/constants'
6+
import { getUniqueId } from '@Shared/Helpers'
7+
8+
import { Icon } from '../Icon'
9+
import { INDETERMINATE_ICON_WIDTH_MAP, LOADING_COLOR_MAP } from './constants'
10+
import { DTSwitchProps } from './types'
11+
import {
12+
getSwitchContainerClass,
13+
getSwitchIconColor,
14+
getSwitchThumbClass,
15+
getSwitchTrackColor,
16+
getSwitchTrackHoverColor,
17+
getThumbPadding,
18+
getThumbPosition,
19+
} from './utils'
20+
21+
import './switch.scss'
22+
23+
const Switch = ({
24+
ariaLabel,
25+
dataTestId,
26+
isDisabled,
27+
isLoading,
28+
isChecked,
29+
tooltipContent,
30+
shape = 'rounded',
31+
variant = 'positive',
32+
iconColor,
33+
iconName,
34+
indeterminate = false,
35+
size = ComponentSizeType.medium,
36+
name,
37+
onChange,
38+
}: DTSwitchProps) => {
39+
const inputId = useRef(getUniqueId())
40+
41+
const getAriaCheckedValue = (): AriaAttributes['aria-checked'] => {
42+
if (!isChecked) {
43+
return false
44+
}
45+
46+
return indeterminate ? 'mixed' : true
47+
}
48+
49+
const ariaCheckedValue = getAriaCheckedValue()
50+
51+
const showIndeterminateIcon = ariaCheckedValue === 'mixed'
52+
53+
const renderContent = () => (
54+
<motion.span
55+
className={`flex flex-grow-1 ${getThumbPadding({ shape, isLoading })} ${getThumbPosition({ isChecked, isLoading })}`}
56+
layout
57+
transition={{ ease: 'easeInOut', duration: 0.2 }}
58+
>
59+
{isLoading ? (
60+
<motion.span
61+
transition={{ ease: 'easeInOut', duration: 0.2 }}
62+
layoutId={`${name}-loader`}
63+
className="flex-grow-1 h-100 dc__fill-available-space dc__no-shrink"
64+
>
65+
<Icon name="ic-circle-loader" color={LOADING_COLOR_MAP[variant]} size={null} />
66+
</motion.span>
67+
) : (
68+
<motion.span
69+
layoutId={`${name}-thumb`}
70+
className={getSwitchThumbClass({ shape, size, showIndeterminateIcon })}
71+
layout
72+
transition={{ ease: 'easeInOut', duration: 0.2 }}
73+
>
74+
<AnimatePresence>
75+
{showIndeterminateIcon ? (
76+
<motion.span
77+
className={`${INDETERMINATE_ICON_WIDTH_MAP[size]} h-2 br-4 dc__no-shrink bg__white`}
78+
initial={{ scale: 0, opacity: 0 }}
79+
animate={{ scale: 1, opacity: 1 }}
80+
exit={{ scale: 0, opacity: 0 }}
81+
/>
82+
) : (
83+
iconName && (
84+
<motion.span
85+
className="icon-dim-12 flex dc__fill-available-space dc__no-shrink"
86+
initial={{ scale: 0.8, opacity: 0 }}
87+
animate={{ scale: 1, opacity: 1 }}
88+
exit={{ scale: 0.8, opacity: 0 }}
89+
>
90+
<Icon
91+
name={iconName}
92+
color={getSwitchIconColor({
93+
isChecked,
94+
iconColor,
95+
variant,
96+
})}
97+
size={null}
98+
/>
99+
</motion.span>
100+
)
101+
)}
102+
</AnimatePresence>
103+
</motion.span>
104+
)}
105+
</motion.span>
106+
)
107+
108+
return (
109+
<Tooltip alwaysShowTippyOnHover={!!tooltipContent} content={tooltipContent}>
110+
<label
111+
htmlFor={inputId.current}
112+
className={`${getSwitchContainerClass({ shape, size })} flex dc__no-shrink py-2 m-0`}
113+
>
114+
<input
115+
type="checkbox"
116+
id={inputId.current}
117+
name={name}
118+
checked={isChecked}
119+
disabled={isDisabled}
120+
readOnly
121+
hidden
122+
/>
123+
124+
<button
125+
type="button"
126+
role="checkbox"
127+
aria-checked={ariaCheckedValue}
128+
aria-labelledby={inputId.current}
129+
aria-label={isLoading ? 'Loading...' : ariaLabel}
130+
data-testid={dataTestId}
131+
disabled={isDisabled || isLoading}
132+
aria-disabled={isDisabled}
133+
className={`p-0-imp h-100 flex flex-grow-1 dc__no-border dt-switch__track ${shape === 'rounded' ? 'br-12' : 'br-4'} ${getSwitchTrackColor({ shape, variant, isChecked, isLoading })} ${isDisabled ? 'dc__disabled' : ''} dc__fill-available-space`}
134+
onClick={onChange}
135+
style={{
136+
// Adding hover styles directly to the button
137+
['--switch-track-hover-color' as string]: getSwitchTrackHoverColor({
138+
shape,
139+
variant,
140+
isChecked,
141+
}),
142+
}}
143+
>
144+
{renderContent()}
145+
</button>
146+
</label>
147+
</Tooltip>
148+
)
149+
}
150+
151+
export default Switch
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ComponentSizeType } from '@Shared/constants'
2+
import { IconBaseColorType } from '@Shared/types'
3+
4+
import { DTSwitchProps } from './types'
5+
6+
export const ROUNDED_SWITCH_SIZE_MAP: Readonly<Record<DTSwitchProps['size'], string>> = {
7+
[ComponentSizeType.medium]: 'w-32',
8+
[ComponentSizeType.small]: 'w-24',
9+
}
10+
11+
export const SQUARE_SWITCH_SIZE_MAP: typeof ROUNDED_SWITCH_SIZE_MAP = {
12+
[ComponentSizeType.medium]: 'w-28',
13+
[ComponentSizeType.small]: 'w-24',
14+
}
15+
16+
export const SWITCH_HEIGHT_MAP: Readonly<Record<DTSwitchProps['size'], string>> = {
17+
[ComponentSizeType.medium]: 'h-24',
18+
[ComponentSizeType.small]: 'h-20',
19+
}
20+
21+
export const LOADING_COLOR_MAP: Record<DTSwitchProps['variant'], IconBaseColorType> = {
22+
theme: 'B500',
23+
positive: 'G500',
24+
}
25+
26+
export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record<DTSwitchProps['variant'], string> = {
27+
theme: 'bcb-5',
28+
positive: 'bcg-5',
29+
}
30+
31+
export const ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP: Record<DTSwitchProps['variant'], `var(--${IconBaseColorType})`> = {
32+
theme: 'var(--B600)',
33+
positive: 'var(--G600)',
34+
}
35+
36+
export const SQUARE_SWITCH_TRACK_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_COLOR_MAP = {
37+
theme: 'bcb-3',
38+
positive: 'bcg-3',
39+
}
40+
41+
export const SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP = {
42+
theme: 'var(--B400)',
43+
positive: 'var(--G400)',
44+
}
45+
46+
export const ROUNDED_SWITCH_THUMB_SIZE_MAP: Record<DTSwitchProps['size'], string> = {
47+
[ComponentSizeType.medium]: 'icon-dim-16',
48+
[ComponentSizeType.small]: 'icon-dim-12',
49+
}
50+
51+
export const INDETERMINATE_ICON_WIDTH_MAP: Record<DTSwitchProps['size'], string> = {
52+
[ComponentSizeType.medium]: 'w-12',
53+
[ComponentSizeType.small]: 'w-10',
54+
}
55+
56+
export const SWITCH_THUMB_PADDING_MAP: Record<DTSwitchProps['size'], string> = {
57+
[ComponentSizeType.medium]: 'p-3',
58+
[ComponentSizeType.small]: 'p-1',
59+
}
60+
61+
export const THUMB_OUTER_PADDING_MAP: Record<DTSwitchProps['shape'], string> = {
62+
rounded: 'p-2',
63+
square: 'p-1',
64+
}

src/Shared/Components/Switch/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as DTSwitch } from './Switch.component'
2+
export type { DTSwitchProps } from './types'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.dt-switch {
2+
&__track {
3+
--switch-track-hover-color: 'transparent';
4+
5+
&:hover {
6+
background-color: var(--switch-track-hover-color);
7+
}
8+
}
9+
}

src/Shared/Components/Switch/types.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { ComponentSizeType } from '@Shared/constants'
2+
import { IconBaseColorType } from '@Shared/types'
3+
4+
import { IconName } from '../Icon'
5+
6+
/**
7+
* Represents the properties for configuring the shape and behavior of a switch component.
8+
*
9+
* - When `shape` is `rounded`:
10+
* - The switch will have a rounded appearance.
11+
* - `iconName`, `iconColor`, and `indeterminate` are not applicable.
12+
*
13+
* - When `shape` is `square`:
14+
* - The switch will have a square appearance.
15+
* - `iconName` specifies the name of the icon to display.
16+
* - `iconColor` allows customization of the icon's color in the active state.
17+
* - `indeterminate` indicates whether the switch is in an indeterminate state, typically used for checkboxes to represent a mixed state.
18+
* If `indeterminate` is true, the switch will not be fully checked or unchecked.
19+
*/
20+
type SwitchShapeProps =
21+
| {
22+
/**
23+
* The shape of the switch. Defaults to `rounded` if not specified.
24+
*/
25+
shape?: 'rounded'
26+
27+
/**
28+
* Icon name is not applicable for the `rounded` shape.
29+
*/
30+
iconName?: never
31+
32+
/**
33+
* Icon color is not applicable for the `rounded` shape.
34+
*/
35+
iconColor?: 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
44+
}
45+
| {
46+
/**
47+
* The shape of the switch. Must be `square` to enable icon-related properties.
48+
*/
49+
shape: 'square'
50+
51+
/**
52+
* The name of the icon to display when the shape is `square`.
53+
*/
54+
iconName: IconName
55+
56+
/**
57+
* The color of the icon. If provided, this will override the default color in the active state.
58+
*/
59+
iconColor?: IconBaseColorType
60+
indeterminate?: never
61+
}
62+
63+
/**
64+
* Represents the properties for the `Switch` component.
65+
*/
66+
export type DTSwitchProps = {
67+
/**
68+
* The ARIA label for the switch, used for accessibility purposes.
69+
*/
70+
ariaLabel: string
71+
72+
/**
73+
* Used in forms to identify the switch.
74+
*/
75+
name: string
76+
77+
/**
78+
* A unique identifier for testing purposes.
79+
*/
80+
dataTestId: string
81+
82+
/**
83+
* The visual variant of the switch.
84+
*
85+
* @default `positive`
86+
*/
87+
variant?: 'theme' | 'positive'
88+
89+
/**
90+
* The size of the switch.
91+
* @default `ComponentSizeType.medium`
92+
*/
93+
size?: Extract<ComponentSizeType, ComponentSizeType.medium | ComponentSizeType.small>
94+
95+
/**
96+
* Callback function that is called when the switch state changes.
97+
* This function should handle the logic for toggling the switch.
98+
*/
99+
onChange: () => void
100+
101+
/**
102+
* Indicates whether the switch is disabled.
103+
*/
104+
isDisabled?: boolean
105+
106+
/**
107+
* Indicates whether the switch is in a loading state.
108+
*/
109+
isLoading?: boolean
110+
111+
/**
112+
* Indicates whether the switch is currently checked (on).
113+
*/
114+
isChecked: boolean
115+
116+
/**
117+
* Optional tooltip content to display when hovering over the switch.
118+
*
119+
* @default undefined
120+
*/
121+
tooltipContent?: string
122+
} & SwitchShapeProps

0 commit comments

Comments
 (0)