diff --git a/README.md b/README.md index 12be4371ba..6b65d8a35b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ - [`useWindowSize`](./docs/useWindowSize.md) — tracks `Window` dimensions. [![][img-demo]](https://codesandbox.io/s/m7ln22668) - [`useMeasure`](./docs/useMeasure.md) — tracks an HTML element's dimensions using the Resize Observer API.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo) - [`createBreakpoint`](./docs/createBreakpoint.md) — tracks `innerWidth` + - [`useScrollbarWidth`](./docs/useScrollbarWidth.md) — detects browser's native scrollbars width. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescrollbarwidth--demo)

- [**UI**](./docs/UI.md) diff --git a/docs/useScrollbarWidth.md b/docs/useScrollbarWidth.md new file mode 100644 index 0000000000..b3246dfa91 --- /dev/null +++ b/docs/useScrollbarWidth.md @@ -0,0 +1,25 @@ +# `useScrollbarWidth` + +Hook that will return current browser's scrollbar width. +In case hook been called before DOM ready, it will return `undefined` and will cause re-render on first available RAF. +> **_NOTE:_** it does not work (return 0) for mobile devices, because their scrollbar width can not be determined. + +## Usage + +```jsx +const Demo = () => { + const sbw = useScrollbarWidth(); + + return ( +
+ {sbw === undefined ? `DOM is not ready yet, SBW detection delayed` : `Browser's scrollbar width is ${sbw}px`} +
+ ); +}; +``` + +## Reference + +```typescript +const sbw: number | undefined = useScrollbarWidth(); +``` diff --git a/package.json b/package.json index 4805ab25df..5d4901d582 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "homepage": "https://github.com/streamich/react-use#readme", "dependencies": { + "@xobotyi/scrollbar-width": "1.5.0", "copy-to-clipboard": "^3.2.0", "nano-css": "^5.2.1", "react-fast-compare": "^2.0.4", diff --git a/src/__stories__/useScrollbarWidth.story.tsx b/src/__stories__/useScrollbarWidth.story.tsx new file mode 100644 index 0000000000..2225f0f4fb --- /dev/null +++ b/src/__stories__/useScrollbarWidth.story.tsx @@ -0,0 +1,18 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useScrollbarWidth } from '..'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const sbw = useScrollbarWidth(); + + return ( +
+ {sbw === undefined ? `DOM is not ready yet, SBW detection delayed` : `Browser's scrollbar width is ${sbw}px`} +
+ ); +}; + +storiesOf('Sensors/useScrollbarWidth', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/index.ts b/src/index.ts index 61ef03e3a3..9d78994a27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,6 +92,7 @@ export { default as useUpsert } from './useUpsert'; export { default as useVibrate } from './useVibrate'; export { default as useVideo } from './useVideo'; export { default as useStateValidator } from './useStateValidator'; +export { useScrollbarWidth } from './useScrollbarWidth'; export { useMultiStateValidator } from './useMultiStateValidator'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; diff --git a/src/useScrollbarWidth.ts b/src/useScrollbarWidth.ts new file mode 100644 index 0000000000..ff637ce4f9 --- /dev/null +++ b/src/useScrollbarWidth.ts @@ -0,0 +1,21 @@ +import { scrollbarWidth } from '@xobotyi/scrollbar-width'; +import { useEffect, useState } from 'react'; + +export function useScrollbarWidth(): number | undefined { + const [sbw, setSbw] = useState(scrollbarWidth()); + + // this needed to ensure the scrollbar width in case hook called before the DOM is ready + useEffect(() => { + if (typeof sbw !== 'undefined') { + return; + } + + const raf = requestAnimationFrame(() => { + setSbw(scrollbarWidth()); + }); + + return () => cancelAnimationFrame(raf); + }, []); + + return sbw; +} diff --git a/tests/useScrollbarWidth.test.ts b/tests/useScrollbarWidth.test.ts new file mode 100644 index 0000000000..26a60aa423 --- /dev/null +++ b/tests/useScrollbarWidth.test.ts @@ -0,0 +1,45 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { scrollbarWidth } from '@xobotyi/scrollbar-width'; +import { useScrollbarWidth } from '../src'; +import { replaceRaf } from 'raf-stub'; + +declare var requestAnimationFrame: { + add: (cb: Function) => number; + remove: (id: number) => void; + flush: (duration?: number) => void; + reset: () => void; + step: (steps?: number, duration?: number) => void; +}; + +describe('useScrollbarWidth', () => { + beforeAll(() => { + replaceRaf(); + }); + + afterEach(() => { + requestAnimationFrame.reset(); + }); + + it('should be defined', () => { + expect(useScrollbarWidth).toBeDefined(); + }); + + it('should return value of scrollbarWidth result', () => { + scrollbarWidth.__cache = 21; + const { result } = renderHook(() => useScrollbarWidth()); + + expect(result.current).toBe(21); + }); + + it('should re-call scrollbar width in RAF in case `scrollbarWidth()` returned undefined', () => { + scrollbarWidth.__cache = undefined; + const { result } = renderHook(() => useScrollbarWidth()); + expect(result.current).toBe(undefined); + scrollbarWidth.__cache = 34; + act(() => { + requestAnimationFrame.step(); + }); + + expect(result.current).toBe(34); + }); +}); diff --git a/yarn.lock b/yarn.lock index 31ebec02d3..770649f50a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3094,6 +3094,11 @@ "@webassemblyjs/wast-parser" "1.8.5" "@xtuc/long" "4.2.2" +"@xobotyi/scrollbar-width@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.5.0.tgz#488210bff634548040dc22a72f62722a85b134e1" + integrity sha512-BK+HR1D00F2xh7n4+5en8/dMkG13uvIXLmEbsjtc1702b7+VwXkvlBDKoRPJMbkRN5hD7VqWa3nS9fNT8JG3CA== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"