diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-09-22 19:34:01 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-10-24 12:23:48 +0200 |
| commit | a6ff5eee45215effb3344cb5d631a27a7c0369aa (patch) | |
| tree | 5051747acf72318b4fc5c18d603e3757fbefdfdb /src/components/organisms/forms/contact-form | |
| parent | 651ea4fc992e77d2f36b3c68f8e7a70644246067 (diff) | |
refactor(components): rewrite form components
Diffstat (limited to 'src/components/organisms/forms/contact-form')
5 files changed, 339 insertions, 0 deletions
diff --git a/src/components/organisms/forms/contact-form/contact-form.module.scss b/src/components/organisms/forms/contact-form/contact-form.module.scss new file mode 100644 index 0000000..c106fb1 --- /dev/null +++ b/src/components/organisms/forms/contact-form/contact-form.module.scss @@ -0,0 +1,15 @@ +.form { + display: flex; + flex-flow: column wrap; + gap: var(--spacing-xs); + max-width: 45ch; +} + +.field { + width: 100%; +} + +.button { + display: block; + margin: var(--spacing-sm) auto 0; +} diff --git a/src/components/organisms/forms/contact-form/contact-form.stories.tsx b/src/components/organisms/forms/contact-form/contact-form.stories.tsx new file mode 100644 index 0000000..4df3db0 --- /dev/null +++ b/src/components/organisms/forms/contact-form/contact-form.stories.tsx @@ -0,0 +1,65 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ContactForm } from './contact-form'; + +/** + * ContactForm - Storybook Meta + */ +export default { + title: 'Organisms/Forms', + component: ContactForm, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the form wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + Notice: { + control: { + type: null, + }, + description: 'A component to display a success or error message.', + table: { + category: 'Options', + }, + type: { + name: 'function', + required: false, + }, + }, + sendMail: { + control: { + type: null, + }, + description: 'A callback function to process the contact form data.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: true, + }, + }, + }, +} as ComponentMeta<typeof ContactForm>; + +const Template: ComponentStory<typeof ContactForm> = (args) => ( + <ContactForm {...args} /> +); + +/** + * Forms Stories - Contact + */ +export const Contact = Template.bind({}); +Contact.args = { + sendMail: async (_data, reset: () => void) => { + reset(); + }, +}; diff --git a/src/components/organisms/forms/contact-form/contact-form.test.tsx b/src/components/organisms/forms/contact-form/contact-form.test.tsx new file mode 100644 index 0000000..59d69fa --- /dev/null +++ b/src/components/organisms/forms/contact-form/contact-form.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '../../../../../tests/utils'; +import { ContactForm } from './contact-form'; + +const props = { + sendMail: async () => { + /** Do nothing. */ + }, +}; + +describe('ContactForm', () => { + it('renders a contact form', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('form', { name: 'Contact form' }) + ).toBeInTheDocument(); + }); + + it('renders a name field', () => { + render(<ContactForm {...props} />); + expect(screen.getByRole('textbox', { name: /^Name:/ })).toBeInTheDocument(); + }); + + it('renders an email field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Email:/ }) + ).toBeInTheDocument(); + }); + + it('renders an object field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Object:/ }) + ).toBeInTheDocument(); + }); + + it('renders a message field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Message:/ }) + ).toBeInTheDocument(); + }); + + it('renders a submit button', () => { + render(<ContactForm {...props} />); + expect(screen.getByRole('button', { name: /^Send/ })).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/forms/contact-form/contact-form.tsx b/src/components/organisms/forms/contact-form/contact-form.tsx new file mode 100644 index 0000000..6208b94 --- /dev/null +++ b/src/components/organisms/forms/contact-form/contact-form.tsx @@ -0,0 +1,210 @@ +import { ChangeEvent, FC, FormEvent, ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Button, Form, Input, Label, Spinner, TextArea } from '../../../atoms'; +import { LabelledField } from '../../../molecules'; +import styles from './contact-form.module.scss'; + +export type ContactFormData = { + email: string; + message: string; + name: string; + object: string; +}; + +export type ContactFormProps = { + /** + * Set additional classnames to the form wrapper. + */ + className?: string; + /** + * Pass a component to print a success/error message. + */ + Notice?: ReactNode; + /** + * A callback function to send mail. + */ + sendMail: (data: ContactFormData, reset: () => void) => Promise<void>; +}; + +/** + * ContactForm component + * + * Render a contact form. + */ +export const ContactForm: FC<ContactFormProps> = ({ + className = '', + Notice, + sendMail, +}) => { + const formClass = `${styles.form} ${className}`; + const intl = useIntl(); + const emptyForm: ContactFormData = { + email: '', + message: '', + name: '', + object: '', + }; + const [data, setData] = useState(emptyForm); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + + /** + * Reset all the form fields. + */ + const resetForm = () => { + setData(emptyForm); + setIsSubmitting(false); + }; + + const updateForm = ( + e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> + ) => { + switch (e.target.name) { + case 'email': + setData((prevData) => { + return { ...prevData, email: e.target.value }; + }); + break; + case 'message': + setData((prevData) => { + return { ...prevData, message: e.target.value }; + }); + break; + case 'name': + setData((prevData) => { + return { ...prevData, name: e.target.value }; + }); + break; + case 'object': + setData((prevData) => { + return { ...prevData, object: e.target.value }; + }); + break; + default: + break; + } + }; + + const formName = intl.formatMessage({ + defaultMessage: 'Contact form', + description: 'ContactForm: form accessible name', + id: 'HFdzae', + }); + + const nameLabel = intl.formatMessage({ + defaultMessage: 'Name:', + description: 'ContactForm: name label', + id: '1dCuCx', + }); + + const emailLabel = intl.formatMessage({ + defaultMessage: 'Email:', + description: 'ContactForm: email label', + id: 'w4B5PA', + }); + + const objectLabel = intl.formatMessage({ + defaultMessage: 'Object:', + description: 'ContactForm: object label', + id: 's8/tyz', + }); + + const messageLabel = intl.formatMessage({ + defaultMessage: 'Message:', + description: 'ContactForm: message label', + id: 'yN5P+m', + }); + + const submitHandler = async (e: FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + sendMail(data, resetForm).then(() => setIsSubmitting(false)); + }; + + return ( + <Form aria-label={formName} className={formClass} onSubmit={submitHandler}> + <LabelledField + className={styles.field} + field={ + <Input + id="contact-name" + isRequired + name="name" + onChange={updateForm} + type="text" + value={data.name} + /> + } + label={ + <Label htmlFor="contact-name" isRequired> + {nameLabel} + </Label> + } + /> + <LabelledField + className={styles.field} + field={ + <Input + id="contact-email" + isRequired + name="email" + onChange={updateForm} + type="email" + value={data.email} + /> + } + label={ + <Label htmlFor="contact-email" isRequired> + {emailLabel} + </Label> + } + /> + <LabelledField + className={styles.field} + field={ + <Input + id="contact-object" + name="object" + onChange={updateForm} + type="text" + value={data.object} + /> + } + label={<Label htmlFor="contact-object">{objectLabel}</Label>} + /> + <LabelledField + className={styles.field} + field={ + <TextArea + id="contact-message" + isRequired + name="message" + onChange={updateForm} + value={data.message} + /> + } + label={ + <Label htmlFor="contact-message" isRequired> + {messageLabel} + </Label> + } + /> + <Button type="submit" kind="primary" className={styles.button}> + {intl.formatMessage({ + defaultMessage: 'Send', + description: 'ContactForm: send button', + id: 'VkAnvv', + })} + </Button> + {isSubmitting && ( + <Spinner + message={intl.formatMessage({ + defaultMessage: 'Sending mail...', + description: 'ContactForm: spinner message on submit', + id: 'xaqaYQ', + })} + /> + )} + {Notice} + </Form> + ); +}; diff --git a/src/components/organisms/forms/contact-form/index.ts b/src/components/organisms/forms/contact-form/index.ts new file mode 100644 index 0000000..c72af3d --- /dev/null +++ b/src/components/organisms/forms/contact-form/index.ts @@ -0,0 +1 @@ +export * from './contact-form'; |
