diff --git a/src/design-system/components/checkbox/CheckBox.scss b/src/design-system/components/checkbox/CheckBox.scss new file mode 100644 index 000000000..fcff0e81f --- /dev/null +++ b/src/design-system/components/checkbox/CheckBox.scss @@ -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); + } +} diff --git a/src/design-system/components/checkbox/CheckBox.tsx b/src/design-system/components/checkbox/CheckBox.tsx new file mode 100644 index 000000000..6008ae00b --- /dev/null +++ b/src/design-system/components/checkbox/CheckBox.tsx @@ -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 { + variant: 'check' | 'middle' + label: ReactNode + labelPosition?: 'top' | 'bottom' | 'end' + color?: 'primary' | 'secondary' | 'error' | 'success' + size?: 'sm' | 'md' | 'lg' + loading?: boolean +} + +const CheckBox: FC = ({ + color = 'primary', + disabled = false, + label, + labelPosition = 'end', + loading = false, + variant = 'check', + size = 'md', + ...props +}) => { + const [checked, setChecked] = useState(false) + + const handleChange = (event: React.ChangeEvent) => { + setChecked(event.target.checked) + } + + const loaderSizeMapping: Record = { + sm: 14, + md: 18, + lg: 20 + } + + const loader = + + return ( + + ) +} + +export default CheckBox diff --git a/src/design-system/stories/CheckBox.stories.tsx b/src/design-system/stories/CheckBox.stories.tsx new file mode 100644 index 000000000..1be05002d --- /dev/null +++ b/src/design-system/stories/CheckBox.stories.tsx @@ -0,0 +1,162 @@ +import type { Meta, StoryObj } from '@storybook/react' +import CheckBox from '~scss-components/checkbox/CheckBox' + +const meta: Meta = { + 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 + +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.' + } + } + } +} diff --git a/tests/unit/design-system/components/Checkbox/Checkbox.spec.jsx b/tests/unit/design-system/components/Checkbox/Checkbox.spec.jsx new file mode 100644 index 000000000..032c5a1f6 --- /dev/null +++ b/tests/unit/design-system/components/Checkbox/Checkbox.spec.jsx @@ -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() + expect(screen.getByText('test label')).toBeInTheDocument() + }) + + test('toggles on click', () => { + render() + 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() + expect(screen.getByTestId('checkbox-loader')).toBeInTheDocument() + }) + + test('sets indeterminate state when variant is middle', () => { + render() + const checkbox = document.querySelector('.PrivateSwitchBase-input') + fireEvent.click(checkbox) + expect(checkbox).toHaveAttribute('data-indeterminate', 'true') + }) + + test('is disabled when disabled', () => { + render() + const checkbox = screen.getByTestId('checkbox-input') + expect(checkbox).toHaveAttribute('aria-disabled', 'true') + }) + + test('applies correct size class', () => { + render() + const label = screen.getByText('test label').closest('label') + expect(label).toHaveClass('s2s-checkbox--lg') + }) + + test('starts unchecked', () => { + render() + const checkbox = document.querySelector('.PrivateSwitchBase-input') + expect(checkbox).not.toBeChecked() + }) + + test('applies correct color when success', () => { + render() + const label = screen.getByTestId('checkbox-label') + expect(label).toHaveClass('s2s-checkbox--success') + }) + + test('applies correct color when error', () => { + render() + const label = screen.getByTestId('checkbox-label') + expect(label).toHaveClass('s2s-checkbox--error') + }) + + test('renders loader with correct size', () => { + render() + const loader = screen.getByRole('progressbar') + expect(loader).toHaveStyle('width: 14px; height: 14px') + }) +})