diff options
Diffstat (limited to 'src/components/molecules/forms/radio-group')
6 files changed, 295 insertions, 0 deletions
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<typeof RadioGroupComponent>; + +const Template: ComponentStory<typeof RadioGroupComponent> = ({ + value, + ...args +}) => { + const [selection, setSelection] = useState(value); + + const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback( + (e) => { + setSelection(e.target.value); + }, + [] + ); + + return ( + <RadioGroupComponent {...args} onSwitch={handleChange} value={selection} /> + ); +}; + +/** + * Radio Group Story + */ +export const RadioGroup = Template.bind({}); +RadioGroup.args = { + legend: <Legend>Options:</Legend>, + 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( + <RadioGroup + legend={<Legend>{legend}</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( + <RadioGroup + name="eaque" + onSwitch={doNothing} + options={options} + value={initialChoice} + /> + ); + + expect(screen.getAllByRole('radio')).toHaveLength(options.length); + }); + + it('can render an inlined radio group', () => { + const options = getOptions(); + + render( + <RadioGroup + isInline + name="architecto" + onSwitch={doNothing} + options={options} + value={initialChoice} + /> + ); + + 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<FieldsetProps, 'children' | 'role'> & { + /** + * 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 ( + <Fieldset + {...props} + className={groupClass} + isInline={isInline} + ref={ref} + role="radiogroup" + > + {options.map((option) => ( + <LabelledField + className={styles.option} + field={ + <Radio + id={option.id} + isChecked={value === option.value} + name={name} + onChange={onSwitch} + value={option.value} + /> + } + isInline + isReversedOrder + key={option.id} + label={<Label htmlFor={option.id}>{option.label}</Label>} + /> + ))} + </Fieldset> + ); +}; + +/** + * RadioGroup component + * + * Render a group of labelled radio buttons. + */ +export const RadioGroup = forwardRef(RadioGroupWithRef); |
