diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-04 19:26:16 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 2771de88f40a5f4ed7480bd8614532dda72deeda (patch) | |
| tree | ed4d1b9ee3f6322817efa2d1b1113c247367a12a /src/components | |
| parent | c4a561c333f6f82678efcffef5ce3ed0f8e322f4 (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')
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> |
