diff --git a/package-lock.json b/package-lock.json index 78dc92871..9438c49e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.1-pre-1", + "version": "1.14.1-pre-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.1-pre-1", + "version": "1.14.1-pre-2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 870b5ae5f..a1816526f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Shared/Components/Switch/Switch.component.tsx b/src/Shared/Components/Switch/Switch.component.tsx new file mode 100644 index 000000000..b006d743c --- /dev/null +++ b/src/Shared/Components/Switch/Switch.component.tsx @@ -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 = () => ( + + {isLoading ? ( + + + + ) : ( + + + {showIndeterminateIcon ? ( + + ) : ( + iconName && ( + + + + ) + )} + + + )} + + ) + + return ( + + + + ) +} + +export default Switch diff --git a/src/Shared/Components/Switch/constants.ts b/src/Shared/Components/Switch/constants.ts new file mode 100644 index 000000000..6d1c97cbc --- /dev/null +++ b/src/Shared/Components/Switch/constants.ts @@ -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> = { + [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> = { + [ComponentSizeType.medium]: 'h-24', + [ComponentSizeType.small]: 'h-20', +} + +export const LOADING_COLOR_MAP: Record = { + theme: 'B500', + positive: 'G500', +} + +export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record = { + theme: 'bcb-5', + positive: 'bcg-5', +} + +export const ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP: Record = { + 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 = { + [ComponentSizeType.medium]: 'icon-dim-16', + [ComponentSizeType.small]: 'icon-dim-12', +} + +export const INDETERMINATE_ICON_WIDTH_MAP: Record = { + [ComponentSizeType.medium]: 'w-12', + [ComponentSizeType.small]: 'w-10', +} + +export const SWITCH_THUMB_PADDING_MAP: Record = { + [ComponentSizeType.medium]: 'p-3', + [ComponentSizeType.small]: 'p-1', +} + +export const THUMB_OUTER_PADDING_MAP: Record = { + rounded: 'p-2', + square: 'p-1', +} diff --git a/src/Shared/Components/Switch/index.ts b/src/Shared/Components/Switch/index.ts new file mode 100644 index 000000000..a5ff57e39 --- /dev/null +++ b/src/Shared/Components/Switch/index.ts @@ -0,0 +1,2 @@ +export { default as DTSwitch } from './Switch.component' +export type { DTSwitchProps } from './types' diff --git a/src/Shared/Components/Switch/switch.scss b/src/Shared/Components/Switch/switch.scss new file mode 100644 index 000000000..522b011c0 --- /dev/null +++ b/src/Shared/Components/Switch/switch.scss @@ -0,0 +1,9 @@ +.dt-switch { + &__track { + --switch-track-hover-color: 'transparent'; + + &:hover { + background-color: var(--switch-track-hover-color); + } + } +} \ No newline at end of file diff --git a/src/Shared/Components/Switch/types.ts b/src/Shared/Components/Switch/types.ts new file mode 100644 index 000000000..2af63d037 --- /dev/null +++ b/src/Shared/Components/Switch/types.ts @@ -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 + + /** + * Callback function that is called when the switch state changes. + * This function should handle the logic for toggling the switch. + */ + onChange: () => void + + /** + * 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 diff --git a/src/Shared/Components/Switch/utils.ts b/src/Shared/Components/Switch/utils.ts new file mode 100644 index 000000000..2dd6cf73a --- /dev/null +++ b/src/Shared/Components/Switch/utils.ts @@ -0,0 +1,94 @@ +import { IconBaseColorType } from '@Shared/types' + +import { + ROUNDED_SWITCH_SIZE_MAP, + ROUNDED_SWITCH_THUMB_SIZE_MAP, + ROUNDED_SWITCH_TRACK_COLOR_MAP, + ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP, + SQUARE_SWITCH_SIZE_MAP, + SQUARE_SWITCH_TRACK_COLOR_MAP, + SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP, + SWITCH_HEIGHT_MAP, + SWITCH_THUMB_PADDING_MAP, + THUMB_OUTER_PADDING_MAP, +} from './constants' +import { DTSwitchProps } from './types' + +export const getSwitchContainerClass = ({ shape, size }: Required>): string => + `${SWITCH_HEIGHT_MAP[size]} ${shape === 'rounded' ? ROUNDED_SWITCH_SIZE_MAP[size] : SQUARE_SWITCH_SIZE_MAP[size]}` + +export const getSwitchTrackColor = ({ + shape, + variant, + isChecked, + isLoading, +}: Required>): string => { + if (isLoading) { + return 'dc__transparent--unstyled' + } + + if (!isChecked) { + return 'bcn-2' + } + + return shape === 'rounded' ? ROUNDED_SWITCH_TRACK_COLOR_MAP[variant] : SQUARE_SWITCH_TRACK_COLOR_MAP[variant] +} + +export const getSwitchTrackHoverColor = ({ + shape, + variant, + isChecked, +}: Required< + Pick +>): (typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP)[DTSwitchProps['variant']] => { + if (!isChecked) { + return 'var(--N300)' + } + + return shape === 'rounded' + ? ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP[variant] + : SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP[variant] +} + +export const getSwitchThumbClass = ({ + shape, + size, + showIndeterminateIcon, +}: Pick & { showIndeterminateIcon: boolean }) => { + if (showIndeterminateIcon) { + return 'w-100 h-100 flex' + } + + return `flex ${SWITCH_THUMB_PADDING_MAP[size]} ${shape === 'rounded' ? `dc__border-radius-50-per ${ROUNDED_SWITCH_THUMB_SIZE_MAP[size]}` : 'br-3'} bg__white` +} + +export const getSwitchIconColor = ({ + iconColor, + isChecked, + variant, +}: Pick): IconBaseColorType => { + if (!isChecked) { + return 'N200' + } + + return iconColor || (variant === 'theme' ? 'B500' : 'G500') +} + +export const getThumbPosition = ({ + isLoading, + isChecked, +}: Pick): 'left' | 'right' | 'center' => { + if (isLoading) { + return 'center' + } + + return isChecked ? 'right' : 'left' +} + +export const getThumbPadding = ({ shape, isLoading }: Pick): string => { + if (isLoading) { + return '' + } + + return THUMB_OUTER_PADDING_MAP[shape] +} diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 12e39877c..c3affe06a 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -87,6 +87,7 @@ export * from './SelectPicker' export * from './ShowMoreText' export * from './SSOProviderIcon' export * from './StatusComponent' +export * from './Switch' export * from './TabGroup' export * from './Table' export * from './TagsKeyValueTable'