diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-09-22 19:34:01 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-10-24 12:23:48 +0200 |
| commit | a6ff5eee45215effb3344cb5d631a27a7c0369aa (patch) | |
| tree | 5051747acf72318b4fc5c18d603e3757fbefdfdb /src/components/organisms/forms/search-form | |
| parent | 651ea4fc992e77d2f36b3c68f8e7a70644246067 (diff) | |
refactor(components): rewrite form components
Diffstat (limited to 'src/components/organisms/forms/search-form')
5 files changed, 247 insertions, 0 deletions
diff --git a/src/components/organisms/forms/search-form/index.ts b/src/components/organisms/forms/search-form/index.ts new file mode 100644 index 0000000..e7d3f3d --- /dev/null +++ b/src/components/organisms/forms/search-form/index.ts @@ -0,0 +1 @@ +export * from './search-form'; diff --git a/src/components/organisms/forms/search-form/search-form.module.scss b/src/components/organisms/forms/search-form/search-form.module.scss new file mode 100644 index 0000000..e485380 --- /dev/null +++ b/src/components/organisms/forms/search-form/search-form.module.scss @@ -0,0 +1,67 @@ +@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 { + 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; + } + + &: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 { + &:focus-within ~ .btn { + background: var(--color-bg); + border-color: var(--color-primary); + box-shadow: none; + transform: translate(fun.convert-px(3), fun.convert-px(3)); + transition: + all 0.2s ease-in-out 0s, + transform 0.3s ease-out 0s; + } + + &:hover:not(:focus-within) ~ .btn { + 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)); + } +} diff --git a/src/components/organisms/forms/search-form/search-form.stories.tsx b/src/components/organisms/forms/search-form/search-form.stories.tsx new file mode 100644 index 0000000..c5fbeb9 --- /dev/null +++ b/src/components/organisms/forms/search-form/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: { + 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', + }, + 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 = { + 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 new file mode 100644 index 0000000..b53b9cf --- /dev/null +++ b/src/components/organisms/forms/search-form/search-form.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '../../../../../tests/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/search-form.tsx b/src/components/organisms/forms/search-form/search-form.tsx new file mode 100644 index 0000000..826e6c8 --- /dev/null +++ b/src/components/organisms/forms/search-form/search-form.tsx @@ -0,0 +1,98 @@ +import { useRouter } from 'next/router'; +import { + ChangeEvent, + FormEvent, + forwardRef, + ForwardRefRenderFunction, + useId, + useState, +} from 'react'; +import { useIntl } from 'react-intl'; +import { Button, Form, Input, Label, MagnifyingGlass } from '../../../atoms'; +import { LabelledField } from '../../../molecules'; +import styles from './search-form.module.scss'; + +export type SearchFormProps = { + /** + * Should the label be visually hidden? + * + * @default false + */ + isLabelHidden?: boolean; + /** + * The search page url. + */ + searchPage: string; +}; + +const SearchFormWithRef: ForwardRefRenderFunction< + HTMLInputElement, + SearchFormProps +> = ({ isLabelHidden = false, 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 = (e: FormEvent) => { + e.preventDefault(); + router.push({ pathname: searchPage, query: { s: value } }); + setValue(''); + }; + + const updateForm = (e: ChangeEvent<HTMLInputElement>) => { + setValue(e.target.value); + }; + + const id = useId(); + + return ( + <Form className={styles.wrapper} onSubmit={submitHandler}> + <LabelledField + className={styles.field} + field={ + <Input + className={styles.field} + id={`search-form-${id}`} + name="search-form" + onChange={updateForm} + ref={ref} + type="search" + value={value} + /> + } + label={ + <Label htmlFor={`search-form-${id}`} isHidden={isLabelHidden}> + {fieldLabel} + </Label> + } + /> + <Button + aria-label={buttonLabel} + className={styles.btn} + kind="neutral" + shape="initial" + type="submit" + > + <MagnifyingGlass className={styles.btn__icon} /> + </Button> + </Form> + ); +}; + +/** + * SearchForm component + * + * Render a search form. + */ +export const SearchForm = forwardRef(SearchFormWithRef); |
