From a6ff5eee45215effb3344cb5d631a27a7c0369aa Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 22 Sep 2023 19:34:01 +0200 Subject: refactor(components): rewrite form components --- .../molecules/forms/radio-group/index.ts | 1 + .../forms/radio-group/radio-group.fixture.tsx | 41 ++++++++ .../forms/radio-group/radio-group.module.scss | 9 ++ .../forms/radio-group/radio-group.stories.tsx | 75 ++++++++++++++ .../forms/radio-group/radio-group.test.tsx | 59 +++++++++++ .../molecules/forms/radio-group/radio-group.tsx | 110 +++++++++++++++++++++ 6 files changed, 295 insertions(+) create mode 100644 src/components/molecules/forms/radio-group/index.ts create mode 100644 src/components/molecules/forms/radio-group/radio-group.fixture.tsx create mode 100644 src/components/molecules/forms/radio-group/radio-group.module.scss create mode 100644 src/components/molecules/forms/radio-group/radio-group.stories.tsx create mode 100644 src/components/molecules/forms/radio-group/radio-group.test.tsx create mode 100644 src/components/molecules/forms/radio-group/radio-group.tsx (limited to 'src/components/molecules/forms/radio-group') diff --git a/src/components/molecules/forms/radio-group/index.ts b/src/components/molecules/forms/radio-group/index.ts new file mode 100644 index 0000000..ed40543 --- /dev/null +++ b/src/components/molecules/forms/radio-group/index.ts @@ -0,0 +1 @@ +export * from './radio-group'; diff --git a/src/components/molecules/forms/radio-group/radio-group.fixture.tsx b/src/components/molecules/forms/radio-group/radio-group.fixture.tsx new file mode 100644 index 0000000..f1cbc05 --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.fixture.tsx @@ -0,0 +1,41 @@ +import { RadioGroupItem } 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: RadioGroupItem[] = [ + { + id: `${name}-${value1}`, + label: 'Option 1', + value: value1, + }, + { + id: `${name}-${value2}`, + label: 'Option 2', + value: value2, + }, + { + id: `${name}-${value3}`, + label: 'Option 3', + value: value3, + }, + { + id: `${name}-${value4}`, + label: 'Option 4', + value: value4, + }, + { + id: `${name}-${value5}`, + label: 'Option 5', + value: value5, + }, + ]; + + return options; +}; + +export const initialChoice = 'option2'; diff --git a/src/components/molecules/forms/radio-group/radio-group.module.scss b/src/components/molecules/forms/radio-group/radio-group.module.scss new file mode 100644 index 0000000..ad09c78 --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.module.scss @@ -0,0 +1,9 @@ +.group { + &--inline { + .option { + &:not(:last-of-type) { + margin-right: var(--spacing-xs); + } + } + } +} diff --git a/src/components/molecules/forms/radio-group/radio-group.stories.tsx b/src/components/molecules/forms/radio-group/radio-group.stories.tsx new file mode 100644 index 0000000..8e77c6e --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.stories.tsx @@ -0,0 +1,75 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Legend } from '../../../atoms'; +import { RadioGroup as RadioGroupComponent } from './radio-group'; +import { getOptions, initialChoice } from './radio-group.fixture'; +import { ChangeEventHandler, useCallback, useState } from 'react'; + +/** + * RadioGroup - Storybook Meta + */ +export default { + title: 'Molecules/Forms', + component: RadioGroupComponent, + args: {}, + argTypes: { + onChange: { + control: { + type: null, + }, + description: 'A callback function to handle selected option change.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: false, + }, + }, + options: { + description: 'An array of radio option object.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + value: { + control: { + type: 'text', + }, + description: 'The default selected option id.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = ({ + value, + ...args +}) => { + const [selection, setSelection] = useState(value); + + const handleChange: ChangeEventHandler = useCallback( + (e) => { + setSelection(e.target.value); + }, + [] + ); + + return ( + + ); +}; + +/** + * Radio Group Story + */ +export const RadioGroup = Template.bind({}); +RadioGroup.args = { + legend: Options:, + options: getOptions('group1'), + value: initialChoice, +}; diff --git a/src/components/molecules/forms/radio-group/radio-group.test.tsx b/src/components/molecules/forms/radio-group/radio-group.test.tsx new file mode 100644 index 0000000..dba1541 --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from '../../../../../tests/utils'; +import { Legend } from '../../../atoms'; +import { RadioGroup } from './radio-group'; +import { getOptions, initialChoice } from './radio-group.fixture'; + +const doNothing = () => { + /* Do nothing. */ +}; + +describe('RadioGroup', () => { + it('renders a legend', () => { + const legend = 'Options:'; + + render( + {legend}} + name="possimus" + onSwitch={doNothing} + options={getOptions()} + value={initialChoice} + /> + ); + + expect( + screen.getByRole('radiogroup', { name: legend }) + ).toBeInTheDocument(); + }); + + it('renders the correct number of radio', () => { + const options = getOptions(); + + render( + + ); + + expect(screen.getAllByRole('radio')).toHaveLength(options.length); + }); + + it('can render an inlined radio group', () => { + const options = getOptions(); + + render( + + ); + + expect(screen.getByRole('radiogroup')).toHaveClass('group--inline'); + }); +}); diff --git a/src/components/molecules/forms/radio-group/radio-group.tsx b/src/components/molecules/forms/radio-group/radio-group.tsx new file mode 100644 index 0000000..0ca4dac --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.tsx @@ -0,0 +1,110 @@ +import { ForwardRefRenderFunction, forwardRef } from 'react'; +import { + Fieldset, + FieldsetProps, + Label, + LabelProps, + Radio, + RadioProps, +} from '../../../atoms'; +import { LabelledField } from '../labelled-field'; +import styles from './radio-group.module.scss'; + +export type RadioGroupItem = { + /** + * The item id. + */ + id: string; + /** + * Should the item be disabled? + */ + isDisabled?: boolean; + /** + * The item label. + */ + label: LabelProps['children']; + /** + * The item value. + */ + value: string; +}; + +export type RadioGroupProps = Omit & { + /** + * Should we display the radio buttons inlined? + * + * @default false + */ + isInline?: boolean; + /** + * The radio group name. + */ + name: string; + /** + * A function to handle selection change. + */ + onSwitch?: RadioProps['onChange']; + /** + * The options. + */ + options: RadioGroupItem[]; + /** + * The selected value. It should match a RadioGroupItem value or be undefined. + */ + value?: RadioGroupItem['value']; +}; + +const RadioGroupWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + RadioGroupProps +> = ( + { + className = '', + isInline = false, + name, + onSwitch, + options, + value, + ...props + }, + ref +) => { + const layoutModifier = isInline ? styles['group--inline'] : ''; + const groupClass = `${layoutModifier} ${className}`; + + return ( +
+ {options.map((option) => ( + + } + isInline + isReversedOrder + key={option.id} + label={} + /> + ))} +
+ ); +}; + +/** + * RadioGroup component + * + * Render a group of labelled radio buttons. + */ +export const RadioGroup = forwardRef(RadioGroupWithRef); -- cgit v1.2.3