aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/forms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-15 14:38:54 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-15 14:38:54 +0200
commitb836f0a9f8b783e3328983ad087aa2b7b297b43a (patch)
treeb2ef8841a3fc94a1fe400e53597f9338e5391858 /src/components/organisms/forms
parent0ff94252e27a80221e221c6159761f46aa111ac3 (diff)
chore: add a CommentForm component
Diffstat (limited to 'src/components/organisms/forms')
-rw-r--r--src/components/organisms/forms/comment-form.module.scss8
-rw-r--r--src/components/organisms/forms/comment-form.stories.tsx86
-rw-r--r--src/components/organisms/forms/comment-form.test.tsx20
-rw-r--r--src/components/organisms/forms/comment-form.tsx174
4 files changed, 288 insertions, 0 deletions
diff --git a/src/components/organisms/forms/comment-form.module.scss b/src/components/organisms/forms/comment-form.module.scss
new file mode 100644
index 0000000..f3f2646
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.module.scss
@@ -0,0 +1,8 @@
+.field {
+ width: 100%;
+}
+
+.button {
+ display: block;
+ margin: auto;
+}
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
new file mode 100644
index 0000000..1ab7cf2
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -0,0 +1,86 @@
+import Notice from '@components/atoms/layout/notice';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import CommentFormComponent from './comment-form';
+
+export default {
+ title: 'Organisms/Forms',
+ component: CommentFormComponent,
+ 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,
+ },
+ },
+ saveComment: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to process the comment form data.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The form title.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ },
+ description: 'The title level (hn).',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CommentFormComponent>;
+
+const Template: ComponentStory<typeof CommentFormComponent> = (args) => (
+ <IntlProvider locale="en">
+ <CommentFormComponent {...args} />
+ </IntlProvider>
+);
+
+export const CommentForm = Template.bind({});
+CommentForm.args = {
+ saveComment: (reset: () => void) => {
+ reset();
+ },
+};
diff --git a/src/components/organisms/forms/comment-form.test.tsx b/src/components/organisms/forms/comment-form.test.tsx
new file mode 100644
index 0000000..0d387b5
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from '@test-utils';
+import CommentForm from './comment-form';
+
+const title = 'Cum voluptas voluptatibus';
+
+describe('CommentForm', () => {
+ it('renders a form', () => {
+ render(<CommentForm saveComment={() => null} />);
+ expect(screen.getByRole('form')).toBeInTheDocument();
+ });
+
+ it('renders an optional title', () => {
+ render(
+ <CommentForm saveComment={() => null} title={title} titleLevel={2} />
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: title })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
new file mode 100644
index 0000000..6acbf94
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -0,0 +1,174 @@
+import Button from '@components/atoms/buttons/button';
+import Form from '@components/atoms/forms/form';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import Spinner from '@components/atoms/loaders/spinner';
+import LabelledField from '@components/molecules/forms/labelled-field';
+import { ReactNode, useState, VFC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './comment-form.module.scss';
+
+export type CommentFormProps = {
+ /**
+ * Set additional classnames to the form wrapper.
+ */
+ className?: string;
+ /**
+ * Pass a component to print a success/error message.
+ */
+ Notice?: ReactNode;
+ /**
+ * A callback function to save comment. It takes a function as parameter to
+ * reset the form.
+ */
+ saveComment: (reset: () => void) => void;
+ /**
+ * The form title.
+ */
+ title?: string;
+ /**
+ * The title level.
+ */
+ titleLevel?: HeadingLevel;
+};
+
+const CommentForm: VFC<CommentFormProps> = ({
+ className = '',
+ Notice,
+ saveComment,
+ title,
+ titleLevel = 2,
+}) => {
+ const intl = useIntl();
+ const [name, setName] = useState<string>('');
+ const [email, setEmail] = useState<string>('');
+ const [website, setWebsite] = useState<string>('');
+ const [comment, setComment] = useState<string>('');
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ /**
+ * Reset all the form fields.
+ */
+ const resetForm = () => {
+ setName('');
+ setEmail('');
+ setWebsite('');
+ setComment('');
+ setIsSubmitting(false);
+ };
+
+ const nameLabel = intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'CommentForm: name label',
+ id: 'ZIrTee',
+ });
+
+ const emailLabel = intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'CommentForm: email label',
+ id: 'Bh7z5v',
+ });
+
+ const websiteLabel = intl.formatMessage({
+ defaultMessage: 'Website:',
+ description: 'CommentForm: website label',
+ id: 'u41qSk',
+ });
+
+ const commentLabel = intl.formatMessage({
+ defaultMessage: 'Comment:',
+ description: 'CommentForm: comment label',
+ id: 'A8hGaK',
+ });
+
+ const formTitle = intl.formatMessage({
+ defaultMessage: 'Comment form',
+ description: 'CommentForm: aria label',
+ id: 'dz2kDV',
+ });
+
+ const formAriaLabel = title ? undefined : formTitle;
+ const formId = 'comment-form-title';
+ const formLabelledBy = title ? formId : undefined;
+
+ /**
+ * Handle form submit.
+ */
+ const submitHandler = () => {
+ setIsSubmitting(true);
+ saveComment(resetForm);
+ };
+
+ return (
+ <Form
+ onSubmit={submitHandler}
+ className={className}
+ aria-label={formAriaLabel}
+ aria-labelledby={formLabelledBy}
+ >
+ {title && (
+ <Heading id={formId} level={titleLevel}>
+ {title}
+ </Heading>
+ )}
+ <LabelledField
+ type="text"
+ id="commenter-name"
+ name="commenter-name"
+ label={nameLabel}
+ required={true}
+ value={name}
+ setValue={setName}
+ className={styles.field}
+ />
+ <LabelledField
+ type="email"
+ id="commenter-email"
+ name="commenter-email"
+ label={emailLabel}
+ required={true}
+ value={email}
+ setValue={setEmail}
+ className={styles.field}
+ />
+ <LabelledField
+ type="text"
+ id="commenter-website"
+ name="commenter-website"
+ label={websiteLabel}
+ required={false}
+ value={website}
+ setValue={setWebsite}
+ className={styles.field}
+ />
+ <LabelledField
+ type="textarea"
+ id="commenter-comment"
+ name="commenter-comment"
+ label={commentLabel}
+ required={true}
+ value={comment}
+ setValue={setComment}
+ className={styles.field}
+ />
+ <Button type="submit" kind="primary" className={styles.button}>
+ {intl.formatMessage({
+ defaultMessage: 'Publish',
+ description: 'CommentForm: submit button',
+ id: 'OL0Yzx',
+ })}
+ </Button>
+ {isSubmitting && (
+ <Spinner
+ message={intl.formatMessage({
+ defaultMessage: 'Submitting...',
+ description: 'CommentForm: spinner message on submit',
+ id: 'IY5ew6',
+ })}
+ />
+ )}
+ {Notice}
+ </Form>
+ );
+};
+
+export default CommentForm;