aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/forms/radio-group
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/forms/radio-group')
-rw-r--r--src/components/molecules/forms/radio-group/index.ts1
-rw-r--r--src/components/molecules/forms/radio-group/radio-group.fixture.tsx41
-rw-r--r--src/components/molecules/forms/radio-group/radio-group.module.scss9
-rw-r--r--src/components/molecules/forms/radio-group/radio-group.stories.tsx75
-rw-r--r--src/components/molecules/forms/radio-group/radio-group.test.tsx59
-rw-r--r--src/components/molecules/forms/radio-group/radio-group.tsx110
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);