aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/forms/search-form
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-22 19:34:01 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:23:48 +0200
commita6ff5eee45215effb3344cb5d631a27a7c0369aa (patch)
tree5051747acf72318b4fc5c18d603e3757fbefdfdb /src/components/organisms/forms/search-form
parent651ea4fc992e77d2f36b3c68f8e7a70644246067 (diff)
refactor(components): rewrite form components
Diffstat (limited to 'src/components/organisms/forms/search-form')
-rw-r--r--src/components/organisms/forms/search-form/index.ts1
-rw-r--r--src/components/organisms/forms/search-form/search-form.module.scss67
-rw-r--r--src/components/organisms/forms/search-form/search-form.stories.tsx65
-rw-r--r--src/components/organisms/forms/search-form/search-form.test.tsx16
-rw-r--r--src/components/organisms/forms/search-form/search-form.tsx98
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);