Merge branch 'master' into improve-useMeasure

This commit is contained in:
streamich 2020-05-16 14:55:42 +02:00
commit 21e53ffc0f
8 changed files with 320 additions and 3 deletions

View File

@ -1,3 +1,10 @@
# [14.3.0](https://github.com/streamich/react-use/compare/v14.2.0...v14.3.0) (2020-05-16)
### Features
* 🎸 add useScratch hook ([2a2a298](https://github.com/streamich/react-use/commit/2a2a298b73f7beb9a2a61c309e649be3d2527473))
# [14.2.0](https://github.com/streamich/react-use/compare/v14.1.1...v14.2.0) (2020-04-24)

View File

@ -60,13 +60,13 @@
- [`useNetwork`](./docs/useNetwork.md) — tracks state of user's internet connection.
- [`useOrientation`](./docs/useOrientation.md) — tracks state of device's screen orientation.
- [`usePageLeave`](./docs/usePageLeave.md) — triggers when mouse leaves page boundaries.
- [`useScratch`](./docs/useScratch.md) — tracks mouse click-and-scrub state.
- [`useScroll`](./docs/useScroll.md) — tracks an HTML element's scroll position. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescroll--docs)
- [`useScrolling`](./docs/useScrolling.md) — tracks whether HTML element is scrolling.
- [`useSize`](./docs/useSize.md) — tracks an HTML element's size.
- [`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 using the Resize Observer API.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo)
- [`useMeasure`](./docs/useMeasure.md) and [`useSize`](./docs/useSize.md) — tracks an HTML element's dimensions. [![][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)
<br/>

75
docs/useScratch.md Normal file
View File

@ -0,0 +1,75 @@
# `useScratch`
React sensor hook that tracks state of mouse "scrubs" (or "scratches").
## Usage
```jsx
import useScratch from 'react-use/lib/useScratch';
const Demo = () => {
const [ref, state] = useScratch();
const blockStyle: React.CSSProperties = {
position: 'relative',
width: 400,
height: 400,
border: '1px solid tomato',
};
const preStyle: React.CSSProperties = {
pointerEvents: 'none',
userSelect: 'none',
};
let { x = 0, y = 0, dx = 0, dy = 0 } = state;
if (dx < 0) [x, dx] = [x + dx, -dx];
if (dy < 0) [y, dy] = [y + dy, -dy];
const rectangleStyle: React.CSSProperties = {
position: 'absolute',
left: x,
top: y,
width: dx,
height: dy,
border: '1px solid tomato',
pointerEvents: 'none',
userSelect: 'none',
};
return (
<div ref={ref} style={blockStyle}>
<pre style={preStyle}>{JSON.stringify(state, null, 4)}</pre>
{state.isScratching && <div style={rectangleStyle} />}
</div>
);
};
```
## Reference
```ts
const [ref, state] = useScratch();
```
`state` is:
```ts
export interface ScratchSensorState {
isScratching: boolean;
start?: number;
end?: number;
x?: number;
y?: number;
dx?: number;
dy?: number;
docX?: number;
docY?: number;
posX?: number;
posY?: number;
elH?: number;
elW?: number;
elX?: number;
elY?: number;
}
```

View File

@ -1,6 +1,6 @@
{
"name": "react-use",
"version": "14.2.0",
"version": "14.3.0",
"description": "Collection of React Hooks",
"main": "lib/index.js",
"module": "esm/index.js",
@ -53,6 +53,7 @@
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.2.1",
"react-universal-interface": "^0.6.0",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"set-harmonic-interval": "^1.0.1",

View File

@ -73,6 +73,7 @@ export { default as useRaf } from './useRaf';
export { default as useRafLoop } from './useRafLoop';
export { default as useRafState } from './useRafState';
export { default as useSearchParam } from './useSearchParam';
export { default as useScratch } from './useScratch';
export { default as useScroll } from './useScroll';
export { default as useScrolling } from './useScrolling';
export { default as useSessionStorage } from './useSessionStorage';

180
src/useScratch.ts Normal file
View File

@ -0,0 +1,180 @@
import { useState, useEffect, useRef, FC, cloneElement } from 'react';
import { render } from 'react-universal-interface';
const noop = () => {};
export interface ScratchSensorParams {
disabled?: boolean;
onScratch?: (state: ScratchSensorState) => void;
onScratchStart?: (state: ScratchSensorState) => void;
onScratchEnd?: (state: ScratchSensorState) => void;
}
export interface ScratchSensorState {
isScratching: boolean;
start?: number;
end?: number;
x?: number;
y?: number;
dx?: number;
dy?: number;
docX?: number;
docY?: number;
posX?: number;
posY?: number;
elH?: number;
elW?: number;
elX?: number;
elY?: number;
}
const useScratch = ({
disabled,
onScratch = noop,
onScratchStart = noop,
onScratchEnd = noop,
}: ScratchSensorParams = {}): [(el: HTMLElement | null) => void, ScratchSensorState] => {
const [state, setState] = useState<ScratchSensorState>({ isScratching: false });
const refState = useRef<ScratchSensorState>(state);
const refScratching = useRef<boolean>(false);
const refAnimationFrame = useRef<any>(null);
const [el, setEl] = useState<HTMLElement | null>(null);
useEffect(() => {
if (disabled) return;
if (!el) return;
const onMoveEvent = (docX, docY) => {
cancelAnimationFrame(refAnimationFrame.current);
refAnimationFrame.current = requestAnimationFrame(() => {
const { left, top } = el.getBoundingClientRect();
const elX = left + window.scrollX;
const elY = top + window.scrollY;
const x = docX - elX;
const y = docY - elY;
setState(oldState => {
const newState = {
...oldState,
dx: x - (oldState.x || 0),
dy: y - (oldState.y || 0),
end: Date.now(),
isScratching: true,
};
refState.current = newState;
onScratch(newState);
return newState;
});
});
};
const onMouseMove = event => {
onMoveEvent(event.pageX, event.pageY);
};
const onTouchMove = event => {
onMoveEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
};
let onMouseUp;
let onTouchEnd;
const stopScratching = () => {
if (!refScratching.current) return;
refScratching.current = false;
refState.current = { ...refState.current, isScratching: false };
onScratchEnd(refState.current);
setState({ isScratching: false });
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onTouchEnd);
};
onMouseUp = stopScratching;
onTouchEnd = stopScratching;
const startScratching = (docX, docY) => {
if (!refScratching.current) return;
const { left, top } = el.getBoundingClientRect();
const elX = left + window.scrollX;
const elY = top + window.scrollY;
const x = docX - elX;
const y = docY - elY;
const time = Date.now();
const newState = {
isScratching: true,
start: time,
end: time,
docX,
docY,
x,
y,
dx: 0,
dy: 0,
elH: el.offsetHeight,
elW: el.offsetWidth,
elX,
elY,
};
refState.current = newState;
onScratchStart(newState);
setState(newState);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('touchend', onTouchEnd);
};
const onMouseDown = event => {
refScratching.current = true;
startScratching(event.pageX, event.pageY);
};
const onTouchStart = event => {
refScratching.current = true;
startScratching(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
};
el.addEventListener('mousedown', onMouseDown);
el.addEventListener('touchstart', onTouchStart);
return () => {
el.removeEventListener('mousedown', onMouseDown);
el.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onTouchEnd);
if (refAnimationFrame.current) cancelAnimationFrame(refAnimationFrame.current);
refAnimationFrame.current = null;
refScratching.current = false;
refState.current = { isScratching: false };
setState(refState.current);
};
}, [el, disabled, onScratchStart, onScratch, onScratchEnd]);
return [setEl, state];
};
export interface ScratchSensorProps extends ScratchSensorParams {
children: (state: ScratchSensorState, ref: (el: HTMLElement | null) => void) => React.ReactElement<any>;
}
export const ScratchSensor: FC<ScratchSensorProps> = props => {
const { children, ...params } = props;
const [ref, state] = useScratch(params);
const element = render(props, state);
return cloneElement(element, {
...element.props,
ref: el => {
if (element.props.ref) {
if (typeof element.props.ref === 'object') element.props.ref.current = el;
if (typeof element.props.ref === 'function') element.props.ref(el);
}
ref(el);
},
});
};
export default useScratch;

View File

@ -0,0 +1,46 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useScratch } from '../src';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const [ref, state] = useScratch();
const blockStyle: React.CSSProperties = {
position: 'relative',
width: 400,
height: 400,
border: '1px solid tomato',
};
const preStyle: React.CSSProperties = {
pointerEvents: 'none',
userSelect: 'none',
};
let { x = 0, y = 0, dx = 0, dy = 0 } = state;
if (dx < 0) [x, dx] = [x + dx, -dx];
if (dy < 0) [y, dy] = [y + dy, -dy];
const rectangleStyle: React.CSSProperties = {
position: 'absolute',
left: x,
top: y,
width: dx,
height: dy,
border: '1px solid tomato',
pointerEvents: 'none',
userSelect: 'none',
};
return (
<div ref={ref} style={blockStyle}>
<pre style={preStyle}>{JSON.stringify(state, null, 4)}</pre>
{state.isScratching && <div style={rectangleStyle} />}
</div>
);
};
storiesOf('Sensors/useScratch', module)
.add('Docs', () => <ShowDocs md={require('../docs/useScratch.md')} />)
.add('Demo', () => <Demo />);

View File

@ -13181,6 +13181,13 @@ react-transition-group@^2.2.1:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-universal-interface@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.0.tgz#b65cbf7d71a2f3f7dd9705d8e4f06748539bd465"
integrity sha512-PzApKKWfd7gvDi1sU/D07jUqnLvFxYqvJi+GEtLvBO5tXJjKr2Sa8ETVHkMA7Jcvdwt7ttbPq7Sed1JpFdNqBQ==
dependencies:
tslib "^1.9.3"
react@16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"