Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtron-labs/devtron-fe-common-lib",
"version": "1.14.1-pre-1",
"version": "1.14.1-pre-2",
"description": "Supporting common component library",
"type": "module",
"main": "dist/index.js",
Expand Down
151 changes: 151 additions & 0 deletions src/Shared/Components/Switch/Switch.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { AriaAttributes, useRef } from 'react'
import { AnimatePresence, motion } from 'framer-motion'

import { Tooltip } from '@Common/Tooltip'
import { ComponentSizeType } from '@Shared/constants'
import { getUniqueId } from '@Shared/Helpers'

import { Icon } from '../Icon'
import { INDETERMINATE_ICON_WIDTH_MAP, LOADING_COLOR_MAP } from './constants'
import { DTSwitchProps } from './types'
import {
getSwitchContainerClass,
getSwitchIconColor,
getSwitchThumbClass,
getSwitchTrackColor,
getSwitchTrackHoverColor,
getThumbPadding,
getThumbPosition,
} from './utils'

import './switch.scss'

const Switch = ({
ariaLabel,
dataTestId,
isDisabled,
isLoading,
isChecked,
tooltipContent,
shape = 'rounded',
variant = 'positive',
iconColor,
iconName,
indeterminate = false,
size = ComponentSizeType.medium,
name,
onChange,
}: DTSwitchProps) => {
const inputId = useRef(getUniqueId())

const getAriaCheckedValue = (): AriaAttributes['aria-checked'] => {
if (!isChecked) {
return false
}

return indeterminate ? 'mixed' : true
}

const ariaCheckedValue = getAriaCheckedValue()

const showIndeterminateIcon = ariaCheckedValue === 'mixed'

const renderContent = () => (
<motion.span
className={`flex flex-grow-1 ${getThumbPadding({ shape, isLoading })} ${getThumbPosition({ isChecked, isLoading })}`}
layout
transition={{ ease: 'easeInOut', duration: 0.2 }}
>
{isLoading ? (
<motion.span
transition={{ ease: 'easeInOut', duration: 0.2 }}
layoutId={`${name}-loader`}
className="flex-grow-1 h-100 dc__fill-available-space dc__no-shrink"
>
<Icon name="ic-circle-loader" color={LOADING_COLOR_MAP[variant]} size={null} />
</motion.span>
) : (
<motion.span
layoutId={`${name}-thumb`}
className={getSwitchThumbClass({ shape, size, showIndeterminateIcon })}
layout
transition={{ ease: 'easeInOut', duration: 0.2 }}
>
<AnimatePresence>
{showIndeterminateIcon ? (
<motion.span
className={`${INDETERMINATE_ICON_WIDTH_MAP[size]} h-2 br-4 dc__no-shrink bg__white`}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
/>
) : (
iconName && (
<motion.span
className="icon-dim-12 flex dc__fill-available-space dc__no-shrink"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
>
<Icon
name={iconName}
color={getSwitchIconColor({
isChecked,
iconColor,
variant,
})}
size={null}
/>
</motion.span>
)
)}
</AnimatePresence>
</motion.span>
)}
</motion.span>
)

return (
<Tooltip alwaysShowTippyOnHover={!!tooltipContent} content={tooltipContent}>
<label
htmlFor={inputId.current}
className={`${getSwitchContainerClass({ shape, size })} flex dc__no-shrink py-2 m-0`}
>
<input
type="checkbox"
id={inputId.current}
name={name}
checked={isChecked}
disabled={isDisabled}
readOnly
hidden
/>

<button
type="button"
role="checkbox"
aria-checked={ariaCheckedValue}
aria-labelledby={inputId.current}
aria-label={isLoading ? 'Loading...' : ariaLabel}
data-testid={dataTestId}
disabled={isDisabled || isLoading}
aria-disabled={isDisabled}
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`}
onClick={onChange}
style={{
// Adding hover styles directly to the button
['--switch-track-hover-color' as string]: getSwitchTrackHoverColor({
shape,
variant,
isChecked,
}),
}}
>
{renderContent()}
</button>
</label>
</Tooltip>
)
}

export default Switch
64 changes: 64 additions & 0 deletions src/Shared/Components/Switch/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ComponentSizeType } from '@Shared/constants'
import { IconBaseColorType } from '@Shared/types'

import { DTSwitchProps } from './types'

export const ROUNDED_SWITCH_SIZE_MAP: Readonly<Record<DTSwitchProps['size'], string>> = {
[ComponentSizeType.medium]: 'w-32',
[ComponentSizeType.small]: 'w-24',
}

export const SQUARE_SWITCH_SIZE_MAP: typeof ROUNDED_SWITCH_SIZE_MAP = {
[ComponentSizeType.medium]: 'w-28',
[ComponentSizeType.small]: 'w-24',
}

export const SWITCH_HEIGHT_MAP: Readonly<Record<DTSwitchProps['size'], string>> = {
[ComponentSizeType.medium]: 'h-24',
[ComponentSizeType.small]: 'h-20',
}

export const LOADING_COLOR_MAP: Record<DTSwitchProps['variant'], IconBaseColorType> = {
theme: 'B500',
positive: 'G500',
}

export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record<DTSwitchProps['variant'], string> = {
theme: 'bcb-5',
positive: 'bcg-5',
}

export const ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP: Record<DTSwitchProps['variant'], `var(--${IconBaseColorType})`> = {
theme: 'var(--B600)',
positive: 'var(--G600)',
}

export const SQUARE_SWITCH_TRACK_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_COLOR_MAP = {
theme: 'bcb-3',
positive: 'bcg-3',
}

export const SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP = {
theme: 'var(--B400)',
positive: 'var(--G400)',
}

export const ROUNDED_SWITCH_THUMB_SIZE_MAP: Record<DTSwitchProps['size'], string> = {
[ComponentSizeType.medium]: 'icon-dim-16',
[ComponentSizeType.small]: 'icon-dim-12',
}

export const INDETERMINATE_ICON_WIDTH_MAP: Record<DTSwitchProps['size'], string> = {
[ComponentSizeType.medium]: 'w-12',
[ComponentSizeType.small]: 'w-10',
}

export const SWITCH_THUMB_PADDING_MAP: Record<DTSwitchProps['size'], string> = {
[ComponentSizeType.medium]: 'p-3',
[ComponentSizeType.small]: 'p-1',
}

export const THUMB_OUTER_PADDING_MAP: Record<DTSwitchProps['shape'], string> = {
rounded: 'p-2',
square: 'p-1',
}
2 changes: 2 additions & 0 deletions src/Shared/Components/Switch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as DTSwitch } from './Switch.component'
export type { DTSwitchProps } from './types'
9 changes: 9 additions & 0 deletions src/Shared/Components/Switch/switch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.dt-switch {
&__track {
--switch-track-hover-color: 'transparent';

&:hover {
background-color: var(--switch-track-hover-color);
}
}
}
122 changes: 122 additions & 0 deletions src/Shared/Components/Switch/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ComponentSizeType } from '@Shared/constants'
import { IconBaseColorType } from '@Shared/types'

import { IconName } from '../Icon'

/**
* Represents the properties for configuring the shape and behavior of a switch component.
*
* - When `shape` is `rounded`:
* - The switch will have a rounded appearance.
* - `iconName`, `iconColor`, and `indeterminate` are not applicable.
*
* - When `shape` is `square`:
* - The switch will have a square appearance.
* - `iconName` specifies the name of the icon to display.
* - `iconColor` allows customization of the icon's color in the active state.
* - `indeterminate` indicates whether the switch is in an indeterminate state, typically used for checkboxes to represent a mixed state.
* If `indeterminate` is true, the switch will not be fully checked or unchecked.
*/
type SwitchShapeProps =
| {
/**
* The shape of the switch. Defaults to `rounded` if not specified.
*/
shape?: 'rounded'

/**
* Icon name is not applicable for the `rounded` shape.
*/
iconName?: never

/**
* Icon color is not applicable for the `rounded` shape.
*/
iconColor?: never
/**
* Indicates whether the switch is in an indeterminate state.
* This state is typically used for checkboxes to indicate a mixed state.
* If true, the switch will not be fully checked or unchecked. Due this state alone we are keeping role as `checkbox` instead of `switch`.
* This property is not applicable for the `square` shape.
* @default false
*/
indeterminate?: boolean
}
| {
/**
* The shape of the switch. Must be `square` to enable icon-related properties.
*/
shape: 'square'

/**
* The name of the icon to display when the shape is `square`.
*/
iconName: IconName

/**
* The color of the icon. If provided, this will override the default color in the active state.
*/
iconColor?: IconBaseColorType
indeterminate?: never
}

/**
* Represents the properties for the `Switch` component.
*/
export type DTSwitchProps = {
/**
* The ARIA label for the switch, used for accessibility purposes.
*/
ariaLabel: string

/**
* Used in forms to identify the switch.
*/
name: string

/**
* A unique identifier for testing purposes.
*/
dataTestId: string

/**
* The visual variant of the switch.
*
* @default `positive`
*/
variant?: 'theme' | 'positive'

/**
* The size of the switch.
* @default `ComponentSizeType.medium`
*/
size?: Extract<ComponentSizeType, ComponentSizeType.medium | ComponentSizeType.small>

/**
* Callback function that is called when the switch state changes.
* This function should handle the logic for toggling the switch.
*/
onChange: () => void
Comment thread
RohitRaj011 marked this conversation as resolved.

/**
* Indicates whether the switch is disabled.
*/
isDisabled?: boolean

/**
* Indicates whether the switch is in a loading state.
*/
isLoading?: boolean

/**
* Indicates whether the switch is currently checked (on).
*/
isChecked: boolean

/**
* Optional tooltip content to display when hovering over the switch.
*
* @default undefined
*/
tooltipContent?: string
} & SwitchShapeProps
Loading