diff options
Diffstat (limited to 'src/components/organisms/forms')
16 files changed, 1042 insertions, 0 deletions
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..1a9e7b7 --- /dev/null +++ b/src/components/organisms/forms/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.test.tsx b/src/components/organisms/forms/comment-form.test.tsx new file mode 100644 index 0000000..c67ad6b --- /dev/null +++ b/src/components/organisms/forms/comment-form.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@test-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.tsx b/src/components/organisms/forms/comment-form.tsx new file mode 100644 index 0000000..b2c725f --- /dev/null +++ b/src/components/organisms/forms/comment-form.tsx @@ -0,0 +1,193 @@ +import Button from '@components/atoms/buttons/button'; +import Form, { type FormProps } from '@components/atoms/forms/form'; +import Heading, { + type HeadingProps, + type HeadingLevel, +} from '@components/atoms/headings/heading'; +import Spinner from '@components/atoms/loaders/spinner'; +import LabelledField from '@components/molecules/forms/labelled-field'; +import { FC, ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './comment-form.module.scss'; + +export type CommentFormData = { + comment: string; + email: string; + name: 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; +}; + +const CommentForm: FC<CommentFormProps> = ({ + Notice, + parentId, + saveComment, + title, + titleAlignment, + titleLevel = 2, + ...props +}) => { + 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({ comment, email, name, parentId, website }, resetForm).then( + () => setIsSubmitting(false) + ); + }; + + return ( + <Form + onSubmit={submitHandler} + aria-label={formAriaLabel} + aria-labelledby={formLabelledBy} + {...props} + > + {title && ( + <Heading id={formId} level={titleLevel} alignment={titleAlignment}> + {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; diff --git a/src/components/organisms/forms/contact-form.module.scss b/src/components/organisms/forms/contact-form.module.scss new file mode 100644 index 0000000..f3f2646 --- /dev/null +++ b/src/components/organisms/forms/contact-form.module.scss @@ -0,0 +1,8 @@ +.field { + width: 100%; +} + +.button { + display: block; + margin: auto; +} diff --git a/src/components/organisms/forms/contact-form.stories.tsx b/src/components/organisms/forms/contact-form.stories.tsx new file mode 100644 index 0000000..191d448 --- /dev/null +++ b/src/components/organisms/forms/contact-form.stories.tsx @@ -0,0 +1,65 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import ContactForm from './contact-form'; + +/** + * ContactForm - Storybook Meta + */ +export default { + title: 'Organisms/Forms', + component: ContactForm, + 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, + }, + }, + sendMail: { + control: { + type: null, + }, + description: 'A callback function to process the contact form data.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: true, + }, + }, + }, +} as ComponentMeta<typeof ContactForm>; + +const Template: ComponentStory<typeof ContactForm> = (args) => ( + <ContactForm {...args} /> +); + +/** + * Forms Stories - Contact + */ +export const Contact = Template.bind({}); +Contact.args = { + sendMail: async (_data, reset: () => void) => { + reset(); + }, +}; diff --git a/src/components/organisms/forms/contact-form.test.tsx b/src/components/organisms/forms/contact-form.test.tsx new file mode 100644 index 0000000..6225fa9 --- /dev/null +++ b/src/components/organisms/forms/contact-form.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@test-utils'; +import ContactForm from './contact-form'; + +const props = { + sendMail: async () => { + /** Do nothing. */ + }, +}; + +describe('ContactForm', () => { + it('renders a contact form', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('form', { name: 'Contact form' }) + ).toBeInTheDocument(); + }); + + it('renders a name field', () => { + render(<ContactForm {...props} />); + expect(screen.getByRole('textbox', { name: /^Name:/ })).toBeInTheDocument(); + }); + + it('renders an email field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Email:/ }) + ).toBeInTheDocument(); + }); + + it('renders an object field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Object:/ }) + ).toBeInTheDocument(); + }); + + it('renders a message field', () => { + render(<ContactForm {...props} />); + expect( + screen.getByRole('textbox', { name: /^Message:/ }) + ).toBeInTheDocument(); + }); + + it('renders a submit button', () => { + render(<ContactForm {...props} />); + expect(screen.getByRole('button', { name: /^Send/ })).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx new file mode 100644 index 0000000..912402c --- /dev/null +++ b/src/components/organisms/forms/contact-form.tsx @@ -0,0 +1,158 @@ +import Button from '@components/atoms/buttons/button'; +import Form from '@components/atoms/forms/form'; +import Spinner from '@components/atoms/loaders/spinner'; +import LabelledField from '@components/molecules/forms/labelled-field'; +import { FC, ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './contact-form.module.scss'; + +export type ContactFormData = { + email: string; + message: string; + name: string; + subject: string; +}; + +export type ContactFormProps = { + /** + * Set additional classnames to the form wrapper. + */ + className?: string; + /** + * Pass a component to print a success/error message. + */ + Notice?: ReactNode; + /** + * A callback function to send mail. + */ + sendMail: (data: ContactFormData, reset: () => void) => Promise<void>; +}; + +/** + * ContactForm component + * + * Render a contact form. + */ +const ContactForm: FC<ContactFormProps> = ({ + className = '', + Notice, + sendMail, +}) => { + const intl = useIntl(); + const [name, setName] = useState<string>(''); + const [email, setEmail] = useState<string>(''); + const [object, setObject] = useState<string>(''); + const [message, setMessage] = useState<string>(''); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + + /** + * Reset all the form fields. + */ + const resetForm = () => { + setName(''); + setEmail(''); + setObject(''); + setMessage(''); + setIsSubmitting(false); + }; + + const formName = intl.formatMessage({ + defaultMessage: 'Contact form', + description: 'ContactForm: form accessible name', + id: 'HFdzae', + }); + + const nameLabel = intl.formatMessage({ + defaultMessage: 'Name:', + description: 'ContactForm: name label', + id: '1dCuCx', + }); + + const emailLabel = intl.formatMessage({ + defaultMessage: 'Email:', + description: 'ContactForm: email label', + id: 'w4B5PA', + }); + + const objectLabel = intl.formatMessage({ + defaultMessage: 'Object:', + description: 'ContactForm: object label', + id: 's8/tyz', + }); + + const messageLabel = intl.formatMessage({ + defaultMessage: 'Message:', + description: 'ContactForm: message label', + id: 'yN5P+m', + }); + + const submitHandler = async () => { + setIsSubmitting(true); + sendMail({ email, message, name, subject: object }, resetForm).then(() => + setIsSubmitting(false) + ); + }; + + return ( + <Form aria-label={formName} onSubmit={submitHandler} className={className}> + <LabelledField + type="text" + id="contact-name" + name="contact-name" + label={nameLabel} + required={true} + value={name} + setValue={setName} + className={styles.field} + /> + <LabelledField + type="email" + id="contact-email" + name="contact-email" + label={emailLabel} + required={true} + value={email} + setValue={setEmail} + className={styles.field} + /> + <LabelledField + type="text" + id="contact-object" + name="contact-object" + label={objectLabel} + value={object} + setValue={setObject} + className={styles.field} + /> + <LabelledField + type="textarea" + id="contact-message" + name="contact-message" + label={messageLabel} + required={true} + value={message} + setValue={setMessage} + className={styles.field} + /> + <Button type="submit" kind="primary" className={styles.button}> + {intl.formatMessage({ + defaultMessage: 'Send', + description: 'ContactForm: send button', + id: 'VkAnvv', + })} + </Button> + {isSubmitting && ( + <Spinner + message={intl.formatMessage({ + defaultMessage: 'Sending mail...', + description: 'ContactForm: spinner message on submit', + id: 'xaqaYQ', + })} + /> + )} + {Notice} + </Form> + ); +}; + +export default ContactForm; diff --git a/src/components/organisms/forms/search-form.module.scss b/src/components/organisms/forms/search-form.module.scss new file mode 100644 index 0000000..1d388a4 --- /dev/null +++ b/src/components/organisms/forms/search-form.module.scss @@ -0,0 +1,58 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { + display: flex; + align-items: center; + position: relative; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + max-width: 35ch; + } + } +} + +.btn { + position: absolute; + right: 0; + + &__icon { + transform: scale(0.85); + transition: all 0.3s ease-in-out 0s; + } + + &:focus { + outline: var(--color-primary-light) solid fun.convert-px(3); + } + + &:active { + outline: none; + } + + &:hover &, + &:focus & { + &__icon { + transform: scale(0.85) rotate(20deg) translateY(#{fun.convert-px(3)}); + } + } + + &:active & { + &__icon { + transform: scale(0.7); + } + } +} + +.field { + width: 100%; + padding-right: var(--spacing-lg); + + &:hover ~ .btn { + transform: translate(fun.convert-px(-3), fun.convert-px(-3)); + } + + &:focus ~ .btn { + transform: translate(fun.convert-px(3), fun.convert-px(3)); + } +} diff --git a/src/components/organisms/forms/search-form.stories.tsx b/src/components/organisms/forms/search-form.stories.tsx new file mode 100644 index 0000000..d8c8e1e --- /dev/null +++ b/src/components/organisms/forms/search-form.stories.tsx @@ -0,0 +1,65 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import SearchForm from './search-form'; + +/** + * SearchForm - Storybook Meta + */ +export default { + title: 'Organisms/Forms', + component: SearchForm, + args: { + hideLabel: false, + searchPage: '#', + }, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the form wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + hideLabel: { + control: { + type: 'boolean', + }, + description: 'Determine if the input label should be visually hidden.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + searchPage: { + control: { + type: 'text', + }, + description: 'The search results page url.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof SearchForm>; + +const Template: ComponentStory<typeof SearchForm> = (args) => ( + <SearchForm {...args} /> +); + +/** + * Forms Stories - Search + */ +export const Search = Template.bind({}); +Search.args = { + hideLabel: true, +}; diff --git a/src/components/organisms/forms/search-form.test.tsx b/src/components/organisms/forms/search-form.test.tsx new file mode 100644 index 0000000..59a2f68 --- /dev/null +++ b/src/components/organisms/forms/search-form.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@test-utils'; +import SearchForm from './search-form'; + +describe('SearchForm', () => { + it('renders a search input', () => { + render(<SearchForm searchPage="#" />); + expect( + screen.getByRole('searchbox', { name: 'Search for:' }) + ).toBeInTheDocument(); + }); + + it('renders a submit button', () => { + render(<SearchForm searchPage="#" />); + expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx new file mode 100644 index 0000000..1b5f662 --- /dev/null +++ b/src/components/organisms/forms/search-form.tsx @@ -0,0 +1,76 @@ +import Button from '@components/atoms/buttons/button'; +import Form from '@components/atoms/forms/form'; +import MagnifyingGlass from '@components/atoms/icons/magnifying-glass'; +import LabelledField, { + type LabelledFieldProps, +} from '@components/molecules/forms/labelled-field'; +import { useRouter } from 'next/router'; +import { forwardRef, ForwardRefRenderFunction, useId, useState } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './search-form.module.scss'; + +export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'> & { + /** + * The search page url. + */ + searchPage: string; +}; + +/** + * SearchForm component + * + * Render a search form. + */ +const SearchForm: ForwardRefRenderFunction< + HTMLInputElement, + SearchFormProps +> = ({ hideLabel, searchPage }, ref) => { + const intl = useIntl(); + const fieldLabel = intl.formatMessage({ + defaultMessage: 'Search for:', + description: 'SearchForm: field accessible label', + id: 'X8oujO', + }); + const buttonLabel = intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm: button accessible name', + id: 'WMqQrv', + }); + + const router = useRouter(); + const [value, setValue] = useState<string>(''); + + const submitHandler = () => { + router.push({ pathname: searchPage, query: { s: value } }); + setValue(''); + }; + + const id = useId(); + + return ( + <Form grouped={false} onSubmit={submitHandler} className={styles.wrapper}> + <LabelledField + className={styles.field} + hideLabel={hideLabel} + id={`search-form-${id}`} + label={fieldLabel} + name="search-form" + ref={ref} + setValue={setValue} + type="search" + value={value} + /> + <Button + type="submit" + kind="neutral" + shape="initial" + className={styles.btn} + aria-label={buttonLabel} + > + <MagnifyingGlass className={styles.btn__icon} /> + </Button> + </Form> + ); +}; + +export default forwardRef(SearchForm); diff --git a/src/components/organisms/forms/settings-form.module.scss b/src/components/organisms/forms/settings-form.module.scss new file mode 100644 index 0000000..a6a2077 --- /dev/null +++ b/src/components/organisms/forms/settings-form.module.scss @@ -0,0 +1,11 @@ +@use "@styles/abstracts/mixins" as mix; + +.label { + margin-right: auto; + + @include mix.media("screen") { + @include mix.dimensions(null, "2xs", "height") { + font-size: var(--font-size-sm); + } + } +} diff --git a/src/components/organisms/forms/settings-form.stories.tsx b/src/components/organisms/forms/settings-form.stories.tsx new file mode 100644 index 0000000..70e1844 --- /dev/null +++ b/src/components/organisms/forms/settings-form.stories.tsx @@ -0,0 +1,67 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import SettingsForm from './settings-form'; + +/** + * SettingsModal - Storybook Meta + */ +export default { + title: 'Organisms/Forms', + component: SettingsForm, + argTypes: { + ackeeStorageKey: { + control: { + type: 'text', + }, + description: 'The local storage key for Ackee setting.', + type: { + name: 'string', + required: true, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the modal wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + motionStorageKey: { + control: { + type: 'text', + }, + description: 'The local storage key for reduced motion setting.', + type: { + name: 'string', + required: true, + }, + }, + tooltipClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the tooltip wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof SettingsForm>; + +const Template: ComponentStory<typeof SettingsForm> = (args) => ( + <SettingsForm {...args} /> +); + +/** + * Form Stories - Settings + */ +export const Settings = Template.bind({}); diff --git a/src/components/organisms/forms/settings-form.test.tsx b/src/components/organisms/forms/settings-form.test.tsx new file mode 100644 index 0000000..43d546e --- /dev/null +++ b/src/components/organisms/forms/settings-form.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from '@test-utils'; +import SettingsForm from './settings-form'; + +const ackeeStorageKey = 'ackee-tracking'; +const motionStorageKey = 'reduce-motion'; + +describe('SettingsForm', () => { + it('renders a form', () => { + render( + <SettingsForm + ackeeStorageKey={ackeeStorageKey} + motionStorageKey={motionStorageKey} + /> + ); + expect( + screen.getByRole('form', { name: /^Settings form/i }) + ).toBeInTheDocument(); + }); + + it('renders a theme toggle setting', () => { + render( + <SettingsForm + ackeeStorageKey={ackeeStorageKey} + motionStorageKey={motionStorageKey} + /> + ); + expect( + screen.getByRole('checkbox', { name: /^Theme:/i }) + ).toBeInTheDocument(); + }); + + it('renders a code blocks toggle setting', () => { + render( + <SettingsForm + ackeeStorageKey={ackeeStorageKey} + motionStorageKey={motionStorageKey} + /> + ); + expect( + screen.getByRole('checkbox', { name: /^Code blocks:/i }) + ).toBeInTheDocument(); + }); + + it('renders a motion setting', () => { + render( + <SettingsForm + ackeeStorageKey={ackeeStorageKey} + motionStorageKey={motionStorageKey} + /> + ); + expect( + screen.getByRole('checkbox', { name: /^Animations:/i }) + ).toBeInTheDocument(); + }); + + it('renders a Ackee setting', () => { + render( + <SettingsForm + ackeeStorageKey={ackeeStorageKey} + motionStorageKey={motionStorageKey} + /> + ); + expect( + screen.getByRole('combobox', { name: /^Tracking:/i }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/forms/settings-form.tsx b/src/components/organisms/forms/settings-form.tsx new file mode 100644 index 0000000..c897fa5 --- /dev/null +++ b/src/components/organisms/forms/settings-form.tsx @@ -0,0 +1,56 @@ +import Form from '@components/atoms/forms/form'; +import AckeeSelect, { + type AckeeSelectProps, +} from '@components/molecules/forms/ackee-select'; +import MotionToggle, { + MotionToggleProps, +} from '@components/molecules/forms/motion-toggle'; +import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; +import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './settings-form.module.scss'; + +export type SettingsFormProps = Pick<AckeeSelectProps, 'tooltipClassName'> & { + /** + * The local storage key for Ackee settings. + */ + ackeeStorageKey: AckeeSelectProps['storageKey']; + /** + * The local storage key for Reduce motion settings. + */ + motionStorageKey: MotionToggleProps['storageKey']; +}; + +const SettingsForm: FC<SettingsFormProps> = ({ + ackeeStorageKey, + motionStorageKey, + tooltipClassName, +}) => { + const intl = useIntl(); + const ariaLabel = intl.formatMessage({ + defaultMessage: 'Settings form', + id: 'gX+YVy', + description: 'SettingsForm: an accessible form name', + }); + + return ( + <Form aria-label={ariaLabel} onSubmit={() => null}> + <ThemeToggle labelClassName={styles.label} /> + <PrismThemeToggle labelClassName={styles.label} /> + <MotionToggle + labelClassName={styles.label} + storageKey={motionStorageKey} + value={false} + /> + <AckeeSelect + initialValue="full" + labelClassName={styles.label} + tooltipClassName={tooltipClassName} + storageKey={ackeeStorageKey} + /> + </Form> + ); +}; + +export default SettingsForm; |
