diff --git a/.changeset/forty-tables-drive.md b/.changeset/forty-tables-drive.md new file mode 100644 index 0000000..179ce61 --- /dev/null +++ b/.changeset/forty-tables-drive.md @@ -0,0 +1,5 @@ +--- +'@raddix/use-scroll-lock': major +--- + +Added useScrollLock hook. diff --git a/.changeset/six-students-run.md b/.changeset/six-students-run.md new file mode 100644 index 0000000..08bb74e --- /dev/null +++ b/.changeset/six-students-run.md @@ -0,0 +1,5 @@ +--- +'@raddix/use-clipboard': minor +--- + +Remove @raddix/use-timeout of peerDependecies diff --git a/.changeset/tasty-boats-tease.md b/.changeset/tasty-boats-tease.md new file mode 100644 index 0000000..03e7601 --- /dev/null +++ b/.changeset/tasty-boats-tease.md @@ -0,0 +1,6 @@ +--- +'@raddix/use-scroll-position': minor +'@raddix/use-window-size': minor +--- + +Remove @raddix/use-event-listener of peerDependencies diff --git a/docs/_config.json b/docs/_config.json index 2f9f8cf..6cb1e30 100644 --- a/docs/_config.json +++ b/docs/_config.json @@ -28,10 +28,18 @@ "title": "useMediaQuery", "path": "/hooks/use-media-query" }, + { + "title": "useScrollLock", + "path": "/hooks/use-scroll-lock" + }, { "title": "useScrollPosition", "path": "/hooks/use-scroll-position" }, + { + "title": "useScrollSpy", + "path": "/hooks/use-scroll-spy" + }, { "title": "useWindowSize", "path": "/hooks/use-window-size" @@ -64,6 +72,10 @@ "title": "useCounter", "path": "/hooks/use-counter" }, + { + "title": "useCountDown", + "path": "/hooks/use-count-down" + }, { "title": "useDebounce", "path": "/hooks/use-debounce" @@ -103,20 +115,6 @@ "path": "/hooks/use-isomorphic-effect" } ] - }, - { - "title": "Utilities", - "heading": true, - "children": [ - { - "title": "useCountDown", - "path": "/hooks/use-count-down" - }, - { - "title": "useScrollSpy", - "path": "/hooks/use-scroll-spy" - } - ] } ] } diff --git a/docs/en/use-scroll-lock.mdx b/docs/en/use-scroll-lock.mdx new file mode 100644 index 0000000..178a7a1 --- /dev/null +++ b/docs/en/use-scroll-lock.mdx @@ -0,0 +1,56 @@ +--- +title: useScrollLock +description: Disables scrolling in the document body. +--- + +## Features + +- Removes the scroll bar from the document, while preserving the width of the page. +- Works on any desktop or mobile browser. + +## Installation + +Install the custom hook from your command line. + + + +## Usage + +Once the component using the `useScrollLock` hook is mounted, scrolling is disabled +in the document body. When the component is unmounted, the hook returns a cleanup function +that restores the original overflow style. + +```jsx +import { useState } from 'react'; +import { useScrollLock } from '@raddix/use-scroll-lock'; + +function Modal({ handleClose }) { + useScrollLock(); + + return ( + + +

Modal

+
+ ) +} + +export default function App() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + {isOpen && setIsOpen(false)} />} + + + ); +} +``` + +## API + +### Parameters + +The `useScrollLock` hook does not accept any parameters. \ No newline at end of file diff --git a/docs/es/use-scroll-lock.mdx b/docs/es/use-scroll-lock.mdx new file mode 100644 index 0000000..95fbe3b --- /dev/null +++ b/docs/es/use-scroll-lock.mdx @@ -0,0 +1,56 @@ +--- +title: useScrollLock +description: Deshabilita el desplazamiento en el cuerpo del documento. +--- + +## Características + +- Elimina la barra de desplazamiento del documento, conservando el ancho de la página. +- Funciona en cualquier navegador de escritorio o móvil. + +## Instalación + +Instala el custom hook desde su linea de comando. + + + +## Uso + +Una vez que el componente que utiliza el hook `useScrollLock` se monta, se deshabilita el desplazamiento +en el cuerpo del documento. Cuando se desmonta el componente el hook devuelve una función de limpieza +que restaura el estilo de desbordamiento original. + +```jsx +import { useState } from 'react'; +import { useScrollLock } from '@raddix/use-scroll-lock'; + +function Modal({ handleClose }) { + useScrollLock(); + + return ( + + +

Modal

+
+ ) +} + +export default function App() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + {isOpen && setIsOpen(false)} />} + + + ); +} +``` + +## API + +### Parámetros + +El hook `useScrollLock` no acepta ningun parámetro. \ No newline at end of file diff --git a/packages/hooks/use-clipboard/package.json b/packages/hooks/use-clipboard/package.json index 738614e..b811b7c 100644 --- a/packages/hooks/use-clipboard/package.json +++ b/packages/hooks/use-clipboard/package.json @@ -33,8 +33,7 @@ ], "peerDependencies": { "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "@raddix/use-timeout": "workspace:*" + "react-dom": ">=16.8.0" }, "clean-package": "../../../clean-package.config.json", "tsup": { diff --git a/packages/hooks/use-clipboard/src/index.ts b/packages/hooks/use-clipboard/src/index.ts index e83573a..08113d5 100644 --- a/packages/hooks/use-clipboard/src/index.ts +++ b/packages/hooks/use-clipboard/src/index.ts @@ -1,5 +1,4 @@ -import { useTimeout } from '@raddix/use-timeout'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; export type UseClipboard = (options?: { timeout?: number; @@ -10,7 +9,7 @@ export type UseClipboard = (options?: { export const useClipboard: UseClipboard = (options = {}) => { const { timeout = 2000, onError, onSuccess } = options; const [isCopied, setIsCopied] = useState(false); - const { run } = useTimeout(() => setIsCopied(false), timeout, false); + const idTimeout = useRef(null); const copy = (data: string) => { if ('clipboard' in navigator) { @@ -19,7 +18,7 @@ export const useClipboard: UseClipboard = (options = {}) => { .then(() => { setIsCopied(true); onSuccess?.(); - run(); + idTimeout.current = setTimeout(() => setIsCopied(false), timeout); }) .catch(err => onError?.(err)); } else { @@ -27,5 +26,11 @@ export const useClipboard: UseClipboard = (options = {}) => { } }; + useEffect(() => { + return () => { + if (idTimeout.current) clearTimeout(idTimeout.current); + }; + }, []); + return [isCopied, copy]; }; diff --git a/packages/hooks/use-scroll-lock/README.md b/packages/hooks/use-scroll-lock/README.md new file mode 100644 index 0000000..82ab538 --- /dev/null +++ b/packages/hooks/use-scroll-lock/README.md @@ -0,0 +1,5 @@ +# useScrollLock + +A hook that locks and unlocks scroll. + +Please refer to the [documentation](https://raddix.dev/hooks/use-scroll-lock) for more information. \ No newline at end of file diff --git a/packages/hooks/use-scroll-lock/package.json b/packages/hooks/use-scroll-lock/package.json new file mode 100644 index 0000000..69a1a86 --- /dev/null +++ b/packages/hooks/use-scroll-lock/package.json @@ -0,0 +1,46 @@ +{ + "name": "@raddix/use-scroll-lock", + "description": "A hook that locks and unlocks scroll.", + "version": "0.1.0", + "license": "MIT", + "main": "src/index.ts", + "author": "Moises Machuca Valverde (https://www.moisesmachuca.com)", + "homepage": "https://raddix.dev", + "repository": { + "type": "git", + "url": "https://github.com/gdvu/raddix.git" + }, + "keywords": [ + "react-hook", + "react-scroll-lock-hook", + "react-use-scroll-lock", + "use-scroll-lock", + "use-scroll-lock-hook", + "hook-scroll-lock" + ], + "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" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} diff --git a/packages/hooks/use-scroll-lock/src/index.ts b/packages/hooks/use-scroll-lock/src/index.ts new file mode 100644 index 0000000..b3645d2 --- /dev/null +++ b/packages/hooks/use-scroll-lock/src/index.ts @@ -0,0 +1,36 @@ +import { useLayoutEffect, useRef } from 'react'; + +interface OriginalStyle { + overflow: string; + paddingRight: string; +} + +export const useScrollLock = (): void => { + const originalStyle = useRef(null); + + const preventTouch = (e: Event) => { + e.preventDefault(); + return false; + }; + + useLayoutEffect(() => { + const scrollbarWidth = window.innerWidth - document.body.scrollWidth; + const paddingRight = window.getComputedStyle(document.body).paddingRight; + const overflow = window.getComputedStyle(document.body).overflow; + const right = scrollbarWidth + parseInt(paddingRight, 10); + + originalStyle.current = { overflow, paddingRight }; + document.body.style.paddingRight = `${right}px`; + document.body.style.overflow = 'hidden'; + document.addEventListener('touchmove', preventTouch, { passive: false }); + + return () => { + if (originalStyle.current) { + document.body.style.overflow = originalStyle.current.overflow; + document.body.style.paddingRight = originalStyle.current.paddingRight; + } + + document.removeEventListener('touchmove', preventTouch); + }; + }, []); +}; diff --git a/packages/hooks/use-scroll-lock/tests/index.test.ts b/packages/hooks/use-scroll-lock/tests/index.test.ts new file mode 100644 index 0000000..5bb6dc7 --- /dev/null +++ b/packages/hooks/use-scroll-lock/tests/index.test.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { renderHook } from '@testing-library/react'; +import { useScrollLock } from '../src'; + +describe('useScrollLock test:', () => { + beforeEach(() => { + document.body.style.overflow = 'auto'; + document.body.style.paddingRight = '0px'; + + jest.spyOn(document, 'addEventListener'); + jest.spyOn(document, 'removeEventListener'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should block the scroll and add the touchmove event', () => { + const { unmount } = renderHook(() => useScrollLock()); + const touchMoveEvent = new Event('touchmove'); + const scrollbarWidth = window.innerWidth - document.body.scrollWidth; + touchMoveEvent.preventDefault = jest.fn(); + document.dispatchEvent(touchMoveEvent); + + expect(touchMoveEvent.preventDefault).toHaveBeenCalled(); + expect(document.body.style.overflow).toBe('hidden'); + expect(document.body.style.paddingRight).toBe(`${scrollbarWidth}px`); + + expect(document.addEventListener).toHaveBeenCalledWith( + 'touchmove', + expect.any(Function), + { passive: false } + ); + + unmount(); + expect(document.body.style.overflow).toBe('auto'); + expect(document.body.style.paddingRight).toBe('0px'); + + expect(document.removeEventListener).toHaveBeenCalledWith( + 'touchmove', + expect.any(Function) + ); + }); +}); diff --git a/packages/hooks/use-scroll-position/package.json b/packages/hooks/use-scroll-position/package.json index a08fe52..894e2a5 100644 --- a/packages/hooks/use-scroll-position/package.json +++ b/packages/hooks/use-scroll-position/package.json @@ -32,8 +32,7 @@ ], "peerDependencies": { "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "@raddix/use-event-listener": "workspace:*" + "react-dom": ">=16.8.0" }, "clean-package": "../../../clean-package.config.json", "tsup": { diff --git a/packages/hooks/use-scroll-position/src/index.ts b/packages/hooks/use-scroll-position/src/index.ts index e8a8928..1ea6d94 100644 --- a/packages/hooks/use-scroll-position/src/index.ts +++ b/packages/hooks/use-scroll-position/src/index.ts @@ -1,39 +1,35 @@ import { type RefObject, useEffect, useState } from 'react'; -import { useEventListener } from '@raddix/use-event-listener'; export interface ScrollPosition { x: number | null; y: number | null; } -export interface Options { - target?: RefObject | Document; +interface Options { + target?: RefObject; } -export const useScrollPosition = ({ - target = globalThis.document -}: Options = {}): ScrollPosition => { +export const useScrollPosition = ({ target }: Options = {}): ScrollPosition => { const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 }); - const handle = () => { - const targetElement = - target instanceof Document ? document.documentElement : target.current; - - setScrollPosition({ - x: targetElement?.scrollLeft ?? null, - y: targetElement?.scrollTop ?? null - }); - }; - useEffect(() => { + const element = target ? target.current : window; + + const handle = () => { + setScrollPosition({ + x: (target ? target.current?.scrollLeft : window.scrollX) ?? null, + y: (target ? target.current?.scrollTop : window.scrollY) ?? null + }); + }; handle(); + + element?.addEventListener('scroll', handle, { passive: true }); + return () => element?.removeEventListener('scroll', handle); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEventListener('scroll', handle, { target, passive: true }); - return scrollPosition; }; diff --git a/packages/hooks/use-scroll-position/tests/index.test.ts b/packages/hooks/use-scroll-position/tests/index.test.ts index 8ca1d7a..cf27729 100644 --- a/packages/hooks/use-scroll-position/tests/index.test.ts +++ b/packages/hooks/use-scroll-position/tests/index.test.ts @@ -42,9 +42,17 @@ describe('useScrollPosition test:', () => { expect(result.current).toEqual({ x: 0, y: 0 }); act(() => { - document.documentElement.scrollTop = 100; - document.documentElement.scrollLeft = 50; - document.dispatchEvent(new Event('scroll')); + Object.defineProperty(window, 'scrollY', { + value: 100, + configurable: true + }); + + Object.defineProperty(window, 'scrollX', { + value: 50, + configurable: true + }); + + window.dispatchEvent(new Event('scroll')); }); expect(result.current).toEqual({ x: 50, y: 100 }); diff --git a/packages/hooks/use-window-size/package.json b/packages/hooks/use-window-size/package.json index ec2f7ba..072738f 100644 --- a/packages/hooks/use-window-size/package.json +++ b/packages/hooks/use-window-size/package.json @@ -32,8 +32,7 @@ ], "peerDependencies": { "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "@raddix/use-event-listener": "workspace:*" + "react-dom": ">=16.8.0" }, "clean-package": "../../../clean-package.config.json", "tsup": { diff --git a/packages/hooks/use-window-size/src/index.ts b/packages/hooks/use-window-size/src/index.ts index 4128253..8347a93 100644 --- a/packages/hooks/use-window-size/src/index.ts +++ b/packages/hooks/use-window-size/src/index.ts @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { useEventListener } from '@raddix/use-event-listener'; interface Size { width: number; @@ -21,9 +20,10 @@ export const useWindowSize = (): Size => { useEffect(() => { handleResize(); - }, []); + window.addEventListener('resize', handleResize); - useEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); return windowSize; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efd0ca5..fe2335d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,9 +237,6 @@ importers: packages/hooks/use-clipboard: dependencies: - '@raddix/use-timeout': - specifier: workspace:* - version: link:../use-timeout react: specifier: '>=16.8.0' version: 18.2.0 @@ -358,11 +355,17 @@ importers: specifier: '>=16.8.0' version: 18.2.0(react@18.2.0) + packages/hooks/use-scroll-lock: + dependencies: + 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-position: dependencies: - '@raddix/use-event-listener': - specifier: workspace:* - version: link:../use-event-listener react: specifier: '>=16.8.0' version: 18.2.0 @@ -414,9 +417,6 @@ importers: packages/hooks/use-window-size: dependencies: - '@raddix/use-event-listener': - specifier: workspace:* - version: link:../use-event-listener react: specifier: '>=16.8.0' version: 18.2.0