Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/components/form/RadioField/RadioField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {Meta, StoryObj} from '@storybook/react-vite';
import {expect, userEvent, within} from 'storybook/test';

import {withFormik} from '@/sb-decorators';

import RadioField from './RadioField';

export default {
title: 'Internal API / Form / RadioField',
component: RadioField,
decorators: [withFormik],
args: {
name: 'radiofield',
label: 'Radiofield label',
options: [
{value: 'coffee', label: 'Coffee'},
{value: 'tea', label: 'Tea'},
{value: 'beer', label: 'Beer'},
],
},
parameters: {
formik: {
initialValues: {
radiofield: '',
},
},
},
} satisfies Meta<typeof RadioField>;

type Story = StoryObj<typeof RadioField>;

export const Default: Story = {
play: async ({canvasElement, step}) => {
const canvas = within(canvasElement);
const radios = canvas.getAllByRole('radio');
await expect(radios).toHaveLength(3);

await expect(canvas.getByText('Coffee')).toBeVisible();
await expect(canvas.getByText('Beer')).toBeVisible();

await step('Select value', async () => {
await userEvent.click(canvas.getByLabelText('Tea'));
await expect(radios[1]).toBeChecked();
});
},
};

export const Required: Story = {
args: {
isRequired: true,
},
};

export const WithInitialValue: Story = {
parameters: {
formik: {
initialValues: {
radiofield: 'coffee',
},
},
},
};

export const WithError: Story = {
parameters: {
formik: {
initialErrors: {
radiofield: 'Invalid!',
},
initialTouched: {
radiofield: true,
},
},
},
};
62 changes: 62 additions & 0 deletions src/components/form/RadioField/RadioField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type {Option} from '@maykin-ui/admin-ui';
import {ErrorMessage, RadioGroup} from '@maykin-ui/admin-ui';
import {useField, useFormikContext} from 'formik';
import {useId} from 'react';

import FormField from '../FormField';
import Label from '../Label';

export interface RadioFieldProps {
/**
* The name of the form field/input, used to set/track the field value in the form state.
*/
name: string;
/**
* The (accessible) label for the field - anything that can be rendered.
*
* You must always provide a label to ensure the field is accessible to users of
* assistive technologies.
*/
label: React.ReactNode;
/**
* Required fields get additional markup/styling to indicate this validation requirement.
*/
isRequired?: boolean;
/**
* The options for the radio group.
*/
options: Option[];
}

const RadioField: React.FC<RadioFieldProps> = ({name, label, options, isRequired = false}) => {
const {validateField} = useFormikContext();
const [{...props}, {error = '', touched}] = useField(name);
const id = useId();

const invalid = touched && !!error;
const errorMessageId = invalid ? `${id}-error-message` : undefined;

return (
<FormField>
<Label id={id} isRequired={isRequired}>
{label}
</Label>

{invalid && <ErrorMessage id={errorMessageId}>{error}</ErrorMessage>}
<RadioGroup
aria-labelledby={id}
aria-describedby={errorMessageId}
aria-invalid={invalid}
required={isRequired}
{...props}
options={options}
onBlur={async event => {
props.onBlur(event);
await validateField(name);
}}
/>
</FormField>
);
};

export default RadioField;
3 changes: 3 additions & 0 deletions src/components/form/RadioField/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import RadioField from './RadioField';

export {RadioField};