diff --git a/src/hooks/__tests__/useScroll.test.ts b/src/hooks/__tests__/useScroll.test.ts new file mode 100644 index 0000000..390911e --- /dev/null +++ b/src/hooks/__tests__/useScroll.test.ts @@ -0,0 +1,49 @@ +import { act, renderHook } from '@testing-library/react'; +import useScroll from '../useScroll'; + +describe('useScroll hook', () => { + it('should monitor and update scroll position for the window', async () => { + const { result } = renderHook(() => useScroll()); + + expect(result.current[0]).toEqual({ x: 0, y: 0 }); + + act(() => { + window.scrollX = 100; + window.scrollY = 200; + window.dispatchEvent(new Event('scroll')); + }); + + expect(result.current[0]).toEqual({ x: 100, y: 200 }); + }); + + it('should monitor and update scroll position for a specific DOM element', async () => { + const element = document.createElement('div'); + const { result } = renderHook(() => useScroll({ current: element })); + + expect(result.current[0]).toEqual({ x: 0, y: 0 }); + + act(() => { + element.scrollLeft = 150; + element.scrollTop = 250; + element.dispatchEvent(new Event('scroll')); + }); + + expect(result.current[0]).toEqual({ x: 150, y: 250 }); + }); + + it('should monitor and update scroll position after setting the monitored element', async () => { + const element = document.createElement('main'); + const { result } = renderHook(() => useScroll()); + + expect(result.current[0]).toEqual({ x: 0, y: 0 }); + + act(() => { + result.current[1](element); + element.scrollLeft = 180; + element.scrollTop = 280; + element.dispatchEvent(new Event('scroll')); + }); + + expect(result.current[0]).toEqual({ x: 180, y: 280 }); + }); +}); diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts new file mode 100644 index 0000000..44a2001 --- /dev/null +++ b/src/hooks/useScroll.ts @@ -0,0 +1,45 @@ +import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; + +type ScrollElement = HTMLElement | Window | null; + +/** + * This hook allows tracking the scroll position of a specified DOM element or the window. + */ +function useScroll(ref?: RefObject) { + const [scroll, setScroll] = useState({ x: 0, y: 0 }); + const internalElementRef = useRef(null); + + const handleScroll = useCallback(() => { + const element = internalElementRef.current; + + if (element === window) { + setScroll({ x: element.scrollX, y: element.scrollY }); + } else if (element instanceof Element) { + setScroll({ x: element.scrollLeft, y: element.scrollTop }); + } + }, []); + + const setInternalElementRef = useCallback( + (element: ScrollElement) => { + internalElementRef.current?.removeEventListener('scroll', handleScroll); + internalElementRef.current = element; + internalElementRef.current?.addEventListener('scroll', handleScroll); + handleScroll(); + }, + [handleScroll] + ); + + useEffect(() => { + internalElementRef.current = ref?.current || window; + + const element = internalElementRef.current; + + element.addEventListener('scroll', handleScroll); + + return () => element.removeEventListener('scroll', handleScroll); + }, [ref, handleScroll]); + + return [scroll, setInternalElementRef] as const; +} + +export default useScroll;