diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-02 15:36:21 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 655be4404630a20ae4ca40c4af84afcc2e63557b (patch) | |
| tree | b69cf241cfb4df7042f50be6a56b7c5f4414edd0 /src | |
| parent | e2daf7f81789c54b23ade72bd164492e7304d375 (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.tsx | 11 | ||||
| -rw-r--r-- | src/utils/hooks/index.ts | 2 | ||||
| -rw-r--r-- | src/utils/hooks/use-autofocus/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-autofocus/use-autofocus.test.ts | 79 | ||||
| -rw-r--r-- | src/utils/hooks/use-autofocus/use-autofocus.ts | 40 | ||||
| -rw-r--r-- | src/utils/hooks/use-input-autofocus.tsx | 37 |
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]); -}; |
