aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-04 19:26:16 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commit2771de88f40a5f4ed7480bd8614532dda72deeda (patch)
treeed4d1b9ee3f6322817efa2d1b1113c247367a12a /src/components
parentc4a561c333f6f82678efcffef5ce3ed0f8e322f4 (diff)
refactor(components): rewrite CommentForm component
* remove `Notice` prop to handle it directly in the form * replace `saveComment` prop with `onSubmit` * use `useForm` hook to handle the form
Diffstat (limited to 'src/components')
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.module.scss21
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.stories.tsx81
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.test.tsx164
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx350
-rw-r--r--src/components/organisms/layout/comment.fixture.ts6
-rw-r--r--src/components/organisms/layout/comment.module.scss11
-rw-r--r--src/components/organisms/layout/comment.stories.tsx17
-rw-r--r--src/components/organisms/layout/comment.tsx18
-rw-r--r--src/components/organisms/layout/comments-list.stories.tsx17
-rw-r--r--src/components/organisms/layout/comments-list.test.tsx4
-rw-r--r--src/components/organisms/layout/comments-list.tsx15
-rw-r--r--src/components/templates/page/page-layout.module.scss7
-rw-r--r--src/components/templates/page/page-layout.test.tsx2
-rw-r--r--src/components/templates/page/page-layout.tsx117
14 files changed, 382 insertions, 448 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
index a4de51e..0e40c2a 100644
--- a/src/components/organisms/forms/comment-form/comment-form.module.scss
+++ b/src/components/organisms/forms/comment-form/comment-form.module.scss
@@ -2,23 +2,22 @@
display: flex;
flex-flow: column wrap;
gap: var(--spacing-xs);
+ max-width: 45ch;
+}
- > * {
- max-width: 45ch;
- }
+.field {
+ width: 100%;
}
-.title {
+.btn {
width: fit-content;
- margin-inline: auto;
- margin-bottom: var(--spacing-sm);
+ margin: var(--spacing-sm) auto 0;
}
-.field {
- width: 100%;
+.notice {
+ margin-block-start: var(--spacing-sm);
}
-.button {
- display: block;
- margin: var(--spacing-sm) auto 0;
+.spinner {
+ margin-inline: auto;
}
diff --git a/src/components/organisms/forms/comment-form/comment-form.stories.tsx b/src/components/organisms/forms/comment-form/comment-form.stories.tsx
index a521bf7..fcc76fa 100644
--- a/src/components/organisms/forms/comment-form/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.stories.tsx
@@ -1,48 +1,13 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { CommentForm as CommentFormComponent } from './comment-form';
-const saveComment = async () => {
- /** Do nothing. */
-};
-
/**
* CommentForm - Storybook Meta
*/
export default {
title: 'Organisms/Forms',
component: CommentFormComponent,
- 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,
@@ -53,7 +18,7 @@ export default {
required: false,
},
},
- saveComment: {
+ onSubmit: {
control: {
type: null,
},
@@ -63,50 +28,6 @@ export default {
},
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,
},
},
diff --git a/src/components/organisms/forms/comment-form/comment-form.test.tsx b/src/components/organisms/forms/comment-form/comment-form.test.tsx
index 88a7de9..3578db9 100644
--- a/src/components/organisms/forms/comment-form/comment-form.test.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.test.tsx
@@ -1,24 +1,158 @@
import { describe, expect, it } from '@jest/globals';
+import { userEvent } from '@testing-library/user-event';
import { render, screen as rtlScreen } from '../../../../../tests/utils';
-import { CommentForm } from './comment-form';
-
-const saveComment = async () => {
- /** Do nothing. */
-};
-const title = 'Cum voluptas voluptatibus';
+import { CommentForm, type CommentFormData } from './comment-form';
describe('CommentForm', () => {
- it('renders a form', () => {
- render(<CommentForm saveComment={saveComment} />);
- expect(rtlScreen.getByRole('form')).toBeInTheDocument();
- });
+ const user = userEvent.setup();
- it('renders an optional title', () => {
- render(
- <CommentForm saveComment={saveComment} title={title} titleLevel={2} />
- );
+ it('renders the form fields with a submit button', () => {
+ const label = 'Comment form';
+ render(<CommentForm aria-label={label} />);
+
+ expect(rtlScreen.getByRole('form')).toHaveAccessibleName(label);
+ expect(
+ rtlScreen.getByRole('textbox', { name: /^Name:/ })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('textbox', { name: /^Email:/ })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('textbox', { name: /^Website:/ })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('textbox', { name: /^Comment:/ })
+ ).toBeInTheDocument();
expect(
- rtlScreen.getByRole('heading', { level: 2, name: title })
+ rtlScreen.getByRole('button', { name: /^Publish/ })
).toBeInTheDocument();
});
+
+ /* eslint-disable max-statements */
+ it('can submit the form', async () => {
+ const onSubmit = jest.fn((_data: CommentFormData) => undefined);
+ const values = {
+ author: 'Brandon_West93',
+ comment: 'Ut aspernatur et aut et ab.',
+ email: 'Fannie_Connelly8@example.net',
+ website: 'https://example.com',
+ } satisfies CommentFormData;
+
+ render(<CommentForm onSubmit={onSubmit} />);
+
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ expect.assertions(3);
+
+ expect(onSubmit).not.toHaveBeenCalled();
+
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Name:/ }),
+ values.author
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Email:/ }),
+ values.email
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Website:/ }),
+ values.website
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Comment:/ }),
+ values.comment
+ );
+ await user.click(rtlScreen.getByRole('button', { name: /^Publish/ }));
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ expect(onSubmit).toHaveBeenCalledWith(values);
+ });
+ /* eslint-enable max-statements */
+
+ /* eslint-disable max-statements */
+ it('can submit and inform user on success', async () => {
+ const successMsg = 'Comment has been saved.';
+ const onSubmit = jest.fn((_data: CommentFormData) => {
+ return {
+ messages: { success: successMsg },
+ validator: () => true,
+ };
+ });
+ const values = {
+ author: 'Brandon_West93',
+ comment: 'Ut aspernatur et aut et ab.',
+ email: 'Fannie_Connelly8@example.net',
+ parentId: undefined,
+ website: '',
+ } satisfies CommentFormData;
+
+ render(<CommentForm onSubmit={onSubmit} />);
+
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ expect.assertions(4);
+
+ expect(onSubmit).not.toHaveBeenCalled();
+
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Name:/ }),
+ values.author
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Email:/ }),
+ values.email
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Comment:/ }),
+ values.comment
+ );
+ await user.click(rtlScreen.getByRole('button', { name: /^Publish/ }));
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ expect(onSubmit).toHaveBeenCalledWith(values);
+ expect(rtlScreen.getByText(successMsg)).toBeInTheDocument();
+ });
+ /* eslint-enable max-statements */
+
+ /* eslint-disable max-statements */
+ it('can abort submit on error and inform user', async () => {
+ const errorMsg = 'Cannot save comment.';
+ const onSubmit = jest.fn((_data: CommentFormData) => {
+ return {
+ messages: { error: errorMsg },
+ validator: () => false,
+ };
+ });
+ const values = {
+ author: 'Brandon_West93',
+ comment: 'Ut aspernatur et aut et ab.',
+ email: 'Fannie_Connelly8@example.net',
+ parentId: undefined,
+ website: '',
+ } satisfies CommentFormData;
+
+ render(<CommentForm onSubmit={onSubmit} />);
+
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ expect.assertions(4);
+
+ expect(onSubmit).not.toHaveBeenCalled();
+
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Name:/ }),
+ values.author
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Email:/ }),
+ values.email
+ );
+ await user.type(
+ rtlScreen.getByRole('textbox', { name: /^Comment:/ }),
+ values.comment
+ );
+ await user.click(rtlScreen.getByRole('button', { name: /^Publish/ }));
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ expect(onSubmit).toHaveBeenCalledWith(values);
+ expect(rtlScreen.getByText(errorMsg)).toBeInTheDocument();
+ });
+ /* eslint-enable max-statements */
});
diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx
index 9059cbc..336e85a 100644
--- a/src/components/organisms/forms/comment-form/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.tsx
@@ -1,26 +1,21 @@
-/* eslint-disable max-statements */
-import {
- type ChangeEvent,
- type FC,
- type FormEvent,
- type ReactNode,
- useCallback,
- useMemo,
- useState,
- useId,
-} from 'react';
+import { type FC, useCallback, useId } from 'react';
import { useIntl } from 'react-intl';
-import { useBoolean } from '../../../../utils/hooks';
+import type { Nullable } from '../../../../types';
+import {
+ type FormSubmitHandler,
+ useForm,
+ type FormSubmitStatus,
+ type FormSubmitMessages,
+} from '../../../../utils/hooks';
import {
Button,
Form,
type FormProps,
- Heading,
- type HeadingLevel,
Spinner,
Input,
TextArea,
Label,
+ Notice,
} from '../../../atoms';
import { LabelledField } from '../../../molecules';
import styles from './comment-form.module.scss';
@@ -33,93 +28,114 @@ export type CommentFormData = {
website?: string;
};
-export type CommentFormProps = Pick<FormProps, 'className'> & {
+export type CommentFormSubmit = FormSubmitHandler<CommentFormData>;
+
+export type CommentFormProps = Omit<FormProps, 'children' | 'onSubmit'> & {
/**
- * Pass a component to print a success/error message.
+ * A callback function to handle form submit.
*/
- Notice?: ReactNode;
+ onSubmit?: CommentFormSubmit;
/**
* 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 title level. Default: 2.
- */
- titleLevel?: HeadingLevel;
};
export const CommentForm: FC<CommentFormProps> = ({
className = '',
- Notice,
+ onSubmit,
parentId,
- saveComment,
- title,
- titleLevel = 2,
...props
}) => {
+ const formId = useId();
const formClass = `${styles.form} ${className}`;
const intl = useIntl();
- const emptyForm: CommentFormData = useMemo(() => {
- return {
- author: '',
- comment: '',
- email: '',
- parentId,
- website: '',
- };
- }, [parentId]);
- const [data, setData] = useState(emptyForm);
- const {
- activate: activateNotice,
- deactivate: deactivateNotice,
- state: isSubmitting,
- } = useBoolean(false);
+ const { messages, submit, submitStatus, update, values } =
+ useForm<CommentFormData>({
+ initialValues:
+ /* The order matter: it will be reused to generate the fields in the right
+ * order. */
+ {
+ parentId,
+ author: '',
+ email: '',
+ website: '',
+ comment: '',
+ },
+ submitHandler: onSubmit,
+ });
- /**
- * Reset all the form fields.
- */
- const resetForm = useCallback(() => {
- setData(emptyForm);
- deactivateNotice();
- }, [deactivateNotice, emptyForm]);
+ const renderFields = useCallback(() => {
+ const entries = Object.entries(values) as [
+ keyof CommentFormData,
+ CommentFormData[keyof CommentFormData],
+ ][];
+ const labels: Record<Exclude<keyof CommentFormData, 'parentId'>, string> = {
+ author: intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'CommentForm: name label',
+ id: 'ZIrTee',
+ }),
+ comment: intl.formatMessage({
+ defaultMessage: 'Comment:',
+ description: 'CommentForm: comment label',
+ id: 'A8hGaK',
+ }),
+ email: intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'CommentForm: email label',
+ id: 'Bh7z5v',
+ }),
+ website: intl.formatMessage({
+ defaultMessage: 'Website:',
+ description: 'CommentForm: website label',
+ id: 'u41qSk',
+ }),
+ };
- const nameLabel = intl.formatMessage({
- defaultMessage: 'Name:',
- description: 'CommentForm: name label',
- id: 'ZIrTee',
- });
+ return entries.map(([field, value]) => {
+ const isRequired = field !== 'website';
+ const inputType = field === 'email' ? 'email' : 'text';
+ const fieldId = `${formId}-${field}`;
- const emailLabel = intl.formatMessage({
- defaultMessage: 'Email:',
- description: 'CommentForm: email label',
- id: 'Bh7z5v',
- });
+ return field === 'parentId' ? null : (
+ <LabelledField
+ className={styles.field}
+ field={
+ field === 'comment' ? (
+ <TextArea
+ id={fieldId}
+ isRequired
+ name={field}
+ onChange={update}
+ value={value}
+ />
+ ) : (
+ <Input
+ id={fieldId}
+ isRequired={isRequired}
+ name={field}
+ onChange={update}
+ type={inputType}
+ value={value}
+ />
+ )
+ }
+ key={field}
+ label={
+ <Label htmlFor={fieldId} isRequired={isRequired}>
+ {labels[field]}
+ </Label>
+ }
+ />
+ );
+ });
+ }, [values, formId, intl, update]);
- 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 btnLabel = intl.formatMessage({
+ defaultMessage: 'Publish',
+ description: 'CommentForm: submit button',
+ id: 'OL0Yzx',
});
const loadingMsg = intl.formatMessage({
@@ -128,137 +144,59 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'IY5ew6',
});
- const formAriaLabel = title ? undefined : formTitle;
- const formId = useId();
- const formLabelledBy = title ? formId : undefined;
-
- const updateForm = useCallback(
- (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;
+ const renderNotice = useCallback(
+ (
+ currentStatus: FormSubmitStatus,
+ msg: Nullable<Partial<FormSubmitMessages>>
+ ) => {
+ switch (currentStatus) {
+ case 'FAILED':
+ return msg?.error ? (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="error"
+ >
+ {msg.error}
+ </Notice>
+ ) : null;
+ case 'PENDING':
+ return (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="info"
+ >
+ <Spinner className={styles.spinner}>{loadingMsg}</Spinner>
+ </Notice>
+ );
+ case 'SUCCEEDED':
+ return msg?.success ? (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="success"
+ >
+ {msg.success}
+ </Notice>
+ ) : null;
default:
- break;
+ return null;
}
},
- []
- );
-
- const sendForm = useCallback(
- (e: FormEvent) => {
- e.preventDefault();
- activateNotice();
- saveComment(data, resetForm).then(() => deactivateNotice());
- },
- [activateNotice, data, deactivateNotice, resetForm, saveComment]
+ [loadingMsg]
);
return (
- <Form
- {...props}
- aria-label={formAriaLabel}
- aria-labelledby={formLabelledBy}
- className={formClass}
- onSubmit={sendForm}
- >
- {title ? (
- <Heading className={styles.title} id={formId} level={titleLevel}>
- {title}
- </Heading>
- ) : null}
- <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',
- })}
+ <Form {...props} className={formClass} onSubmit={submit}>
+ {renderFields()}
+ <Button
+ className={styles.btn}
+ isLoading={submitStatus === 'PENDING'}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="primary"
+ type="submit"
+ >
+ {btnLabel}
</Button>
- {isSubmitting ? <Spinner>{loadingMsg}</Spinner> : null}
- {Notice}
+ {renderNotice(submitStatus, messages)}
</Form>
);
};
diff --git a/src/components/organisms/layout/comment.fixture.ts b/src/components/organisms/layout/comment.fixture.ts
index bb18d22..84aa20e 100644
--- a/src/components/organisms/layout/comment.fixture.ts
+++ b/src/components/organisms/layout/comment.fixture.ts
@@ -23,9 +23,7 @@ export const meta = {
export const id = 5;
-export const saveComment = async () => {
- /** Do nothing. */
-};
+export const saveComment = () => undefined;
export const data: UserCommentProps = {
approved: true,
@@ -33,5 +31,5 @@ export const data: UserCommentProps = {
id,
meta,
parentId: 0,
- saveComment,
+ onSubmit: saveComment,
};
diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss
index f26b3fe..096f4c4 100644
--- a/src/components/organisms/layout/comment.module.scss
+++ b/src/components/organisms/layout/comment.module.scss
@@ -65,9 +65,14 @@
}
.form {
- margin-top: var(--spacing-sm);
+ &__wrapper {
+ margin-top: var(--spacing-sm);
+ }
- form > * {
- margin-inline: auto;
+ &__heading {
+ width: fit-content;
+ margin: 0 auto var(--spacing-md) auto;
}
+
+ margin-inline: auto;
}
diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx
index 9c33ba3..7426fc3 100644
--- a/src/components/organisms/layout/comment.stories.tsx
+++ b/src/components/organisms/layout/comment.stories.tsx
@@ -14,7 +14,7 @@ export default {
component: UserComment,
args: {
canReply: true,
- saveComment,
+ onSubmit: saveComment,
},
argTypes: {
author: {
@@ -59,19 +59,6 @@ export default {
required: true,
},
},
- 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,
@@ -90,7 +77,7 @@ export default {
value: {},
},
},
- saveComment: {
+ onSubmit: {
control: {
type: null,
},
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index adbb2cc..b55bb3d 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable max-statements */
import NextImage from 'next/image';
import Script from 'next/script';
import type { FC } from 'react';
@@ -6,7 +5,7 @@ import { useIntl } from 'react-intl';
import type { Comment as CommentSchema, WithContext } from 'schema-dts';
import type { SingleComment } from '../../../types';
import { useSettings, useToggle } from '../../../utils/hooks';
-import { Button, Link, Time } from '../../atoms';
+import { Button, Heading, Link, Time } from '../../atoms';
import {
Card,
CardActions,
@@ -24,7 +23,7 @@ export type UserCommentProps = Pick<
SingleComment,
'approved' | 'content' | 'id' | 'meta' | 'parentId'
> &
- Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
+ Pick<CommentFormProps, 'onSubmit'> & {
/**
* Enable or disable the reply button. Default: true.
*/
@@ -42,9 +41,8 @@ export const UserComment: FC<UserCommentProps> = ({
content,
id,
meta,
- Notice,
+ onSubmit,
parentId,
- saveComment,
...props
}) => {
const intl = useIntl();
@@ -173,13 +171,15 @@ export const UserComment: FC<UserCommentProps> = ({
) : null}
</Card>
{isReplying ? (
- <Card className={styles.form} variant={2}>
+ <Card className={styles.form__wrapper} variant={2}>
<CardBody>
+ <Heading className={styles.form__heading} level={2}>
+ {formTitle}
+ </Heading>
<CommentForm
- Notice={Notice}
+ className={styles.form}
+ onSubmit={onSubmit}
parentId={id}
- saveComment={saveComment}
- title={formTitle}
/>
</CardBody>
</Card>
diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx
index 4b32d7b..c1a262e 100644
--- a/src/components/organisms/layout/comments-list.stories.tsx
+++ b/src/components/organisms/layout/comments-list.stories.tsx
@@ -1,4 +1,4 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { CommentsList } from './comments-list';
import { comments } from './comments-list.fixture';
@@ -39,20 +39,7 @@ export default {
required: true,
},
},
- Notice: {
- control: {
- type: null,
- },
- description: 'A component to display a success or error message.',
- table: {
- category: 'Options',
- },
- type: {
- name: 'function',
- required: false,
- },
- },
- saveComment: {
+ onSubmit: {
control: {
type: null,
},
diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx
index f245ebb..d7e170c 100644
--- a/src/components/organisms/layout/comments-list.test.tsx
+++ b/src/components/organisms/layout/comments-list.test.tsx
@@ -1,4 +1,4 @@
-import { describe, expect, it } from '@jest/globals';
+import { describe, it } from '@jest/globals';
import { render } from '../../../../tests/utils';
import { saveComment } from './comment.fixture';
import { CommentsList } from './comments-list';
@@ -7,7 +7,7 @@ import { comments } from './comments-list.fixture';
describe('CommentsList', () => {
it('renders a comments list', () => {
render(
- <CommentsList comments={comments} depth={1} saveComment={saveComment} />
+ <CommentsList comments={comments} depth={1} onSubmit={saveComment} />
);
});
});
diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx
index af0152a..2d43583 100644
--- a/src/components/organisms/layout/comments-list.tsx
+++ b/src/components/organisms/layout/comments-list.tsx
@@ -6,10 +6,7 @@ import { UserComment, type UserCommentProps } from './comment';
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
export type CommentsListDepth = 0 | 1 | 2 | 3 | 4;
-export type CommentsListProps = Pick<
- UserCommentProps,
- 'Notice' | 'saveComment'
-> & {
+export type CommentsListProps = Pick<UserCommentProps, 'onSubmit'> & {
/**
* An array of comments.
*/
@@ -28,8 +25,7 @@ export type CommentsListProps = Pick<
export const CommentsList: FC<CommentsListProps> = ({
comments,
depth,
- Notice,
- saveComment,
+ onSubmit,
}) => {
/**
* Get each comment wrapped in a list item.
@@ -45,12 +41,7 @@ export const CommentsList: FC<CommentsListProps> = ({
return commentsList.map(({ replies, ...comment }) => (
<ListItem key={comment.id}>
- <UserComment
- canReply={!isLastLevel}
- Notice={Notice}
- saveComment={saveComment}
- {...comment}
- />
+ <UserComment canReply={!isLastLevel} onSubmit={onSubmit} {...comment} />
{replies.length && !isLastLevel ? (
<List hideMarker isOrdered spacing="sm">
{getItems(replies, startLevel + 1)}
diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss
index 4615f60..75b996c 100644
--- a/src/components/templates/page/page-layout.module.scss
+++ b/src/components/templates/page/page-layout.module.scss
@@ -73,16 +73,11 @@
&__section {
grid-column: 2;
-
- &:first-child {
- margin: var(--spacing-md) 0 0;
- }
}
&__title {
width: fit-content;
- margin-bottom: var(--spacing-md);
- margin-inline: auto;
+ margin: var(--spacing-md) auto;
}
&__no-comments {
diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx
index 6609b48..394f995 100644
--- a/src/components/templates/page/page-layout.test.tsx
+++ b/src/components/templates/page/page-layout.test.tsx
@@ -83,7 +83,7 @@ describe('PageLayout', () => {
</PageLayout>
);
expect(
- rtlScreen.getByRole('form', { name: /Leave a comment/i })
+ rtlScreen.getByRole('form', { name: /Comment form/i })
).toBeInTheDocument();
});
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 3fd5b02..434b8ff 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -4,15 +4,14 @@ import {
type HTMLAttributes,
type ReactNode,
useRef,
- useState,
useCallback,
} from 'react';
import { useIntl } from 'react-intl';
import type { BreadcrumbList } from 'schema-dts';
import { sendComment } from '../../../services/graphql';
-import type { Approved, SendCommentInput, SingleComment } from '../../../types';
+import type { SendCommentInput, SingleComment } from '../../../types';
import { useIsMounted } from '../../../utils/hooks';
-import { Heading, Notice, type NoticeKind, Sidebar } from '../../atoms';
+import { Heading, Sidebar } from '../../atoms';
import {
PageFooter,
type PageFooterProps,
@@ -21,12 +20,12 @@ import {
} from '../../molecules';
import {
CommentForm,
- type CommentFormProps,
CommentsList,
type CommentsListProps,
TableOfContents,
Breadcrumbs,
type BreadcrumbsItem,
+ type CommentFormSubmit,
} from '../../organisms';
import styles from './page-layout.module.scss';
@@ -40,12 +39,6 @@ const hasComments = (
): comments is SingleComment[] =>
Array.isArray(comments) && comments.length > 0;
-type CommentStatus = {
- isReply: boolean;
- kind: NoticeKind;
- message: string;
-};
-
export type PageLayoutProps = {
/**
* True if the page accepts new comments. Default: false.
@@ -137,21 +130,37 @@ export const PageLayout: FC<PageLayoutProps> = ({
description: 'PageLayout: comments title',
id: '+dJU3e',
});
- const commentFormTitle = intl.formatMessage({
+ const commentFormSectionTitle = intl.formatMessage({
defaultMessage: 'Leave a comment',
description: 'PageLayout: comment form title',
id: 'kzIYoQ',
});
+ const commentFormTitle = intl.formatMessage({
+ defaultMessage: 'Comment form',
+ description: 'PageLayout: comment form accessible name',
+ id: 'l+Jcf6',
+ });
const bodyRef = useRef<HTMLDivElement>(null);
const isMounted = useIsMounted(bodyRef);
- const [commentStatus, setCommentStatus] = useState<CommentStatus | undefined>(
- undefined
- );
- const isSuccessStatus = useCallback(
- (comment: Approved | null, isReply: boolean, isSuccess: boolean) => {
- if (isSuccess) {
+ const saveComment: CommentFormSubmit = useCallback(
+ async (data) => {
+ if (!id) throw new Error('Page id missing. Cannot save comment.');
+
+ const { author, comment: commentBody, email, parentId, website } = data;
+ const commentData: SendCommentInput = {
+ author,
+ authorEmail: email,
+ authorUrl: website ?? '',
+ clientMutationId: 'comment',
+ commentOn: id,
+ content: commentBody,
+ parent: parentId,
+ };
+ const { comment, success } = await sendComment(commentData);
+
+ if (success) {
const successPrefix = intl.formatMessage({
defaultMessage: 'Thanks, your comment was successfully sent.',
description: 'PageLayout: comment form success message',
@@ -168,45 +177,26 @@ export const PageLayout: FC<PageLayoutProps> = ({
id: 'Vmj5cw',
description: 'PageLayout: comment awaiting moderation',
});
- setCommentStatus({
- isReply,
- kind: 'success',
- message: `${successPrefix} ${successMessage}`,
- });
- return true;
+ return {
+ messages: {
+ success: `${successPrefix} ${successMessage}`,
+ },
+ validator: () => success,
+ };
}
- const error = intl.formatMessage({
- defaultMessage: 'An error occurred:',
- description: 'PageLayout: comment form error message',
- id: 'fkcTGp',
- });
- setCommentStatus({ isReply, kind: 'error', message: error });
- return false;
- },
- [intl]
- );
-
- const saveComment: CommentFormProps['saveComment'] = useCallback(
- async (data, reset) => {
- if (!id) throw new Error('Page id missing. Cannot save comment.');
-
- const { author, comment: commentBody, email, parentId, website } = data;
- const isReply = !!parentId;
- const commentData: SendCommentInput = {
- author,
- authorEmail: email,
- authorUrl: website ?? '',
- clientMutationId: 'comment',
- commentOn: id,
- content: commentBody,
- parent: parentId,
+ return {
+ messages: {
+ error: intl.formatMessage({
+ defaultMessage: 'An error occurred:',
+ description: 'PageLayout: comment form error message',
+ id: 'fkcTGp',
+ }),
+ },
+ validator: () => success,
};
- const { comment, success } = await sendComment(commentData);
-
- if (isSuccessStatus(comment, isReply, success)) reset();
},
- [id, isSuccessStatus]
+ [id, intl]
);
return (
@@ -276,14 +266,7 @@ export const PageLayout: FC<PageLayoutProps> = ({
<CommentsList
comments={comments}
depth={2}
- Notice={
- commentStatus?.isReply ? (
- <Notice className={styles.notice} kind={commentStatus.kind}>
- {commentStatus.message}
- </Notice>
- ) : null
- }
- saveComment={saveComment}
+ onSubmit={saveComment}
/>
) : (
<p className={styles['comments__no-comments']}>
@@ -296,17 +279,13 @@ export const PageLayout: FC<PageLayoutProps> = ({
)}
</section>
<section className={styles.comments__section}>
+ <Heading className={styles.comments__title} level={2}>
+ {commentFormSectionTitle}
+ </Heading>
<CommentForm
+ aria-label={commentFormTitle}
className={styles.comments__form}
- saveComment={saveComment}
- title={commentFormTitle}
- Notice={
- commentStatus && !commentStatus.isReply ? (
- <Notice className={styles.notice} kind={commentStatus.kind}>
- {commentStatus.message}
- </Notice>
- ) : null
- }
+ onSubmit={saveComment}
/>
</section>
</div>