summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-24 17:54:23 +0200
committerArmand Philippot <git@armandphilippot.com>2022-05-24 17:54:23 +0200
commit691646c97b09f9150ac823670d6c661358c81c1c (patch)
treecc2115a23d9dae87b7cdfab9223f1366aa629c69 /src
parent041fb0974f624368a45316c296c2a3e3c229dae2 (diff)
chore: give autofocus to the toolbar search form
Diffstat (limited to 'src')
-rw-r--r--src/components/atoms/forms/field.tsx24
-rw-r--r--src/components/molecules/forms/labelled-field.tsx21
-rw-r--r--src/components/organisms/forms/search-form.tsx24
-rw-r--r--src/components/organisms/modals/search-modal.tsx25
-rw-r--r--src/components/organisms/toolbar/search.tsx13
-rw-r--r--src/utils/hooks/use-input-autofocus.tsx39
6 files changed, 98 insertions, 48 deletions
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
index e45a8a7..377e1b0 100644
--- a/src/components/atoms/forms/field.tsx
+++ b/src/components/atoms/forms/field.tsx
@@ -1,4 +1,9 @@
-import { ChangeEvent, FC, SetStateAction } from 'react';
+import {
+ ChangeEvent,
+ forwardRef,
+ ForwardRefRenderFunction,
+ SetStateAction,
+} from 'react';
import styles from './forms.module.scss';
export type FieldType =
@@ -72,12 +77,10 @@ export type FieldProps = {
*
* Render either an input or a textarea.
*/
-const Field: FC<FieldProps> = ({
- className = '',
- setValue,
- type,
- ...props
-}) => {
+const Field: ForwardRefRenderFunction<HTMLInputElement, FieldProps> = (
+ { className = '', setValue, type, ...props },
+ ref
+) => {
/**
* Update select value when an option is selected.
* @param e - The option change event.
@@ -96,12 +99,13 @@ const Field: FC<FieldProps> = ({
/>
) : (
<input
- type={type}
- onChange={updateValue}
className={`${styles.field} ${className}`}
+ onChange={updateValue}
+ ref={ref}
+ type={type}
{...props}
/>
);
};
-export default Field;
+export default forwardRef(Field);
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index ecc9255..6a00a3e 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,6 +1,6 @@
import Field, { type FieldProps } from '@components/atoms/forms/field';
import Label from '@components/atoms/forms/label';
-import { FC } from 'react';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
import styles from './labelled-field.module.scss';
export type LabelledFieldProps = FieldProps & {
@@ -23,14 +23,13 @@ export type LabelledFieldProps = FieldProps & {
*
* Render a field tied to a label.
*/
-const LabelledField: FC<LabelledFieldProps> = ({
- hideLabel = false,
- id,
- label,
- labelPosition = 'top',
- required,
- ...props
-}) => {
+const LabelledField: ForwardRefRenderFunction<
+ HTMLInputElement,
+ LabelledFieldProps
+> = (
+ { hideLabel = false, id, label, labelPosition = 'top', required, ...props },
+ ref
+) => {
const positionModifier = `label--${labelPosition}`;
const visibilityClass = hideLabel ? 'screen-reader-text' : '';
@@ -43,9 +42,9 @@ const LabelledField: FC<LabelledFieldProps> = ({
>
{label}
</Label>
- <Field id={id} required={required} {...props} />
+ <Field id={id} ref={ref} required={required} {...props} />
</>
);
};
-export default LabelledField;
+export default forwardRef(LabelledField);
diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx
index 56d3895..1b5f662 100644
--- a/src/components/organisms/forms/search-form.tsx
+++ b/src/components/organisms/forms/search-form.tsx
@@ -5,7 +5,7 @@ import LabelledField, {
type LabelledFieldProps,
} from '@components/molecules/forms/labelled-field';
import { useRouter } from 'next/router';
-import { FC, useState } from 'react';
+import { forwardRef, ForwardRefRenderFunction, useId, useState } from 'react';
import { useIntl } from 'react-intl';
import styles from './search-form.module.scss';
@@ -21,7 +21,10 @@ export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'> & {
*
* Render a search form.
*/
-const SearchForm: FC<SearchFormProps> = ({ hideLabel, searchPage }) => {
+const SearchForm: ForwardRefRenderFunction<
+ HTMLInputElement,
+ SearchFormProps
+> = ({ hideLabel, searchPage }, ref) => {
const intl = useIntl();
const fieldLabel = intl.formatMessage({
defaultMessage: 'Search for:',
@@ -42,17 +45,20 @@ const SearchForm: FC<SearchFormProps> = ({ hideLabel, searchPage }) => {
setValue('');
};
+ const id = useId();
+
return (
<Form grouped={false} onSubmit={submitHandler} className={styles.wrapper}>
<LabelledField
- type="search"
- id="search-form"
- name="search-form"
+ className={styles.field}
+ hideLabel={hideLabel}
+ id={`search-form-${id}`}
label={fieldLabel}
- value={value}
+ name="search-form"
+ ref={ref}
setValue={setValue}
- hideLabel={hideLabel}
- className={styles.field}
+ type="search"
+ value={value}
/>
<Button
type="submit"
@@ -67,4 +73,4 @@ const SearchForm: FC<SearchFormProps> = ({ hideLabel, searchPage }) => {
);
};
-export default SearchForm;
+export default forwardRef(SearchForm);
diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx
index c731048..ed6084a 100644
--- a/src/components/organisms/modals/search-modal.tsx
+++ b/src/components/organisms/modals/search-modal.tsx
@@ -1,20 +1,10 @@
-import Spinner from '@components/atoms/loaders/spinner';
import Modal, { type ModalProps } from '@components/molecules/modals/modal';
-import dynamic from 'next/dynamic';
-import { FC } from 'react';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
import { useIntl } from 'react-intl';
-import { type SearchFormProps } from '../forms/search-form';
+import SearchForm, { type SearchFormProps } from '../forms/search-form';
import styles from './search-modal.module.scss';
-const DynamicSearchForm = dynamic(
- () => import('@components/organisms/forms/search-form'),
- {
- loading: () => <Spinner />,
- ssr: false,
- }
-);
-
-export type SearchModalProps = Pick<SearchFormProps, 'searchPage'> & {
+export type SearchModalProps = SearchFormProps & {
/**
* Set additional classnames to modal wrapper.
*/
@@ -26,7 +16,10 @@ export type SearchModalProps = Pick<SearchFormProps, 'searchPage'> & {
*
* Render a search form modal.
*/
-const SearchModal: FC<SearchModalProps> = ({ className, searchPage }) => {
+const SearchModal: ForwardRefRenderFunction<
+ HTMLInputElement,
+ SearchModalProps
+> = ({ className, searchPage }, ref) => {
const intl = useIntl();
const modalTitle = intl.formatMessage({
defaultMessage: 'Search',
@@ -36,9 +29,9 @@ const SearchModal: FC<SearchModalProps> = ({ className, searchPage }) => {
return (
<Modal title={modalTitle} className={`${styles.wrapper} ${className}`}>
- <DynamicSearchForm hideLabel={true} searchPage={searchPage} />
+ <SearchForm hideLabel={true} ref={ref} searchPage={searchPage} />
</Modal>
);
};
-export default SearchModal;
+export default forwardRef(SearchModal);
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index dc71c49..6a8af26 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -1,7 +1,8 @@
import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
import FlippingLabel from '@components/molecules/forms/flipping-label';
-import { forwardRef, ForwardRefRenderFunction } from 'react';
+import useInputAutofocus from '@utils/hooks/use-input-autofocus';
+import { forwardRef, ForwardRefRenderFunction, useRef } from 'react';
import { useIntl } from 'react-intl';
import SearchModal, { type SearchModalProps } from '../modals/search-modal';
import searchStyles from './search.module.scss';
@@ -43,6 +44,13 @@ const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = (
description: 'Search: Open label',
});
+ const searchInputRef = useRef<HTMLInputElement>(null);
+ useInputAutofocus({
+ condition: isActive,
+ delay: 360,
+ ref: searchInputRef,
+ });
+
return (
<div className={`${sharedStyles.item} ${searchStyles.item}`} ref={ref}>
<Checkbox
@@ -61,8 +69,9 @@ const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = (
<MagnifyingGlass />
</FlippingLabel>
<SearchModal
- searchPage={searchPage}
className={`${sharedStyles.modal} ${searchStyles.modal} ${className}`}
+ ref={searchInputRef}
+ searchPage={searchPage}
/>
</div>
);
diff --git a/src/utils/hooks/use-input-autofocus.tsx b/src/utils/hooks/use-input-autofocus.tsx
new file mode 100644
index 0000000..c7700e9
--- /dev/null
+++ b/src/utils/hooks/use-input-autofocus.tsx
@@ -0,0 +1,39 @@
+import { RefObject, useEffect } from 'react';
+
+export type UseInputAutofocusProps = {
+ /**
+ * The focus condition. True give focus to the input.
+ */
+ condition: boolean;
+ /**
+ * An optional delay. Default: 0.
+ */
+ delay?: number;
+ /**
+ * A reference to the input element.
+ */
+ ref: RefObject<HTMLInputElement>;
+};
+
+/**
+ * Set focus on an input with an optional delay.
+ */
+const useInputAutofocus = ({
+ condition,
+ delay = 0,
+ ref,
+}: UseInputAutofocusProps) => {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (ref.current && condition) {
+ ref.current.focus();
+ }
+ }, delay);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [condition, delay, ref]);
+};
+
+export default useInputAutofocus;