diff --git a/src/components/form/RadioField/RadioField.stories.tsx b/src/components/form/RadioField/RadioField.stories.tsx new file mode 100644 index 0000000..a17d9b5 --- /dev/null +++ b/src/components/form/RadioField/RadioField.stories.tsx @@ -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; + +type Story = StoryObj; + +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, + }, + }, + }, +}; diff --git a/src/components/form/RadioField/RadioField.tsx b/src/components/form/RadioField/RadioField.tsx new file mode 100644 index 0000000..3ed5ddb --- /dev/null +++ b/src/components/form/RadioField/RadioField.tsx @@ -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 = ({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 ( + + + + {invalid && {error}} + { + props.onBlur(event); + await validateField(name); + }} + /> + + ); +}; + +export default RadioField; diff --git a/src/components/form/RadioField/index.ts b/src/components/form/RadioField/index.ts new file mode 100644 index 0000000..1b2edaa --- /dev/null +++ b/src/components/form/RadioField/index.ts @@ -0,0 +1,3 @@ +import RadioField from './RadioField'; + +export {RadioField};