diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-30 19:44:37 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-31 23:15:11 +0200 |
| commit | 994ad1bec193b2d1a6e0d38d6ef3f3d2bd66c3ea (patch) | |
| tree | 53df625928d50ef11ceca6b4d0937d433b576aec /src | |
| parent | ae384aec5084b9fb9f02166890686a37d1260ef2 (diff) | |
chore: add a RadioGroup component
Diffstat (limited to 'src')
6 files changed, 348 insertions, 2 deletions
diff --git a/src/components/molecules/forms/labelled-boolean-field.stories.tsx b/src/components/molecules/forms/labelled-boolean-field.stories.tsx index 643f533..6098b51 100644 --- a/src/components/molecules/forms/labelled-boolean-field.stories.tsx +++ b/src/components/molecules/forms/labelled-boolean-field.stories.tsx @@ -10,14 +10,15 @@ export default { title: 'Molecules/Forms/Boolean', component: LabelledBooleanField, args: { + checked: false, + hidden: false, label, labelSize: 'small', - checked: false, }, argTypes: { checked: { control: { - type: 'boolean', + type: null, }, description: 'Should the option be checked?', type: { diff --git a/src/components/molecules/forms/radio-group.fixture.tsx b/src/components/molecules/forms/radio-group.fixture.tsx new file mode 100644 index 0000000..686467c --- /dev/null +++ b/src/components/molecules/forms/radio-group.fixture.tsx @@ -0,0 +1,47 @@ +import { RadioGroupOption } from './radio-group'; + +export const getOptions = (name: string = 'group1') => { + const value1 = 'option1'; + const value2 = 'option2'; + const value3 = 'option3'; + const value4 = 'option4'; + const value5 = 'option5'; + + const options: RadioGroupOption[] = [ + { + id: `${name}-${value1}`, + name: name, + label: 'Option 1', + value: value1, + }, + { + id: `${name}-${value2}`, + name: name, + label: 'Option 2', + value: value2, + }, + { + id: `${name}-${value3}`, + name: name, + label: 'Option 3', + value: value3, + }, + { + id: `${name}-${value4}`, + name: name, + label: 'Option 4', + value: value4, + }, + { + id: `${name}-${value5}`, + name: name, + label: 'Option 5', + value: value5, + }, + ]; + + return options; +}; + +export const initialChoice = 'option2'; +export const legend = 'Options:'; diff --git a/src/components/molecules/forms/radio-group.module.scss b/src/components/molecules/forms/radio-group.module.scss new file mode 100644 index 0000000..feda9bd --- /dev/null +++ b/src/components/molecules/forms/radio-group.module.scss @@ -0,0 +1,13 @@ +.option { + &:not(:last-of-type) { + margin-right: var(--spacing-xs); + } +} + +.wrapper { + &--inline { + .option:first-of-type { + margin-left: var(--spacing-2xs); + } + } +} diff --git a/src/components/molecules/forms/radio-group.stories.tsx b/src/components/molecules/forms/radio-group.stories.tsx new file mode 100644 index 0000000..b4c913a --- /dev/null +++ b/src/components/molecules/forms/radio-group.stories.tsx @@ -0,0 +1,166 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import RadioGroup from './radio-group'; +import { getOptions, initialChoice, legend } from './radio-group.fixture'; + +/** + * RadioGroup - Storybook Meta + */ +export default { + title: 'Molecules/Forms/RadioGroup', + component: RadioGroup, + args: { + labelSize: 'small', + }, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the fieldset.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + initialChoice: { + control: { + type: 'text', + }, + description: 'The default selected option id.', + type: { + name: 'string', + required: true, + }, + }, + labelPosition: { + control: { + type: 'select', + }, + description: 'Determine the label position.', + options: ['left', 'right'], + table: { + category: 'Options', + defaultValue: { summary: 'left' }, + }, + type: { + name: 'string', + required: false, + }, + }, + labelSize: { + control: { + type: 'select', + }, + description: 'The label size.', + options: ['medium', 'small'], + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + legend: { + control: { + type: 'text', + }, + description: 'The fieldset legend.', + type: { + name: 'string', + required: true, + }, + }, + legendClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the legend.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + legendPosition: { + control: { + type: 'select', + }, + description: 'Determine the legend position.', + options: ['inline', 'stacked'], + table: { + category: 'Options', + defaultValue: { summary: 'inline' }, + }, + type: { + name: 'string', + required: false, + }, + }, + options: { + description: 'An array of radio option object.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + }, +} as ComponentMeta<typeof RadioGroup>; + +const Template: ComponentStory<typeof RadioGroup> = (args) => ( + <RadioGroup {...args} /> +); + +/** + * Radio Group Stories - Inlined legend & left label + */ +export const InlinedLegendLeftLabel = Template.bind({}); +InlinedLegendLeftLabel.args = { + initialChoice: initialChoice, + labelPosition: 'left', + legend: legend, + legendPosition: 'inline', + options: getOptions('group1'), +}; + +/** + * Radio Group Stories - Inlined legend & left label + */ +export const InlinedLegendRightLabel = Template.bind({}); +InlinedLegendRightLabel.args = { + initialChoice: initialChoice, + labelPosition: 'right', + legend: legend, + legendPosition: 'inline', + options: getOptions('group2'), +}; + +/** + * Radio Group Stories - Stacked legend & left label + */ +export const StackedLegendLeftLabel = Template.bind({}); +StackedLegendLeftLabel.args = { + initialChoice: initialChoice, + labelPosition: 'left', + legend: legend, + legendPosition: 'stacked', + options: getOptions('group3'), +}; + +/** + * Radio Group Stories - Stacked legend & left label + */ +export const StackedLegendRightLabel = Template.bind({}); +StackedLegendRightLabel.args = { + initialChoice: initialChoice, + labelPosition: 'right', + legend: legend, + legendPosition: 'stacked', + options: getOptions('group4'), +}; diff --git a/src/components/molecules/forms/radio-group.test.tsx b/src/components/molecules/forms/radio-group.test.tsx new file mode 100644 index 0000000..8171a49 --- /dev/null +++ b/src/components/molecules/forms/radio-group.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@test-utils'; +import RadioGroup from './radio-group'; +import { getOptions, initialChoice, legend } from './radio-group.fixture'; + +describe('RadioGroup', () => { + it('renders a legend', () => { + render( + <RadioGroup + initialChoice={initialChoice} + legend={legend} + options={getOptions()} + /> + ); + expect(screen.findByRole('radiogroup', { name: legend })).toBeDefined(); + }); + + it('renders the correct number of radio', () => { + const options = getOptions(); + + render( + <RadioGroup + initialChoice={initialChoice} + legend={legend} + options={options} + /> + ); + const radios = screen.getAllByRole('radio'); + expect(radios).toHaveLength(options.length); + }); +}); diff --git a/src/components/molecules/forms/radio-group.tsx b/src/components/molecules/forms/radio-group.tsx new file mode 100644 index 0000000..68a8adf --- /dev/null +++ b/src/components/molecules/forms/radio-group.tsx @@ -0,0 +1,89 @@ +import Fieldset, { type FieldsetProps } from '@components/atoms/forms/fieldset'; +import { ChangeEvent, FC, useState } from 'react'; +import LabelledBooleanField, { + type LabelledBooleanFieldProps, +} from './labelled-boolean-field'; +import styles from './radio-group.module.scss'; + +export type RadioGroupOption = Pick< + LabelledBooleanFieldProps, + 'id' | 'label' | 'name' | 'value' +>; + +export type RadioGroupProps = Pick< + FieldsetProps, + 'className' | 'legend' | 'legendClassName' +> & + Pick<LabelledBooleanFieldProps, 'labelPosition' | 'labelSize'> & { + /** + * The default option value. + */ + initialChoice: string; + /** + * The legend position. Default: inline. + */ + legendPosition?: FieldsetProps['legendPosition']; + /** + * The options. + */ + options: RadioGroupOption[]; + }; + +/** + * RadioGroup component + * + * Render a group of labelled radio buttons. + */ +const RadioGroup: FC<RadioGroupProps> = ({ + className, + initialChoice, + labelPosition, + labelSize, + legendPosition = 'inline', + options, + ...props +}) => { + const [selectedChoice, setSelectedChoice] = useState<string>(initialChoice); + const wrapperModifier = `wrapper--${legendPosition}`; + + /** + * Update the selected choice based on the change event target. + * + * @param {ChangeEvent<HTMLInputElement>} e - The change event. + */ + const updateChoice = (e: ChangeEvent<HTMLInputElement>) => { + setSelectedChoice(e.target.value); + }; + + /** + * Retrieve an array of radio buttons. + * + * @returns {JSX.Element[]} The radio buttons. + */ + const getOptions = (): JSX.Element[] => { + return options.map((option) => ( + <LabelledBooleanField + key={option.id} + checked={selectedChoice === option.value} + className={styles.option} + labelPosition={labelPosition} + labelSize={labelSize} + onChange={updateChoice} + type="radio" + {...option} + /> + )); + }; + + return ( + <Fieldset + className={`${styles.wrapper} ${styles[wrapperModifier]} ${className}`} + legendPosition={legendPosition} + {...props} + > + {getOptions()} + </Fieldset> + ); +}; + +export default RadioGroup; |
