From fea782051ec0751ac5b86aef39ee16ed8ff7b07c Mon Sep 17 00:00:00 2001 From: lintuming Date: Fri, 23 Aug 2019 18:53:41 +0800 Subject: [PATCH] useMeasure --- README.md | 1 + docs/useMeasure.md | 23 +++++++++ package.json | 1 + src/__tests__/useMeasure.test.ts | 84 ++++++++++++++++++++++++++++++++ src/useMeasure.ts | 42 ++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 docs/useMeasure.md create mode 100644 src/__tests__/useMeasure.test.ts create mode 100644 src/useMeasure.ts diff --git a/README.md b/README.md index 8e36339e..60314abc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ - [`useStartTyping`](./docs/useStartTyping.md) — detects when user starts typing. - [`useWindowScroll`](./docs/useWindowScroll.md) — tracks `Window` scroll position. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usewindowscroll--docs) - [`useWindowSize`](./docs/useWindowSize.md) — tracks `Window` dimensions. [![][img-demo]](https://codesandbox.io/s/m7ln22668) + - [`useMeasure`](./docs/useMeasure.md) — tracks an HTML element's dimensions by [Resize Observer](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)

- [**UI**](./docs/UI.md) diff --git a/docs/useMeasure.md b/docs/useMeasure.md new file mode 100644 index 00000000..64123f24 --- /dev/null +++ b/docs/useMeasure.md @@ -0,0 +1,23 @@ +# `useSize` + +React sensor hook that tracks size of an HTML element. + +## Usage + +```jsx +import { useMeasure } from "react-use"; + +const Demo = () => { + const [ref, { width, height }] = useSize(); + + return ( +
+
width: {width}
+
height: {height}
+
+ ); +}; +``` +## Related hooks + +- [useSize](./useSize.md) \ No newline at end of file diff --git a/package.json b/package.json index 4bb3a3de..c1d5906a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "nano-css": "^5.1.0", "react-fast-compare": "^2.0.4", "react-wait": "^0.3.0", + "resize-observer-polyfill": "^1.5.1", "screenfull": "^4.1.0", "throttle-debounce": "^2.0.1", "ts-easing": "^0.2.0" diff --git a/src/__tests__/useMeasure.test.ts b/src/__tests__/useMeasure.test.ts new file mode 100644 index 00000000..e12136e2 --- /dev/null +++ b/src/__tests__/useMeasure.test.ts @@ -0,0 +1,84 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useMeasure, { ContentRect } from '../useMeasure'; + +interface Entry { + target: HTMLElement; + contentRect: ContentRect; +} + +jest.mock('resize-observer-polyfill', () => { + return class ResizeObserver { + private cb: (entries: Entry[]) => void; + private map: WeakMap; + private targets: HTMLElement[]; + constructor(cb: () => void) { + this.cb = cb; + this.map = new WeakMap(); + this.targets = []; + } + public disconnect() { + this.targets.map(target => { + const originMethod = this.map.get(target); + target.setAttribute = originMethod; + this.map.delete(target); + }); + } + public observe(target: HTMLElement) { + const method = 'setAttribute'; + const originMethod = target[method]; + this.map.set(target, originMethod); + this.targets.push(target); + target[method] = (...args) => { + const [attrName, value] = args; + if (attrName === 'style') { + const rect: ContentRect = { top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 }; + value.split(';').map(kv => { + const [key, v] = kv.split(':'); + if (['top', 'bottom', 'left', 'right', 'width', 'height'].includes(key)) { + rect[key] = parseInt(v, 10); + } + }); + target.getBoundingClientRect = () => rect; + } + originMethod.apply(target, args); + this.fireCallback(); + }; + } + private fireCallback() { + if (this.cb) { + this.cb( + this.targets.map(target => { + return { + target, + contentRect: target.getBoundingClientRect(), + }; + }) + ); + } + } + }; +}); + +it('reacts to changes in size of any of the observed elements', () => { + const { result } = renderHook(() => useMeasure()); + const div = document.createElement('div'); + result.current[0](div); + expect(result.current[1]).toMatchObject({ + width: 0, + height: 0, + top: 0, + bottom: 0, + left: 0, + right: 0, + }); + act(() => div.setAttribute('style', 'width:200px;height:200px;top:100;left:100')); + + expect(result.current[1]).toMatchObject({ + width: 200, + height: 200, + top: 100, + bottom: 0, + left: 100, + right: 0, + }); +}); diff --git a/src/useMeasure.ts b/src/useMeasure.ts new file mode 100644 index 00000000..6a68d015 --- /dev/null +++ b/src/useMeasure.ts @@ -0,0 +1,42 @@ +import { useCallback, useState } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +export interface ContentRect { + width: number; + height: number; + top: number; + right: number; + left: number; + bottom: number; +} +const useMeasure = (): [(instance: T) => void, ContentRect] => { + const [rect, set] = useState({ + width: 0, + height: 0, + top: 0, + left: 0, + bottom: 0, + right: 0, + }); + + const [observer] = useState( + () => + new ResizeObserver(entries => { + const entry = entries[0]; + set(entry.contentRect); + }) + ); + + const ref = useCallback( + node => { + observer.disconnect(); + if (node) { + observer.observe(node); + } + }, + [observer] + ); + return [ref, rect]; +}; + +export default useMeasure;