diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/atoms/buttons/button.stories.tsx | 4 | ||||
| -rw-r--r-- | src/components/atoms/buttons/button.tsx | 6 | ||||
| -rw-r--r-- | src/components/atoms/buttons/buttons.module.scss | 8 | ||||
| -rw-r--r-- | src/components/atoms/headings/heading.tsx | 6 | ||||
| -rw-r--r-- | src/components/organisms/forms/comment-form.module.scss | 8 | ||||
| -rw-r--r-- | src/components/organisms/forms/comment-form.stories.tsx | 86 | ||||
| -rw-r--r-- | src/components/organisms/forms/comment-form.test.tsx | 20 | ||||
| -rw-r--r-- | src/components/organisms/forms/comment-form.tsx | 174 | 
8 files changed, 304 insertions, 8 deletions
| 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<ButtonProps> = ({    className = '', -  ariaLabel,    children,    disabled = false,    kind = 'secondary', @@ -55,7 +54,6 @@ const Button: FC<ButtonProps> = ({        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 @@ -9,6 +9,10 @@ export type HeadingProps = {     */    className?: string;    /** +   * The heading id. +   */ +  id?: string; +  /**     * Use an heading element or only its styles. Default: false.     */    isFake?: boolean; @@ -30,6 +34,7 @@ export type HeadingProps = {  const Heading: FC<HeadingProps> = ({    children,    className, +  id,    isFake = false,    level,    withMargin = true, @@ -41,6 +46,7 @@ const Heading: FC<HeadingProps> = ({    return (      <TitleTag        className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${className}`} +      id={id}      >        {children}      </TitleTag> 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; | 
