From b836f0a9f8b783e3328983ad087aa2b7b297b43a Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 15 Apr 2022 14:38:54 +0200 Subject: chore: add a CommentForm component --- src/components/atoms/buttons/button.stories.tsx | 4 +- src/components/atoms/buttons/button.tsx | 6 +- src/components/atoms/buttons/buttons.module.scss | 8 +- src/components/atoms/headings/heading.tsx | 6 + .../organisms/forms/comment-form.module.scss | 8 + .../organisms/forms/comment-form.stories.tsx | 86 ++++++++++ .../organisms/forms/comment-form.test.tsx | 20 +++ src/components/organisms/forms/comment-form.tsx | 174 +++++++++++++++++++++ 8 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 src/components/organisms/forms/comment-form.module.scss create mode 100644 src/components/organisms/forms/comment-form.stories.tsx create mode 100644 src/components/organisms/forms/comment-form.test.tsx create mode 100644 src/components/organisms/forms/comment-form.tsx (limited to 'src/components') diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx index 1061d83..d47a1ea 100644 --- a/src/components/atoms/buttons/button.stories.tsx +++ b/src/components/atoms/buttons/button.stories.tsx @@ -65,7 +65,7 @@ export default { type: 'select', }, description: 'Button kind.', - options: ['primary', 'secondary', 'tertiary'], + options: ['primary', 'secondary', 'tertiary', 'neutral'], table: { category: 'Options', defaultValue: { summary: 'secondary' }, @@ -93,7 +93,7 @@ export default { type: 'select', }, description: 'The link shape.', - options: ['circle', 'rectangle', 'square'], + options: ['circle', 'rectangle', 'square', 'initial'], table: { category: 'Options', defaultValue: { summary: 'rectangle' }, diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx index b223046..545c5c5 100644 --- a/src/components/atoms/buttons/button.tsx +++ b/src/components/atoms/buttons/button.tsx @@ -9,7 +9,7 @@ export type ButtonProps = { /** * Button accessible label. */ - ariaLabel?: string; + 'aria-label'?: string; /** * Button state. Default: false. */ @@ -25,7 +25,7 @@ export type ButtonProps = { /** * Button shape. Default: rectangle. */ - shape?: 'circle' | 'rectangle' | 'square'; + shape?: 'circle' | 'rectangle' | 'square' | 'initial'; /** * Button type attribute. Default: button. */ @@ -39,7 +39,6 @@ export type ButtonProps = { */ const Button: FC = ({ className = '', - ariaLabel, children, disabled = false, kind = 'secondary', @@ -55,7 +54,6 @@ const Button: FC = ({ type={type} disabled={disabled} className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`} - aria-label={ariaLabel} {...props} > {children} diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss index 87c92db..8e3e196 100644 --- a/src/components/atoms/buttons/buttons.module.scss +++ b/src/components/atoms/buttons/buttons.module.scss @@ -10,6 +10,10 @@ font-weight: 600; transition: all 0.3s ease-in-out 0s; + &--initial { + border-radius: 0; + } + &--rectangle { padding: var(--spacing-2xs) var(--spacing-md); } @@ -107,7 +111,7 @@ fun.convert-px(-4) var(--color-shadow-light), fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3) var(--color-shadow-light); - transform: scale(1.1); + transform: scale(var(--scale-up, 1.1)); } &:focus { @@ -119,7 +123,7 @@ box-shadow: 0 0 0 0 var(--color-shadow); color: var(--color-primary-dark); text-decoration: underline transparent 0; - transform: scale(0.94); + transform: scale(var(--scale-down, 0.94)); } } } diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx index 3048534..4703b5d 100644 --- a/src/components/atoms/headings/heading.tsx +++ b/src/components/atoms/headings/heading.tsx @@ -8,6 +8,10 @@ export type HeadingProps = { * Set additional classnames. */ className?: string; + /** + * The heading id. + */ + id?: string; /** * Use an heading element or only its styles. Default: false. */ @@ -30,6 +34,7 @@ export type HeadingProps = { const Heading: FC = ({ children, className, + id, isFake = false, level, withMargin = true, @@ -41,6 +46,7 @@ const Heading: FC = ({ return ( {children} 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; + +const Template: ComponentStory = (args) => ( + + + +); + +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( null} />); + expect(screen.getByRole('form')).toBeInTheDocument(); + }); + + it('renders an optional title', () => { + render( + 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 = ({ + className = '', + Notice, + saveComment, + title, + titleLevel = 2, +}) => { + const intl = useIntl(); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [website, setWebsite] = useState(''); + const [comment, setComment] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(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 ( +
+ {title && ( + + {title} + + )} + + + + + + {isSubmitting && ( + + )} + {Notice} + + ); +}; + +export default CommentForm; -- cgit v1.2.3