diff options
| -rw-r--r-- | src/components/atoms/forms/field.stories.tsx | 12 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.test.tsx | 20 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.tsx | 8 | ||||
| -rw-r--r-- | src/components/molecules/forms/labelled-field.stories.tsx | 180 | ||||
| -rw-r--r-- | src/components/molecules/forms/labelled-field.test.tsx | 19 | ||||
| -rw-r--r-- | src/components/molecules/forms/labelled-field.tsx | 25 |
6 files changed, 250 insertions, 14 deletions
diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx index 0406f10..02681e7 100644 --- a/src/components/atoms/forms/field.stories.tsx +++ b/src/components/atoms/forms/field.stories.tsx @@ -31,12 +31,9 @@ export default { type: 'text', }, description: 'Field id.', - table: { - category: 'Options', - }, type: { name: 'string', - required: false, + required: true, }, }, max: { @@ -70,12 +67,9 @@ export default { type: 'text', }, description: 'Field name.', - table: { - category: 'Options', - }, type: { name: 'string', - required: false, + required: true, }, }, placeholder: { @@ -171,6 +165,8 @@ const Template: ComponentStory<typeof FieldComponent> = (args) => ( export const Field = Template.bind({}); Field.args = { + id: 'field-storybook', + name: 'field-storybook', setValue: () => null, value: '', }; diff --git a/src/components/atoms/forms/field.test.tsx b/src/components/atoms/forms/field.test.tsx index 5488220..a04a976 100644 --- a/src/components/atoms/forms/field.test.tsx +++ b/src/components/atoms/forms/field.test.tsx @@ -3,12 +3,28 @@ import Field from './field'; describe('Field', () => { it('renders a text input', () => { - render(<Field type="text" value="" setValue={() => null} />); + render( + <Field + id="text-field" + name="text-field" + type="text" + value="" + setValue={() => null} + /> + ); expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text'); }); it('renders a search input', () => { - render(<Field type="search" value="" setValue={() => null} />); + render( + <Field + id="search-field" + name="search-field" + type="search" + value="" + setValue={() => null} + /> + ); expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'search'); }); }); diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx index 7d1ac93..513d2ba 100644 --- a/src/components/atoms/forms/field.tsx +++ b/src/components/atoms/forms/field.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, FC, SetStateAction } from 'react'; import styles from './forms.module.scss'; -type FieldType = +export type FieldType = | 'datetime-local' | 'email' | 'number' @@ -12,7 +12,7 @@ type FieldType = | 'time' | 'url'; -type FieldProps = { +export type FieldProps = { /** * Field state. Either enabled (false) or disabled (true). */ @@ -20,7 +20,7 @@ type FieldProps = { /** * Field id attribute. */ - id?: string; + id: string; /** * Field maximum value. */ @@ -32,7 +32,7 @@ type FieldProps = { /** * Field name attribute. */ - name?: string; + name: string; /** * Placeholder value. */ diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx new file mode 100644 index 0000000..eb7f8b5 --- /dev/null +++ b/src/components/molecules/forms/labelled-field.stories.tsx @@ -0,0 +1,180 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import LabelledFieldComponent from './labelled-field'; + +export default { + title: 'Molecules/Forms', + component: LabelledFieldComponent, + args: { + disabled: false, + required: false, + }, + argTypes: { + disabled: { + control: { + type: 'boolean', + }, + description: 'Field state: either enabled or disabled.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + id: { + control: { + type: 'text', + }, + description: 'Field id.', + type: { + name: 'string', + required: true, + }, + }, + label: { + control: { + type: 'text', + }, + description: 'Field label.', + type: { + name: 'string', + required: true, + }, + }, + max: { + control: { + type: 'number', + }, + description: 'Maximum value.', + table: { + category: 'Options', + }, + type: { + name: 'number', + required: false, + }, + }, + min: { + control: { + type: 'number', + }, + description: 'Minimum value.', + table: { + category: 'Options', + }, + type: { + name: 'number', + required: false, + }, + }, + name: { + control: { + type: 'text', + }, + description: 'Field name.', + type: { + name: 'string', + required: true, + }, + }, + placeholder: { + control: { + type: 'text', + }, + description: 'A placeholder value.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + required: { + control: { + type: 'boolean', + }, + description: 'Determine if the field is required.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + setValue: { + control: { + type: null, + }, + description: 'Callback function to set field value.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: true, + }, + }, + step: { + control: { + type: 'number', + }, + description: 'Field incremental values that are valid.', + table: { + category: 'Options', + }, + type: { + name: 'number', + required: false, + }, + }, + type: { + control: { + type: 'select', + }, + description: 'Field type: input type or textarea.', + options: [ + 'datetime-local', + 'email', + 'number', + 'search', + 'tel', + 'text', + 'textarea', + 'time', + 'url', + ], + type: { + name: 'string', + required: true, + }, + }, + value: { + control: { + type: 'text', + }, + description: 'Field value.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof LabelledFieldComponent>; + +const Template: ComponentStory<typeof LabelledFieldComponent> = (args) => ( + <LabelledFieldComponent {...args} /> +); + +export const LabelledField = Template.bind({}); +LabelledField.args = { + id: 'labelled-field-storybook', + label: 'Labelled field', + name: 'labelled-field-storybook', + setValue: () => null, + value: '', +}; diff --git a/src/components/molecules/forms/labelled-field.test.tsx b/src/components/molecules/forms/labelled-field.test.tsx new file mode 100644 index 0000000..6fabe19 --- /dev/null +++ b/src/components/molecules/forms/labelled-field.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from '@test-utils'; +import LabelledField from './labelled-field'; + +describe('LabelledField', () => { + it('renders a labelled field', () => { + render( + <LabelledField + type="text" + id="jest-text-field" + name="jest-text-field" + label="Jest text field" + value="test" + setValue={() => null} + /> + ); + expect(screen.getByLabelText('Jest text field')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveValue('test'); + }); +}); diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx new file mode 100644 index 0000000..7f81e23 --- /dev/null +++ b/src/components/molecules/forms/labelled-field.tsx @@ -0,0 +1,25 @@ +import Field, { type FieldProps } from '@components/atoms/forms/field'; +import Label from '@components/atoms/forms/label'; +import { FC } from 'react'; + +type LabelledFieldProps = FieldProps & { + label: string; +}; + +const LabelledField: FC<LabelledFieldProps> = ({ + id, + label, + required, + ...props +}) => { + return ( + <> + <Label htmlFor={id} required={required}> + {label} + </Label> + <Field id={id} required={required} {...props} /> + </> + ); +}; + +export default LabelledField; |
