diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-01 16:10:31 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 1e4b48aa075e6131a7244cd4726ddb5ba75fcecf (patch) | |
| tree | ba0377440419f6690f079c1fc9de5915db805a38 /src/utils/hooks/use-on-click-outside | |
| parent | 2844a2bd71dcf1eb17a53992c10129b7496332e0 (diff) | |
refactor(hooks): rewrite useOnClickOutside hook
* remove `useCapture` parameter (it does not make sense to use bubbling
here)
* return a MutableRefObject instead of a RefObject to be able to test
the hook
Diffstat (limited to 'src/utils/hooks/use-on-click-outside')
3 files changed, 81 insertions, 0 deletions
diff --git a/src/utils/hooks/use-on-click-outside/index.ts b/src/utils/hooks/use-on-click-outside/index.ts new file mode 100644 index 0000000..b9fff06 --- /dev/null +++ b/src/utils/hooks/use-on-click-outside/index.ts @@ -0,0 +1 @@ +export * from './use-on-click-outside'; diff --git a/src/utils/hooks/use-on-click-outside/use-on-click-outside.test.ts b/src/utils/hooks/use-on-click-outside/use-on-click-outside.test.ts new file mode 100644 index 0000000..982c0ee --- /dev/null +++ b/src/utils/hooks/use-on-click-outside/use-on-click-outside.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { renderHook } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { useOnClickOutside } from './use-on-click-outside'; + +describe('useOnClickOutside', () => { + it('can execute a function on click outside the given ref', async () => { + const user = userEvent.setup(); + const cb = jest.fn(); + const wrapper = document.createElement('div'); + const el = document.createElement('div'); + + wrapper.append(el); + document.body.append(wrapper); + + const { result } = renderHook(() => useOnClickOutside<HTMLDivElement>(cb)); + + result.current.current = el; + + await user.click(wrapper); + + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('does not execute the callback on click inside the given ref', async () => { + const user = userEvent.setup(); + const cb = jest.fn(); + const wrapper = document.createElement('div'); + const el = document.createElement('div'); + + wrapper.append(el); + document.body.append(wrapper); + + const { result } = renderHook(() => useOnClickOutside(cb)); + + result.current.current = wrapper; + + await user.click(el); + + expect(cb).not.toHaveBeenCalled(); + }); +}); diff --git a/src/utils/hooks/use-on-click-outside/use-on-click-outside.ts b/src/utils/hooks/use-on-click-outside/use-on-click-outside.ts new file mode 100644 index 0000000..c9a1132 --- /dev/null +++ b/src/utils/hooks/use-on-click-outside/use-on-click-outside.ts @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useRef, type MutableRefObject } from 'react'; + +export type useOnClickOutsideHandler = (ev: MouseEvent | FocusEvent) => void; + +/** + * Detect clicks (or focus) outside a component. + * + * @param {useOnClickOutsideHandler} [handler] - A function to handle the event. + * @returns {RefObject<T>} A ref object. + */ +export const useOnClickOutside = <T extends HTMLElement>( + handler?: useOnClickOutsideHandler +): MutableRefObject<T | null> => { + const ref = useRef<T | null>(null); + + const listener = useCallback( + (ev: MouseEvent | FocusEvent) => { + if (!handler || !ref.current || ref.current.contains(ev.target as Node)) { + return; + } + + handler(ev); + }, + [handler] + ); + + useEffect(() => { + document.addEventListener('click', listener, true); + document.addEventListener('focusin', listener, true); + + return () => { + document.removeEventListener('click', listener, true); + document.removeEventListener('focusin', listener, true); + }; + }, [listener]); + + return ref; +}; |
