diff --git a/.changeset/yellow-experts-beam.md b/.changeset/yellow-experts-beam.md new file mode 100644 index 0000000..85c3daf --- /dev/null +++ b/.changeset/yellow-experts-beam.md @@ -0,0 +1,5 @@ +--- +'@raddix/use-scroll-position': major +--- + +Added the useScrollPosition hook diff --git a/packages/hooks/use-scroll-position/README.md b/packages/hooks/use-scroll-position/README.md new file mode 100644 index 0000000..d722f1b --- /dev/null +++ b/packages/hooks/use-scroll-position/README.md @@ -0,0 +1,5 @@ +# useScrollPosition + +The `useScrollPosition` hook listens for the scroll position of the current window or element. + +Please refer to the [documentation](https://www.raddix.website/docs/use-scroll-position) for more information. diff --git a/packages/hooks/use-scroll-position/package.json b/packages/hooks/use-scroll-position/package.json new file mode 100644 index 0000000..26754df --- /dev/null +++ b/packages/hooks/use-scroll-position/package.json @@ -0,0 +1,47 @@ +{ + "name": "@raddix/use-scroll-position", + "description": "A hook to listen to the scroll position of the current window or element.", + "version": "0.1.0", + "license": "MIT", + "main": "src/index.ts", + "author": "Moises Machuca Valverde (https://www.moisesmachuca.com)", + "homepage": "https://www.raddix.website", + "repository": { + "type": "git", + "url": "https://github.com/gdvu/raddix.git" + }, + "keywords": [ + "react-hook", + "react-scroll-position-hook", + "react-use-scroll-position", + "use-scroll-position", + "use-scroll-position-hook", + "hook-scroll-position" + ], + "sideEffects": false, + "scripts": { + "lint": "eslint \"{src,tests}/*.{ts,tsx,css}\"", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", + "build": "tsup src --dts", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "files": [ + "dist", + "README.md" + ], + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "@raddix/use-event-listener": "workspace:*" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} diff --git a/packages/hooks/use-scroll-position/src/index.ts b/packages/hooks/use-scroll-position/src/index.ts new file mode 100644 index 0000000..aed9439 --- /dev/null +++ b/packages/hooks/use-scroll-position/src/index.ts @@ -0,0 +1,40 @@ +import { type RefObject, useEffect, useState } from 'react'; +import { useEventListener, _document } from '@raddix/use-event-listener'; + +export interface ScrollPosition { + x: number | null; + y: number | null; +} + +export interface Options { + target?: RefObject | Document | null; +} + +export const useScroll = ({ + target = _document +}: Options = {}): ScrollPosition => { + const [scrollPosition, setScrollPosition] = useState({ + x: 0, + y: 0 + }); + + const handle = () => { + if (!target) return; + const targetElement = + target instanceof Document ? document.documentElement : target.current; + + setScrollPosition({ + x: targetElement?.scrollLeft ?? null, + y: targetElement?.scrollTop ?? null + }); + }; + + useEffect(() => { + handle(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEventListener(target, 'scroll', handle, { passive: true }); + + return scrollPosition; +}; diff --git a/packages/hooks/use-scroll-position/tests/use-scroll-position.test.ts b/packages/hooks/use-scroll-position/tests/use-scroll-position.test.ts new file mode 100644 index 0000000..af66408 --- /dev/null +++ b/packages/hooks/use-scroll-position/tests/use-scroll-position.test.ts @@ -0,0 +1,74 @@ +import { renderHook, act } from '@testing-library/react'; +import { useScroll } from '../src'; + +describe('useScroll test:', () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + jest.clearAllMocks(); + }); + + it('should initialize scroll position to (0, 0)', () => { + const { result } = renderHook(() => useScroll()); + + expect(result.current).toEqual({ x: 0, y: 0 }); + }); + + it('should update scroll position on scroll event', () => { + const { result } = renderHook(() => + useScroll({ target: { current: container } }) + ); + + expect(result.current).toEqual({ x: 0, y: 0 }); + + act(() => { + container.scrollTop = 100; + container.scrollLeft = 50; + container.dispatchEvent(new Event('scroll')); + }); + + expect(result.current).toEqual({ x: 50, y: 100 }); + }); + + it('should update scroll position on document scroll event', () => { + const { result } = renderHook(() => useScroll()); + + expect(result.current).toEqual({ x: 0, y: 0 }); + + act(() => { + document.documentElement.scrollTop = 100; + document.documentElement.scrollLeft = 50; + document.dispatchEvent(new Event('scroll')); + }); + + expect(result.current).toEqual({ x: 50, y: 100 }); + }); + + it('should not update scroll position if target is null', () => { + const { result } = renderHook(() => useScroll({ target: null })); + + expect(result.current).toEqual({ x: 0, y: 0 }); + + act(() => { + document.documentElement.scrollTop = 100; + document.documentElement.scrollLeft = 50; + document.dispatchEvent(new Event('scroll')); + }); + + expect(result.current).toEqual({ x: 0, y: 0 }); + }); + + it('should not update the scroll position if the element does not exist', () => { + const { result } = renderHook(() => + useScroll({ target: { current: null } }) + ); + + expect(result.current).toEqual({ x: null, y: null }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a149b67..93c5952 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,6 +268,18 @@ importers: specifier: '>=16.8.0' version: 18.2.0(react@18.2.0) + packages/hooks/use-scroll-position: + dependencies: + '@raddix/use-event-listener': + specifier: workspace:* + version: link:../use-event-listener + react: + specifier: '>=16.8.0' + version: 18.2.0 + react-dom: + specifier: '>=16.8.0' + version: 18.2.0(react@18.2.0) + packages/hooks/use-scroll-spy: dependencies: react: