aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils
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/utils
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/utils')
-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
5 files changed, 121 insertions, 38 deletions
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]);
-};