mirror of
https://github.com/streamich/react-use.git
synced 2026-01-25 14:17:16 +00:00
Merge branch 'master' into improve-useMeasure
This commit is contained in:
commit
21e53ffc0f
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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
75
docs/useScratch.md
Normal 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;
|
||||
}
|
||||
```
|
||||
@ -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",
|
||||
|
||||
@ -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
180
src/useScratch.ts
Normal 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;
|
||||
46
stories/useScratch.story.tsx
Normal file
46
stories/useScratch.story.tsx
Normal 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 />);
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user