From ce4a18899f24ba89b63ef743476ec0dbf1999ecf Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 3 Nov 2023 23:03:06 +0100 Subject: refactor(components): rewrite SearchForm component * remove searchPage prop (the consumer should handle the submit) * change onSubmit type * use `useForm` hook to handle the form --- .../forms/search-form/search-form.module.scss | 62 ++++++----- .../forms/search-form/search-form.stories.tsx | 43 +++----- .../forms/search-form/search-form.test.tsx | 32 ++++-- .../organisms/forms/search-form/search-form.tsx | 113 +++++++++++---------- 4 files changed, 133 insertions(+), 117 deletions(-) (limited to 'src/components/organisms/forms') diff --git a/src/components/organisms/forms/search-form/search-form.module.scss b/src/components/organisms/forms/search-form/search-form.module.scss index 1315fde..db247a2 100644 --- a/src/components/organisms/forms/search-form/search-form.module.scss +++ b/src/components/organisms/forms/search-form/search-form.module.scss @@ -1,57 +1,60 @@ @use "../../../../styles/abstracts/functions" as fun; -@use "../../../../styles/abstracts/mixins" as mix; -.wrapper { - display: flex; - align-items: center; - position: relative; +.input { + border-right: none; +} - @include mix.media("screen") { - @include mix.dimensions("sm") { - max-width: 35ch; - } - } +.icon { + transform: scale(0.85); + transition: all 0.3s ease-in-out 0s; } .btn { - align-self: stretch; background: var(--color-bg-tertiary); border: fun.convert-px(2) solid var(--color-border); - border-left: none; box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow); transition: all 0.25s linear 0s; - &__icon { - transform: scale(0.85); - transition: all 0.3s ease-in-out 0s; + &:hover, + &:focus { + .icon { + transform: scale(0.85) rotate(20deg) translateX(#{fun.convert-px(2)}) + translateY(#{fun.convert-px(3)}); + } } &:focus { - outline: var(--color-primary-light) solid fun.convert-px(3); + outline: none; + border-color: var(--color-primary); + box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 + var(--color-primary-dark); } &:active { - outline: none; + .icon { + transform: scale(0.7); + } } +} + +.wrapper { + display: flex; - &:hover &, - &:focus & { - &__icon { - transform: scale(0.85) rotate(20deg) translateY(#{fun.convert-px(3)}); + &--no-label { + .btn { + align-self: stretch; } } - &:active & { - &__icon { - transform: scale(0.7); + &--has-label { + .btn { + align-self: flex-end; + height: fun.convert-px(52); } } } .field { - min-width: 0; - width: 100%; - &:focus-within ~ .btn { background: var(--color-bg); border-color: var(--color-primary); @@ -66,5 +69,10 @@ box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1) var(--color-shadow); transform: translate(fun.convert-px(-3), fun.convert-px(-3)); + + &:focus { + box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1) + var(--color-primary-dark); + } } } diff --git a/src/components/organisms/forms/search-form/search-form.stories.tsx b/src/components/organisms/forms/search-form/search-form.stories.tsx index 971a8ee..d8e4339 100644 --- a/src/components/organisms/forms/search-form/search-form.stories.tsx +++ b/src/components/organisms/forms/search-form/search-form.stories.tsx @@ -5,26 +5,9 @@ import { SearchForm } from './search-form'; * SearchForm - Storybook Meta */ export default { - title: 'Organisms/Forms', + title: 'Organisms/Forms/Search', component: SearchForm, - args: { - isLabelHidden: false, - searchPage: '#', - }, argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the form wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, isLabelHidden: { control: { type: 'boolean', @@ -39,16 +22,6 @@ export default { required: false, }, }, - searchPage: { - control: { - type: 'text', - }, - description: 'The search results page url.', - type: { - name: 'string', - required: true, - }, - }, }, } as ComponentMeta; @@ -57,9 +30,17 @@ const Template: ComponentStory = (args) => ( ); /** - * Forms Stories - Search + * SearchForm Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + isLabelHidden: false, +}; + +/** + * SearchForm Stories - With hidden label */ -export const Search = Template.bind({}); -Search.args = { +export const WithHiddenLabel = Template.bind({}); +WithHiddenLabel.args = { isLabelHidden: true, }; diff --git a/src/components/organisms/forms/search-form/search-form.test.tsx b/src/components/organisms/forms/search-form/search-form.test.tsx index 908a8eb..56ba0d7 100644 --- a/src/components/organisms/forms/search-form/search-form.test.tsx +++ b/src/components/organisms/forms/search-form/search-form.test.tsx @@ -1,19 +1,39 @@ import { describe, expect, it } from '@jest/globals'; +import { userEvent } from '@testing-library/user-event'; import { render, screen as rtlScreen } from '../../../../../tests/utils'; import { SearchForm } from './search-form'; describe('SearchForm', () => { - it('renders a search input', () => { - render(); + it('renders a search input with a submit button', () => { + render(); + expect( rtlScreen.getByRole('searchbox', { name: 'Search for:' }) ).toBeInTheDocument(); - }); - - it('renders a submit button', () => { - render(); expect( rtlScreen.getByRole('button', { name: 'Search' }) ).toBeInTheDocument(); }); + + it('can submit the form', async () => { + const onSubmit = jest.fn((_search: { query?: string }) => undefined); + const user = userEvent.setup(); + const query = 'autem voluptatum eos'; + + render(); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + expect(onSubmit).not.toHaveBeenCalled(); + + await user.type( + rtlScreen.getByRole('searchbox', { name: 'Search for:' }), + query + ); + await user.click(rtlScreen.getByRole('button', { name: 'Search' })); + + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).toHaveBeenCalledWith({ query }); + }); }); diff --git a/src/components/organisms/forms/search-form/search-form.tsx b/src/components/organisms/forms/search-form/search-form.tsx index 5c685c0..3f16ad0 100644 --- a/src/components/organisms/forms/search-form/search-form.tsx +++ b/src/components/organisms/forms/search-form/search-form.tsx @@ -1,20 +1,22 @@ -import { useRouter } from 'next/router'; -import { - type ChangeEvent, - type FormEvent, - forwardRef, - type ForwardRefRenderFunction, - useId, - useState, - useCallback, -} from 'react'; +import { forwardRef, type ForwardRefRenderFunction, useId } from 'react'; import { useIntl } from 'react-intl'; -import { Button, Form, Icon, Input, Label } from '../../../atoms'; +import { type FormSubmitHandler, useForm } from '../../../../utils/hooks'; +import { + Button, + Form, + type FormProps, + Icon, + Input, + Label, +} from '../../../atoms'; import { LabelledField } from '../../../molecules'; import styles from './search-form.module.scss'; -export type SearchFormProps = { - className?: string; +export type SearchFormData = { query: string }; + +export type SearchFormSubmit = FormSubmitHandler; + +export type SearchFormProps = Omit & { /** * Should the label be visually hidden? * @@ -22,75 +24,80 @@ export type SearchFormProps = { */ isLabelHidden?: boolean; /** - * The search page url. + * A callback function to handle search form submit. */ - searchPage: string; + onSubmit?: SearchFormSubmit; }; const SearchFormWithRef: ForwardRefRenderFunction< HTMLInputElement, SearchFormProps -> = ({ className = '', isLabelHidden = false, searchPage }, ref) => { +> = ({ className = '', isLabelHidden = false, onSubmit, ...props }, 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 { values, submit, submitStatus, update } = useForm({ + initialValues: { query: '' }, + submitHandler: onSubmit, }); - - const router = useRouter(); - const [value, setValue] = useState(''); - - const submitHandler = useCallback( - (e: FormEvent) => { - e.preventDefault(); - router.push({ pathname: searchPage, query: { s: value } }); - setValue(''); - }, - [router, searchPage, value] - ); - - const updateForm = useCallback((e: ChangeEvent) => { - setValue(e.target.value); - }, []); - const id = useId(); - const formClass = `${styles.wrapper} ${className}`; + const formClass = [ + styles.wrapper, + styles[isLabelHidden ? 'wrapper--no-label' : 'wrapper--has-label'], + className, + ].join(' '); + const labels = { + button: intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm: button accessible name', + id: 'WMqQrv', + }), + field: intl.formatMessage({ + defaultMessage: 'Search for:', + description: 'SearchForm: field accessible label', + id: 'X8oujO', + }), + }; return ( -
+ } label={ -