From ff4cde7619613cd0bd4fe1e861e00e0461474f16 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 16 May 2020 14:23:57 +0200 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20install=20react-uni?= =?UTF-8?q?versal-interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index d7683cf9..26cd0257 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 93d9ab48..dd88d6d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 2a2a298b73f7beb9a2a61c309e649be3d2527473 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 16 May 2020 14:26:51 +0200 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20useScratch=20h?= =?UTF-8?q?ook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/useScratch.ts | 180 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/useScratch.ts diff --git a/src/useScratch.ts b/src/useScratch.ts new file mode 100644 index 00000000..43650272 --- /dev/null +++ b/src/useScratch.ts @@ -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 = {}): [ScratchSensorState, (el: HTMLElement | null) => void] => { + const [state, setState] = useState({ isScratching: false }); + const refState = useRef(state); + const refScratching = useRef(false); + const refAnimationFrame = useRef(null); + const [el, setEl] = useState(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 [state, setEl]; +}; + +export interface ScratchSensorProps extends ScratchSensorParams { + children: (state: ScratchSensorState, ref: (el: HTMLElement | null) => void) => React.ReactElement; +} + +export const ScratchSensor: FC = props => { + const { children, ...params } = props; + const [state, ref] = 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; From 5f5dd09bf357fdb7cade6320cd60f875cd31f1f7 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 12 Jan 2020 18:17:56 +0100 Subject: [PATCH 3/6] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20useScratch()?= =?UTF-8?q?=20stories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.ts | 1 + src/useScratch.ts | 6 ++--- stories/useScratch.story.tsx | 46 ++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 stories/useScratch.story.tsx diff --git a/src/index.ts b/src/index.ts index 6d018164..d632f7c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/useScratch.ts b/src/useScratch.ts index 43650272..2f73530a 100644 --- a/src/useScratch.ts +++ b/src/useScratch.ts @@ -33,7 +33,7 @@ const useScratch = ({ onScratch = noop, onScratchStart = noop, onScratchEnd = noop, -}: ScratchSensorParams = {}): [ScratchSensorState, (el: HTMLElement | null) => void] => { +}: ScratchSensorParams = {}): [(el: HTMLElement | null) => void, ScratchSensorState] => { const [state, setState] = useState({ isScratching: false }); const refState = useRef(state); const refScratching = useRef(false); @@ -154,7 +154,7 @@ const useScratch = ({ }; }, [el, disabled, onScratchStart, onScratch, onScratchEnd]); - return [state, setEl]; + return [setEl, state]; }; export interface ScratchSensorProps extends ScratchSensorParams { @@ -163,7 +163,7 @@ export interface ScratchSensorProps extends ScratchSensorParams { export const ScratchSensor: FC = props => { const { children, ...params } = props; - const [state, ref] = useScratch(params); + const [ref, state] = useScratch(params); const element = render(props, state); return cloneElement(element, { ...element.props, diff --git a/stories/useScratch.story.tsx b/stories/useScratch.story.tsx new file mode 100644 index 00000000..49fe1edb --- /dev/null +++ b/stories/useScratch.story.tsx @@ -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 ( +
+
{JSON.stringify(state, null, 4)}
+ {state.isScratching &&
} +
+ ); +}; + +storiesOf('Sensors/useScratch', module) + // .add('Docs', () => ) + .add('Demo', () => ); From 1e74bdcf489ea659de89adc23f9a63c112714860 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 12 Jan 2020 18:21:43 +0100 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20add=20useScrat?= =?UTF-8?q?ch=20to=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/useScratch.md | 75 ++++++++++++++++++++++++++++++++++++ stories/useScratch.story.tsx | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/useScratch.md diff --git a/README.md b/README.md index 3f9d1250..32cab21e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ - [`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. diff --git a/docs/useScratch.md b/docs/useScratch.md new file mode 100644 index 00000000..0ba6c311 --- /dev/null +++ b/docs/useScratch.md @@ -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 ( +
+
{JSON.stringify(state, null, 4)}
+ {state.isScratching &&
} +
+ ); +}; +``` + +## 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; +} +``` diff --git a/stories/useScratch.story.tsx b/stories/useScratch.story.tsx index 49fe1edb..43e9e31b 100644 --- a/stories/useScratch.story.tsx +++ b/stories/useScratch.story.tsx @@ -42,5 +42,5 @@ const Demo = () => { }; storiesOf('Sensors/useScratch', module) - // .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); From 2aa9b8ba30c89ff344c2167c1698c568817de885 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 16 May 2020 14:31:24 +0200 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20combine=20el?= =?UTF-8?q?=20measure=20hooks=20in=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 32cab21e..c31946d8 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,10 @@ - [`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)
From 31555837dac04d4f76643c2200996f4dc6c7d7f9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 16 May 2020 12:40:57 +0000 Subject: [PATCH 6/6] chore(release): 14.3.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [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)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9296be8b..b58c7a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.json b/package.json index 26cd0257..7971255e 100644 --- a/package.json +++ b/package.json @@ -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",