Skip to content

Commit

Permalink
Create CheckBox component for design system (#2841)
Browse files Browse the repository at this point in the history
* add checkbox

* fixed tests

* add generic to useState

* review comments

* add cn function call

* remove s2s from CheckBoxProps
  • Loading branch information
nebby2105 authored Nov 28, 2024
1 parent 3827bf0 commit 2c4be0b
Show file tree
Hide file tree
Showing 4 changed files with 396 additions and 0 deletions.
91 changes: 91 additions & 0 deletions src/design-system/components/checkbox/CheckBox.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@use '~scss/utilities' as *;

.#{$prefix}checkbox {
display: flex;
align-items: center;
margin: var(--#{$prefix}space-0-25) var(--#{$prefix}space-1);

& > .#{$prefix}checkbox__input {
padding: var(--#{$prefix}space-0-5);
color: inherit;

&:hover {
background-color: var(--#{$prefix}blue-gray-50);
}
}

&--top {
flex-direction: column-reverse;
align-items: flex-start;

& > span {
padding-left: var(--#{$prefix}space-0-5);
}
}

&--bottom {
flex-direction: column;
align-items: flex-start;

& > span {
padding-left: var(--#{$prefix}space-0-5);
}
}

&--end {
flex-direction: row;
align-items: center;
}

&--sm {
font-size: var(--#{$prefix}font-size-sm);

& > span > svg {
width: var(--#{$prefix}line-height-sm);
height: var(--#{$prefix}line-height-sm);
}
}

&--md {
font-size: var(--#{$prefix}font-size-md);

& > span > svg {
width: var(--#{$prefix}line-height-md);
height: var(--#{$prefix}line-height-md);
}
}

&--lg {
font-size: var(--#{$prefix}font-size-lg);

& > span > svg {
width: var(--#{$prefix}line-height-lg);
height: var(--#{$prefix}line-height-lg);
}
}

&--error {
color: var(--#{$prefix}red-500) !important;
}

&--success {
color: var(--#{$prefix}green-600) !important;
}

&--error,
&--success,
&--primary,
&--secondary {
.Mui-checked {
color: inherit !important;
}
}

&--disabled {
color: var(--#{$prefix}blue-gray-400) !important;
}

&__loader {
margin: var(--#{$prefix}space-0-25);
}
}
77 changes: 77 additions & 0 deletions src/design-system/components/checkbox/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox'
import { FC, ReactNode, useState } from 'react'
import Loader from '~/components/loader/Loader'
import { cn } from '~/utils/cn'

import './CheckBox.scss'

interface CheckBoxProps extends Omit<CheckboxProps, 'size'> {
variant: 'check' | 'middle'
label: ReactNode
labelPosition?: 'top' | 'bottom' | 'end'
color?: 'primary' | 'secondary' | 'error' | 'success'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
}

const CheckBox: FC<CheckBoxProps> = ({
color = 'primary',
disabled = false,
label,
labelPosition = 'end',
loading = false,
variant = 'check',
size = 'md',
...props
}) => {
const [checked, setChecked] = useState<boolean>(false)

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked)
}

const loaderSizeMapping: Record<string, number> = {
sm: 14,
md: 18,
lg: 20
}

const loader = <Loader size={loaderSizeMapping[size]} />

return (
<label
aria-busy={loading}
aria-disabled={disabled || loading}
className={cn(
's2s-checkbox',
`s2s-checkbox--${labelPosition}`,
`s2s-checkbox--${variant}`,
`s2s-checkbox--${size}`,
(disabled || loading) && 's2s-checkbox--disabled',
color === 'error' && 's2s-checkbox--error',
color === 'success' && 's2s-checkbox--success'
)}
data-testid='checkbox-label'
>
{loading ? (
<span className='s2s-checkbox__loader' data-testid='checkbox-loader'>
{loader}
</span>
) : (
<Checkbox
{...props}
checked={checked}
className='s2s-checkbox__input'
color={color}
data-testid='checkbox-input'
disabled={disabled || loading}
indeterminate={variant === 'middle' && checked}
onChange={handleChange}
/>
)}
<span className='s2s-checkbox__label'>{label}</span>
</label>
)
}

export default CheckBox
162 changes: 162 additions & 0 deletions src/design-system/stories/CheckBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import type { Meta, StoryObj } from '@storybook/react'
import CheckBox from '~scss-components/checkbox/CheckBox'

const meta: Meta<typeof CheckBox> = {
title: 'Components/CheckBox',
component: CheckBox,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: `
The \`CheckBox\` component is a highly customizable and user-friendly checkbox designed to fit seamlessly into your application's UI. With support for various styles, sizes, and states, it offers flexibility for diverse use cases while maintaining accessibility and visual consistency.
#### Key Features:
- **Variants:** Choose between the \`check\` style for a standard checkbox or the \`middle\` variant to display a minus sign instead of a checkmark, offering an alternative visual representation.
- **Label Placement:** Position the label above, below, or beside the checkbox to suit your layout preferences (\`top\`, \`bottom\`, or \`end\`).
- **Sizes:** Select from \`sm\`, \`md\`, or \`lg\` to match the checkbox to the context—whether it’s a compact form or a prominent control.
- **Loading State:** When an action is in progress, the checkbox displays a spinner and becomes temporarily non-interactive, enhancing user feedback.
- **Colors:** Use predefined color options like \`primary\`, \`secondary\`, \`success\`, or \`error\` to align the checkbox with your application's theme.
This component is ideal for use in forms, settings pages, or any interface requiring intuitive selection controls. Whether you need a straightforward checkbox or a visually distinctive option with loading feedback, the \`CheckBox\` component adapts to your design and functionality needs.
`
}
}
},
argTypes: {
variant: {
description: '',
control: { type: 'radio' },
options: ['check', 'middle']
},
labelPosition: {
description:
'Specifies the position of the label relative to the checkbox.',
control: { type: 'radio' },
options: ['top', 'bottom', 'end']
},
color: {
description:
"Defines the color of the checkbox, affecting its visual style. Can be used to align the component with your application's theme.",
control: { type: 'radio' },
options: ['primary', 'secondary', 'error', 'success']
},
size: {
description:
"Determines the size of the checkbox, adjusting its dimensions and the label's appearance.",
control: { type: 'radio' },
options: ['sm', 'md', 'lg']
},
loading: {
description:
'When true, displays a loading spinner instead of the checkbox and disables user interaction, signaling an action is in progress.',
control: 'boolean'
},
disabled: {
description:
'When true, disables the checkbox, preventing user interaction and applying a `disabled` style.',
control: 'boolean'
},
label: {
description: 'The content to be displayed as the label of the checkbox.'
}
}
}
export default meta

type Story = StoryObj<typeof CheckBox>

export const Default: Story = {
args: {
variant: 'check',
labelPosition: 'end',
label: 'Check me',
color: 'primary',
size: 'md'
},
parameters: {
docs: {
description: {
story:
'The default configuration of the checkbox with a primary color, medium size, and label positioned at the end.'
}
}
}
}

export const Disabled: Story = {
args: {
...Default.args,
disabled: true
},
parameters: {
docs: {
description: {
story:
'A disabled checkbox that prevents user interaction and applies a "disabled" style.'
}
}
}
}

export const Loading: Story = {
args: {
...Default.args,
loading: true
},
parameters: {
docs: {
description: {
story:
'A checkbox in a loading state, displaying a spinner and disabling user interaction.'
}
}
}
}

export const Error: Story = {
args: {
...Default.args,
color: 'error'
},
parameters: {
docs: {
description: {
story:
'A checkbox styled with an error color to indicate an issue or invalid state.'
}
}
}
}

export const Success: Story = {
args: {
...Default.args,
color: 'success'
},
parameters: {
docs: {
description: {
story:
'A checkbox styled with a success color, often used to indicate a positive or successful action.'
}
}
}
}

export const Indeterminate: Story = {
args: {
...Default.args,
variant: 'middle'
},
parameters: {
docs: {
description: {
story:
'A checkbox that visually displays a minus sign instead of a checkmark, functioning the same as the "check" variant.'
}
}
}
}
66 changes: 66 additions & 0 deletions tests/unit/design-system/components/Checkbox/Checkbox.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { fireEvent, render, screen } from '@testing-library/react'
import CheckBox from '~scss-components/checkbox/CheckBox'
import { expect } from 'vitest'

describe('CheckBox Component', () => {
test('renders checkbox with label', () => {
render(<CheckBox label='test label' />)
expect(screen.getByText('test label')).toBeInTheDocument()
})

test('toggles on click', () => {
render(<CheckBox label='test label' />)
const checkbox = document.querySelector('.PrivateSwitchBase-input')
expect(checkbox).not.toBeChecked()
fireEvent.click(checkbox)
expect(checkbox).toBeChecked()
})

test('renders Loader and disables checkbox in loding state', () => {
render(<CheckBox label='test label' loading />)
expect(screen.getByTestId('checkbox-loader')).toBeInTheDocument()
})

test('sets indeterminate state when variant is middle', () => {
render(<CheckBox label='test label' variant='middle' />)
const checkbox = document.querySelector('.PrivateSwitchBase-input')
fireEvent.click(checkbox)
expect(checkbox).toHaveAttribute('data-indeterminate', 'true')
})

test('is disabled when disabled', () => {
render(<CheckBox label='test label' disabled />)
const checkbox = screen.getByTestId('checkbox-input')
expect(checkbox).toHaveAttribute('aria-disabled', 'true')
})

test('applies correct size class', () => {
render(<CheckBox label='test label' size='lg' />)
const label = screen.getByText('test label').closest('label')
expect(label).toHaveClass('s2s-checkbox--lg')
})

test('starts unchecked', () => {
render(<CheckBox label='test label' variant='check' />)
const checkbox = document.querySelector('.PrivateSwitchBase-input')
expect(checkbox).not.toBeChecked()
})

test('applies correct color when success', () => {
render(<CheckBox label='test label' variant='check' color='success' />)
const label = screen.getByTestId('checkbox-label')
expect(label).toHaveClass('s2s-checkbox--success')
})

test('applies correct color when error', () => {
render(<CheckBox label='test label' variant='check' color='error' />)
const label = screen.getByTestId('checkbox-label')
expect(label).toHaveClass('s2s-checkbox--error')
})

test('renders loader with correct size', () => {
render(<CheckBox label='test label' variant='check' size='sm' loading />)
const loader = screen.getByRole('progressbar')
expect(loader).toHaveStyle('width: 14px; height: 14px')
})
})

0 comments on commit 2c4be0b

Please sign in to comment.