-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create CheckBox component for design system (#2841)
* add checkbox * fixed tests * add generic to useState * review comments * add cn function call * remove s2s from CheckBoxProps
- Loading branch information
Showing
4 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
66
tests/unit/design-system/components/Checkbox/Checkbox.spec.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |