From 655be4404630a20ae4ca40c4af84afcc2e63557b Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Thu, 2 Nov 2023 15:36:21 +0100 Subject: refactor(hooks): replace useInputAutofocus with useAutofocus hook * extract setTimeout logic using useTimeout * change condition to be a function * return a ref --- src/utils/hooks/use-autofocus/index.ts | 1 + .../hooks/use-autofocus/use-autofocus.test.ts | 79 ++++++++++++++++++++++ src/utils/hooks/use-autofocus/use-autofocus.ts | 40 +++++++++++ 3 files changed, 120 insertions(+) create mode 100644 src/utils/hooks/use-autofocus/index.ts create mode 100644 src/utils/hooks/use-autofocus/use-autofocus.test.ts create mode 100644 src/utils/hooks/use-autofocus/use-autofocus.ts (limited to 'src/utils/hooks/use-autofocus') 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()); + 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({ 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({ 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({ 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} The element reference. + */ +export const useAutofocus = ( + config?: UseAutofocusConfig +): MutableRefObject => { + const { condition, delay } = config ?? {}; + const ref = useRef(null); + + const setFocus = useCallback(() => { + const shouldFocus = condition ? condition() : true; + + if (ref.current && shouldFocus) { + ref.current.focus(); + } + }, [condition]); + + useTimeout(setFocus, delay); + + return ref; +}; -- cgit v1.2.3