From d31293894069d0b67ee5bcbfad975eab14fb8b6d Mon Sep 17 00:00:00 2001 From: Sofiia13 <142519729+SofiiaYevush@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:20:14 +0200 Subject: [PATCH] Created RadioButton component (#2842) * Created RadioButton component * Moving files into design-system folders and changing directions * Deleting commented code in files * Refactored RadioButton loader logic --- .../components/radio-button/RadioButton.scss | 108 ++++++++++ .../components/radio-button/RadioButton.tsx | 83 ++++++++ .../stories/RadioButton.stories.tsx | 185 ++++++++++++++++++ .../radio-button/RadioButton.spec.jsx | 85 ++++++++ 4 files changed, 461 insertions(+) create mode 100644 src/design-system/components/radio-button/RadioButton.scss create mode 100644 src/design-system/components/radio-button/RadioButton.tsx create mode 100644 src/design-system/stories/RadioButton.stories.tsx create mode 100644 tests/unit/design-system/components/radio-button/RadioButton.spec.jsx diff --git a/src/design-system/components/radio-button/RadioButton.scss b/src/design-system/components/radio-button/RadioButton.scss new file mode 100644 index 000000000..4936dfca1 --- /dev/null +++ b/src/design-system/components/radio-button/RadioButton.scss @@ -0,0 +1,108 @@ +@use '~scss/utilities' as *; + +.radio-btn-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.#{$prefix}radio-btn { + display: inline-flex; + align-items: center; + justify-content: flex-start; + + &:hover { + .MuiRadio-root { + box-shadow: 0 0 15px rgba(0, 0, 0, 0.4); + } + } + + &-sm { + font-size: $font-size-sm; + } + + &-md { + font-size: $font-size-md; + } + + &-lg { + font-size: $font-size-lg; + } + + &-primary { + .MuiRadio-root { + color: $neutral-950; + } + } + + &-success { + .MuiRadio-root { + color: $green-600; + } + } + + &-error { + .MuiRadio-root { + color: $red-500; + } + } + + &-disabled { + .MuiRadio-root { + opacity: 0.5; + pointer-events: none; + color: $neutral-600 !important; + box-shadow: none !important; + } + } + + .radio-sm { + width: $border-width-sm; + height: $border-width-sm; + } + + .radio-md { + width: $border-width-md; + height: $border-width-md; + } + + .radio-lg { + width: $border-width-lg; + height: $border-width-lg; + } + + .Mui-checked { + &.radio-primary { + color: $neutral-950; + } + + &.radio-success { + color: $green-600; + } + + &.radio-error { + color: $red-500; + } + } + + .MuiFormControlLabel-label { + font-size: inherit; + color: $body-text-color; + font-weight: normal; + margin-left: 8px; + margin-right: 8px; + + &.radio-sm { + font-size: $font-size-sm; + } + + &.radio-md { + font-size: $font-size-md; + } + + &.radio-lg { + font-size: $font-size-lg; + } + } +} diff --git a/src/design-system/components/radio-button/RadioButton.tsx b/src/design-system/components/radio-button/RadioButton.tsx new file mode 100644 index 000000000..dc1685c2d --- /dev/null +++ b/src/design-system/components/radio-button/RadioButton.tsx @@ -0,0 +1,83 @@ +import { forwardRef } from 'react' +import { + Radio, + RadioProps as MuiRadioProps, + FormControlLabel, + CircularProgress +} from '@mui/material' +import { cn } from '~/utils/cn' +import './RadioButton.scss' + +const sizes = ['sm', 'md', 'lg'] as const +const colors = ['primary', 'success', 'error'] as const + +type BaseRadioButtonProps = { + label: string + labelPosition?: 'top' | 'bottom' | 'end' + size?: (typeof sizes)[number] + color?: (typeof colors)[number] + loading?: boolean + checked?: boolean +} + +export type RadioButtonProps = BaseRadioButtonProps & + Omit + +const RadioButton = forwardRef( + ( + { + label, + labelPosition = 'end', + size = 'md', + color = 'primary', + className, + disabled = false, + loading = false, + checked = false, + ...props + }, + forwardedRef + ) => { + const isDisabled = disabled || loading + + const radioClassNames = cn( + `radio-${size}`, + `radio-${color}`, + { 'radio-checked': checked, 'radio-disabled': isDisabled }, + className + ) + + const formControlClassNames = cn( + 's2s-radio-btn', + `s2s-radio-btn-${size}`, + `s2s-radio-btn-${color}`, + { 's2s-radio-btn-disabled': isDisabled }, + className + ) + + return ( + + )) || ( + + ) + } + label={label} + labelPlacement={labelPosition} + ref={forwardedRef} + /> + ) + } +) + +RadioButton.displayName = 'RadioButton' + +export default RadioButton diff --git a/src/design-system/stories/RadioButton.stories.tsx b/src/design-system/stories/RadioButton.stories.tsx new file mode 100644 index 000000000..c04fbb86e --- /dev/null +++ b/src/design-system/stories/RadioButton.stories.tsx @@ -0,0 +1,185 @@ +import type { Meta, StoryObj } from '@storybook/react' +import RadioButton from '~scss-components/radio-button/RadioButton' +import { fn } from '@storybook/test' + +const meta: Meta = { + title: 'Components/RadioButton', + component: RadioButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The \`RadioButton\` component is a customizable radio button element used for selecting one option from a group. It supports various sizes, colors, and states to suit different use cases in your application. +#### Key Features: +- **Sizes:** Adjust the size of the radio button to fit your UI requirements: sm (small), md (medium), lg (large). +- **Colors:** Choose from several predefined color options to align with your design: primary, success, error. +- **Checked/Unchecked States:** Toggle the button between selected and unselected states. +- **Loading State:** Use a loading indicator to signal an ongoing process while the button is clicked. +- **Disabled State:** Disable the button to prevent interaction. + ` + } + } + }, + tags: ['autodocs'], + argTypes: { + label: { + description: 'The label that describes the purpose of the radio button.', + control: { type: 'text' } + }, + size: { + description: 'The size of the radio button.', + options: ['sm', 'md', 'lg'], + control: { type: 'radio' } + }, + color: { + description: 'The visual style of the radio button.', + options: ['primary', 'success', 'error'], + control: { type: 'radio' } + }, + checked: { + description: 'Indicates whether the radio button is selected.', + control: { type: 'boolean' } + }, + disabled: { + description: 'Disables the radio button, preventing user interaction.', + control: { type: 'boolean' } + }, + loading: { + description: + 'Displays a loading spinner instead of the radio button when true.', + control: { type: 'boolean' } + }, + labelPosition: { + description: 'The position of the label relative to the radio button.', + options: ['top', 'bottom', 'end'], + control: { type: 'radio' } + } + }, + args: { + label: 'Radio Button', + checked: false, + disabled: false, + loading: false, + size: 'md', + color: 'primary', + labelPosition: 'end', + onChange: fn() + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + label: 'Default Radio Button', + color: 'primary' + }, + parameters: { + docs: { + description: { + story: 'A basic unselected radio button.' + } + } + } +} + +export const Checked: Story = { + args: { + label: 'Checked Radio Button', + checked: true + }, + parameters: { + docs: { + description: { + story: 'A radio button that is checked (selected).' + } + } + } +} + +export const Loading: Story = { + args: { + label: 'Loading Radio Button', + loading: true + }, + parameters: { + docs: { + description: { + story: + 'A radio button with a loading spinner, indicating a process is ongoing.' + } + } + } +} + +export const Disabled: Story = { + args: { + label: 'Disabled Radio Button', + disabled: true + }, + parameters: { + docs: { + description: { + story: 'A disabled radio button that cannot be interacted with.' + } + } + } +} + +export const CustomSize: Story = { + args: { + label: 'Small Radio Button', + size: 'sm' + }, + parameters: { + docs: { + description: { + story: 'A small-sized radio button.' + } + } + } +} + +export const CustomColor: Story = { + args: { + label: 'Error Radio Button', + color: 'error' + }, + parameters: { + docs: { + description: { + story: 'A radio button with the error color variant.' + } + } + } +} + +export const LabelPositionTop: Story = { + args: { + label: 'Radio Button with Top Label', + labelPosition: 'top' + }, + parameters: { + docs: { + description: { + story: 'A radio button with the label positioned above the button.' + } + } + } +} + +export const LabelPositionBottom: Story = { + args: { + label: 'Radio Button with Bottom Label', + labelPosition: 'bottom' + }, + parameters: { + docs: { + description: { + story: 'A radio button with the label positioned below the button.' + } + } + } +} diff --git a/tests/unit/design-system/components/radio-button/RadioButton.spec.jsx b/tests/unit/design-system/components/radio-button/RadioButton.spec.jsx new file mode 100644 index 000000000..76a8ad358 --- /dev/null +++ b/tests/unit/design-system/components/radio-button/RadioButton.spec.jsx @@ -0,0 +1,85 @@ +import { render, screen } from '@testing-library/react'; +import RadioButton from '~scss-components/radio-button/RadioButton' + +describe('RadioButton Component', () => { + + test('should render the RadioButton with default props', () => { + render(); + const radioLabel = screen.getByText('Default Radio Button'); + expect(radioLabel).toBeInTheDocument(); + const radioButton = screen.getByRole('radio'); + expect(radioButton).toBeInTheDocument(); + expect(radioButton).not.toBeChecked(); + }); + + test('should render the RadioButton with checked state', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton).toBeChecked(); + }); + + test('should render the RadioButton in loading state', () => { + render(); + const loader = screen.getByRole('progressbar'); + expect(loader).toBeInTheDocument(); + const radioButton = screen.queryByRole('radio'); + expect(radioButton).not.toBeInTheDocument(); + }); + + test('should render the RadioButton in disabled state', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton).toBeDisabled(); + }); + + test('should render with custom size (sm)', () => { + render(); + const radioButtonWrapper = screen.getByRole('radio').parentElement; + expect(radioButtonWrapper).toHaveClass('radio-sm'); + }); + + test('should render with custom color (error)', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton.closest('.radio-error')).toBeInTheDocument(); + }); + + test('should render with custom color (success)', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton.closest('.radio-success')).toBeInTheDocument(); + }); + + test('should render with custom color (primary)', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton.closest('.radio-primary')).toBeInTheDocument(); + }); + + test('should render with custom label position (top)', () => { + render(); + const label = screen.getByText('Radio Button with Top Label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('MuiFormControlLabel-label'); + }); + + test('should render with custom label position (bottom)', () => { + render(); + const label = screen.getByText('Radio Button with Bottom Label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('MuiFormControlLabel-label'); + }); + + test('should render with custom label position (end)', () => { + render(); + const label = screen.getByText('Radio Button with End Label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('MuiFormControlLabel-label'); + }); + + test('should apply the correct class when checked', () => { + render(); + const radioButton = screen.getByRole('radio'); + expect(radioButton.closest('.radio-checked')).toBeInTheDocument(); + }); +}); \ No newline at end of file