Skip to content

Commit

Permalink
Merge pull request #573 from lintuming/master
Browse files Browse the repository at this point in the history
feat: add useMeasure
  • Loading branch information
streamich authored Sep 1, 2019
2 parents 99f6639 + f1d51fc commit b96f097
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).[![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo)
<br/>
<br/>
- [**UI**](./docs/UI.md)
Expand Down
24 changes: 24 additions & 0 deletions docs/useMeasure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `useMeasure`

React sensor hook that reacts to changes in size of any of the observed elements.

## Usage

```jsx
import { useMeasure } from "react-use";

const Demo = () => {
const [ref, { width, height }] = useMeasure();

return (
<div ref={ref}>
<div>width: {width}</div>
<div>height: {height}</div>
</div>
);
};
```

## Related hooks

- [useSize](./useSize.md)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"set-harmonic-interval": "^1.0.0",
"throttle-debounce": "^2.0.1",
Expand Down
27 changes: 27 additions & 0 deletions src/__stories__/useMeasure.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { storiesOf } from '@storybook/react';
import React from 'react';
import { useMeasure } from '..';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [ref, { width, height }] = useMeasure();

return (
<>
<div>width: {width}</div>
<div>height: {height}</div>
<div
style={{
background: 'red',
}}
ref={ref}
>
resize me
</div>
</>
);
};

storiesOf('Sensors|useMeasure', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useMeasure.md')} />)
.add('Demo', () => <Demo />);
84 changes: 84 additions & 0 deletions src/__tests__/useMeasure.test.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement, any>;
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,
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ export { default as useVideo } from './useVideo';
export { useWait, Waiter } from './useWait';
export { default as useWindowScroll } from './useWindowScroll';
export { default as useWindowSize } from './useWindowSize';
export { default as useMeasure } from './useMeasure';
43 changes: 43 additions & 0 deletions src/useMeasure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 = <T>(): [(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];
if (entry) {
set(entry.contentRect);
}
})
);

const ref = useCallback(
node => {
observer.disconnect();
if (node) {
observer.observe(node);
}
},
[observer]
);
return [ref, rect];
};

export default useMeasure;

0 comments on commit b96f097

Please sign in to comment.