aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/forms/comment-form
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/organisms/forms/comment-form')
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.module.scss18
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.stories.tsx123
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.test.tsx23
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx251
-rw-r--r--src/components/organisms/forms/comment-form/index.ts1
5 files changed, 416 insertions, 0 deletions
diff --git a/src/components/organisms/forms/comment-form/comment-form.module.scss b/src/components/organisms/forms/comment-form/comment-form.module.scss
new file mode 100644
index 0000000..fbf8c96
--- /dev/null
+++ b/src/components/organisms/forms/comment-form/comment-form.module.scss
@@ -0,0 +1,18 @@
+.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/comment-form/comment-form.stories.tsx b/src/components/organisms/forms/comment-form/comment-form.stories.tsx
new file mode 100644
index 0000000..a6069e6
--- /dev/null
+++ b/src/components/organisms/forms/comment-form/comment-form.stories.tsx
@@ -0,0 +1,123 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { CommentForm } from './comment-form';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
+/**
+ * CommentForm - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: CommentForm,
+ args: {
+ saveComment,
+ titleAlignment: 'left',
+ titleLevel: 2,
+ },
+ 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,
+ },
+ },
+ parentId: {
+ control: {
+ type: null,
+ },
+ description: 'The parent id if it is a reply.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ saveComment: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to process the comment form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The form title.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleAlignment: {
+ control: {
+ type: 'select',
+ },
+ description: 'The heading alignment.',
+ options: ['center', 'left'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'left' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The title level (hn).',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 2 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CommentForm>;
+
+const Template: ComponentStory<typeof CommentForm> = (args) => (
+ <CommentForm {...args} />
+);
+
+/**
+ * Forms Stories - Comment
+ */
+export const Comment = Template.bind({});
diff --git a/src/components/organisms/forms/comment-form/comment-form.test.tsx b/src/components/organisms/forms/comment-form/comment-form.test.tsx
new file mode 100644
index 0000000..8aa38af
--- /dev/null
+++ b/src/components/organisms/forms/comment-form/comment-form.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from '../../../../../tests/utils';
+import { CommentForm } from './comment-form';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+const title = 'Cum voluptas voluptatibus';
+
+describe('CommentForm', () => {
+ it('renders a form', () => {
+ render(<CommentForm saveComment={saveComment} />);
+ expect(screen.getByRole('form')).toBeInTheDocument();
+ });
+
+ it('renders an optional title', () => {
+ render(
+ <CommentForm saveComment={saveComment} title={title} titleLevel={2} />
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: title })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx
new file mode 100644
index 0000000..be5d58f
--- /dev/null
+++ b/src/components/organisms/forms/comment-form/comment-form.tsx
@@ -0,0 +1,251 @@
+import { ChangeEvent, FC, FormEvent, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import {
+ Button,
+ Form,
+ type FormProps,
+ Heading,
+ type HeadingLevel,
+ type HeadingProps,
+ Spinner,
+ Input,
+ TextArea,
+ Label,
+} from '../../../atoms';
+import { LabelledField } from '../../../molecules';
+import styles from './comment-form.module.scss';
+
+export type CommentFormData = {
+ author: string;
+ comment: string;
+ email: string;
+ parentId?: number;
+ website?: string;
+};
+
+export type CommentFormProps = Pick<FormProps, 'className'> & {
+ /**
+ * Pass a component to print a success/error message.
+ */
+ Notice?: ReactNode;
+ /**
+ * The comment parent id.
+ */
+ parentId?: number;
+ /**
+ * A callback function to save comment. It takes a function as parameter to
+ * reset the form.
+ */
+ saveComment: (data: CommentFormData, reset: () => void) => Promise<void>;
+ /**
+ * The form title.
+ */
+ title?: string;
+ /**
+ * The form title alignment. Default: left.
+ */
+ titleAlignment?: HeadingProps['alignment'];
+ /**
+ * The title level. Default: 2.
+ */
+ titleLevel?: HeadingLevel;
+};
+
+export const CommentForm: FC<CommentFormProps> = ({
+ className = '',
+ Notice,
+ parentId,
+ saveComment,
+ title,
+ titleAlignment,
+ titleLevel = 2,
+ ...props
+}) => {
+ const formClass = `${styles.form} ${className}`;
+ const intl = useIntl();
+ const emptyForm: CommentFormData = {
+ author: '',
+ comment: '',
+ email: '',
+ parentId,
+ website: '',
+ };
+ const [data, setData] = useState(emptyForm);
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ /**
+ * Reset all the form fields.
+ */
+ const resetForm = () => {
+ setData(emptyForm);
+ 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;
+
+ const updateForm = (
+ e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+ ) => {
+ switch (e.target.name) {
+ case 'author':
+ setData((prevData) => {
+ return { ...prevData, author: e.target.value };
+ });
+ break;
+ case 'comment':
+ setData((prevData) => {
+ return { ...prevData, comment: e.target.value };
+ });
+ break;
+ case 'email':
+ setData((prevData) => {
+ return { ...prevData, email: e.target.value };
+ });
+ break;
+ case 'website':
+ setData((prevData) => {
+ return { ...prevData, website: e.target.value };
+ });
+ break;
+ default:
+ break;
+ }
+ };
+
+ const submitHandler = (e: FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ saveComment(data, resetForm).then(() => setIsSubmitting(false));
+ };
+
+ return (
+ <Form
+ {...props}
+ aria-label={formAriaLabel}
+ aria-labelledby={formLabelledBy}
+ className={formClass}
+ onSubmit={submitHandler}
+ >
+ {title && (
+ <Heading alignment={titleAlignment} id={formId} level={titleLevel}>
+ {title}
+ </Heading>
+ )}
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="commenter-name"
+ isRequired
+ name="author"
+ onChange={updateForm}
+ type="text"
+ value={data.author}
+ />
+ }
+ label={
+ <Label htmlFor="commenter-name" isRequired>
+ {nameLabel}
+ </Label>
+ }
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="commenter-email"
+ isRequired
+ name="email"
+ onChange={updateForm}
+ type="email"
+ value={data.email}
+ />
+ }
+ label={
+ <Label htmlFor="commenter-email" isRequired>
+ {emailLabel}
+ </Label>
+ }
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="commenter-website"
+ name="website"
+ onChange={updateForm}
+ type="url"
+ value={data.website}
+ />
+ }
+ label={<Label htmlFor="commenter-website">{websiteLabel}</Label>}
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <TextArea
+ id="commenter-comment"
+ isRequired
+ name="comment"
+ onChange={updateForm}
+ value={data.comment}
+ />
+ }
+ label={
+ <Label htmlFor="commenter-comment" isRequired>
+ {commentLabel}
+ </Label>
+ }
+ />
+ <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>
+ );
+};
diff --git a/src/components/organisms/forms/comment-form/index.ts b/src/components/organisms/forms/comment-form/index.ts
new file mode 100644
index 0000000..9e22bd9
--- /dev/null
+++ b/src/components/organisms/forms/comment-form/index.ts
@@ -0,0 +1 @@
+export * from './comment-form';