aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-02 15:36:21 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commit655be4404630a20ae4ca40c4af84afcc2e63557b (patch)
treeb69cf241cfb4df7042f50be6a56b7c5f4414edd0 /src
parente2daf7f81789c54b23ade72bd164492e7304d375 (diff)
refactor(hooks): replace useInputAutofocus with useAutofocus hook
* extract setTimeout logic using useTimeout * change condition to be a function * return a ref
Diffstat (limited to 'src')
-rw-r--r--src/components/organisms/toolbar/search.tsx11
-rw-r--r--src/utils/hooks/index.ts2
-rw-r--r--src/utils/hooks/use-autofocus/index.ts1
-rw-r--r--src/utils/hooks/use-autofocus/use-autofocus.test.ts79
-rw-r--r--src/utils/hooks/use-autofocus/use-autofocus.ts40
-rw-r--r--src/utils/hooks/use-input-autofocus.tsx37
6 files changed, 125 insertions, 45 deletions
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index 6a33aff..4429770 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -1,8 +1,7 @@
-import { forwardRef, type ForwardRefRenderFunction, useRef } from 'react';
+import { forwardRef, type ForwardRefRenderFunction } from 'react';
import { useIntl } from 'react-intl';
-import { useInputAutofocus } from '../../../utils/hooks';
+import { useAutofocus } from '../../../utils/hooks';
import { BooleanField, type BooleanFieldProps, Icon } from '../../atoms';
-import { FlippingLabel } from '../../molecules';
import { SearchModal, type SearchModalProps } from '../modals';
import searchStyles from './search.module.scss';
import sharedStyles from './toolbar-items.module.scss';
@@ -43,11 +42,9 @@ const SearchWithRef: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = (
description: 'Search: Open label',
});
- const searchInputRef = useRef<HTMLInputElement>(null);
- useInputAutofocus({
- condition: isActive,
+ const searchInputRef = useAutofocus<HTMLInputElement>({
+ condition: () => isActive,
delay: 360,
- ref: searchInputRef,
});
return (
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts
index f1063ce..9181b6a 100644
--- a/src/utils/hooks/index.ts
+++ b/src/utils/hooks/index.ts
@@ -1,12 +1,12 @@
export * from './use-ackee';
export * from './use-article';
+export * from './use-autofocus';
export * from './use-boolean';
export * from './use-breadcrumb';
export * from './use-comments';
export * from './use-data-from-api';
export * from './use-github-api';
export * from './use-headings-tree';
-export * from './use-input-autofocus';
export * from './use-is-mounted';
export * from './use-local-storage';
export * from './use-match-media';
diff --git a/src/utils/hooks/use-autofocus/index.ts b/src/utils/hooks/use-autofocus/index.ts
new file mode 100644
index 0000000..bb23089
--- /dev/null
+++ b/src/utils/hooks/use-autofocus/index.ts
@@ -0,0 +1 @@
+export * from './use-autofocus';
diff --git a/src/utils/hooks/use-autofocus/use-autofocus.test.ts b/src/utils/hooks/use-autofocus/use-autofocus.test.ts
new file mode 100644
index 0000000..1a9a3be
--- /dev/null
+++ b/src/utils/hooks/use-autofocus/use-autofocus.test.ts
@@ -0,0 +1,79 @@
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ jest,
+} from '@jest/globals';
+import { renderHook, screen as rtlScreen } from '@testing-library/react';
+import { useAutofocus } from './use-autofocus';
+
+describe('useAutofocus', () => {
+ // When less than 1ms, setTimeout use 1. Default delay is 0ms.
+ const defaultTimeoutDelay = 1;
+ const input = document.createElement('input');
+ input.type = 'text';
+
+ beforeEach(() => {
+ document.body.append(input);
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ document.body.removeChild(input);
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('gives focus to the element without condition', () => {
+ const { result } = renderHook(() => useAutofocus<HTMLInputElement>());
+ result.current.current = input;
+
+ jest.advanceTimersByTime(defaultTimeoutDelay);
+
+ expect(rtlScreen.getByRole('textbox')).toHaveFocus();
+ });
+
+ it('can give focus to the element with custom delay', () => {
+ const delay = 2000;
+ const { result } = renderHook(() =>
+ useAutofocus<HTMLInputElement>({ delay })
+ );
+ result.current.current = input;
+
+ jest.advanceTimersByTime(defaultTimeoutDelay);
+
+ expect(rtlScreen.getByRole('textbox')).not.toHaveFocus();
+
+ jest.advanceTimersByTime(delay);
+
+ expect(rtlScreen.getByRole('textbox')).toHaveFocus();
+ });
+
+ it('can give focus to the element when the condition is met', () => {
+ const condition = jest.fn(() => true);
+ const { result } = renderHook(() =>
+ useAutofocus<HTMLInputElement>({ condition })
+ );
+ result.current.current = input;
+
+ jest.advanceTimersByTime(defaultTimeoutDelay);
+
+ expect(rtlScreen.getByRole('textbox')).toHaveFocus();
+ expect(condition).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not give focus to the element when the condition is not met', () => {
+ const condition = jest.fn(() => false);
+ const { result } = renderHook(() =>
+ useAutofocus<HTMLInputElement>({ condition })
+ );
+ result.current.current = input;
+
+ jest.advanceTimersByTime(defaultTimeoutDelay);
+
+ expect(rtlScreen.getByRole('textbox')).not.toHaveFocus();
+ expect(condition).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/utils/hooks/use-autofocus/use-autofocus.ts b/src/utils/hooks/use-autofocus/use-autofocus.ts
new file mode 100644
index 0000000..0d21a59
--- /dev/null
+++ b/src/utils/hooks/use-autofocus/use-autofocus.ts
@@ -0,0 +1,40 @@
+import { useCallback, useRef, type MutableRefObject } from 'react';
+import { useTimeout } from '../use-timeout';
+
+export type UseAutofocusCondition = () => boolean;
+
+export type UseAutofocusConfig = {
+ /**
+ * A condition to met before giving focus to the element.
+ */
+ condition?: UseAutofocusCondition;
+ /**
+ * A delay in ms before giving focus to the element.
+ */
+ delay?: number;
+};
+
+/**
+ * React hook to give focus to an element automatically.
+ *
+ * @param {UseAutofocusConfig} [config] - A configuration object.
+ * @returns {RefObject<T>} The element reference.
+ */
+export const useAutofocus = <T extends HTMLElement>(
+ config?: UseAutofocusConfig
+): MutableRefObject<T | null> => {
+ const { condition, delay } = config ?? {};
+ const ref = useRef<T | null>(null);
+
+ const setFocus = useCallback(() => {
+ const shouldFocus = condition ? condition() : true;
+
+ if (ref.current && shouldFocus) {
+ ref.current.focus();
+ }
+ }, [condition]);
+
+ useTimeout(setFocus, delay);
+
+ return ref;
+};
diff --git a/src/utils/hooks/use-input-autofocus.tsx b/src/utils/hooks/use-input-autofocus.tsx
deleted file mode 100644
index d0fcd06..0000000
--- a/src/utils/hooks/use-input-autofocus.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-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.
- */
-export 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]);
-};