Skip to content

Commit 45e9648

Browse files
committed
[IMPL] usePinchZoom hook
1 parent ae7d908 commit 45e9648

11 files changed

Lines changed: 188 additions & 20 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useCallback, useRef, useState } from "react"
2+
import { usePinchZoom } from "../../../../../../packages/react-tools/src";
3+
4+
/**
5+
The component renders a bordered div element. When pinch zoom gestures are executed in this div, a message is shown inside it with zoom type.
6+
*/
7+
export const UsePinchZoom = () => {
8+
const [state, setState] = useState("");
9+
const ref = useRef<HTMLDivElement>(null);
10+
const listener = useCallback((evt: PointerEvent, type: "zoomIn" | "zoomOut") => {
11+
setState(type === "zoomIn" ? "Zooming in..." : "Zooming out...");
12+
}, []);
13+
usePinchZoom({
14+
listener,
15+
target: ref
16+
});
17+
18+
return (
19+
<div ref={ref} style={{ margin: '0 auto', width: 300, height: 300, overflow: 'auto', resize: 'both', border: '1px solid lightblue' }}>
20+
{state}
21+
</div>
22+
);
23+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ export const COMPONENTS = [
6464
"useBeforeUnload",
6565
"useDoubleClick",
6666
"useScreen",
67-
"useHotKeys"
67+
"useHotKeys",
68+
"usePinchZoom"
6869
],
6970
//API DOM
7071
[

apps/react-tools-demo/src/markdown/useEventListener.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ useEventListener <T extends Event | CustomEvent>({ type, listener, element = win
4242
>
4343
> - __options__: _Object_
4444
> - __options.type__: _string_
45-
event type
45+
event type.
4646
> - __options.listener__: _(evt: Event | CustomEvent) => void_
47-
listener to be executed on specified event
47+
listener to be executed on specified event.
4848
> - __options.element=window?__: _RefObject<HTMLElement> | Window_
49-
element on which attaching eventListener
49+
element on which attaching eventListener.
5050
> - __options.listenerOpts?__: _boolean | AddEventListenerOptions_
51-
options for listener
51+
options for listener.
5252
> - __options.effectType="normal"?__: _"normal"|"layout"_
5353
option to set which hook is used to attach event listener.
5454
>

apps/react-tools-demo/src/markdown/useHotKeys.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ useHotKeys ({ hotKey, type = "keydown", target = window, listener, listenerOpts
5656
> - __options.hotKey__: _`${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}`_
5757
hotKey string: _ctrlCommand_ indicates to listen __Ctrl__ (on Windows) or __Command__ (on Mac) keys.
5858
> - __options.type="keydown"?__: _"keydown"|"keyup"_
59-
event type
59+
event type.
6060
> - __options.listener__: _(evt: KeyboardEvent|React.KeyboardEvent<HTMLElement>) => void | Promise<void>_
61-
listener to be executed on specified event
61+
listener to be executed on specified event.
6262
> - __options.target=window?__: _RefObject<HTMLElement> | Window_
63-
element on which attaching eventListener
63+
element on which attaching eventListener.
6464
> - __options.listenerOpts?__: _boolean | AddEventListenerOptions_
65-
options for listener
65+
options for listener.
6666
>
6767
6868
> ### Returns
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# usePinchZoom
2+
Hook to handle pinch zoom gestures.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UsePinchZoom = () => {
8+
const [state, setState] = useState("");
9+
const ref = useRef<HTMLDivElement>(null);
10+
const listener = useCallback((evt: PointerEvent, type: "zoomIn" | "zoomOut") => {
11+
setState(type === "zoomIn" ? "Zooming in..." : "Zooming out...");
12+
}, []);
13+
usePinchZoom({
14+
listener,
15+
target: ref
16+
});
17+
18+
return (
19+
<div ref={ref} style={{ margin: '0 auto', width: 300, height: 300, overflow: 'auto', resize: 'both', border: '1px solid lightblue' }}>
20+
{state}
21+
</div>
22+
);
23+
}
24+
```
25+
26+
> The component renders a bordered div element. When pinch zoom gestures are executed in this div, a message is shown inside it with zoom type.
27+
28+
29+
## API
30+
31+
```tsx
32+
usePinchZoom ({ target = window, listener }: { target?: RefObject<HTMLElement> | Window, listener: (evt: PointerEvent, type: "zoomIn" | "zoomOut") => void | Promise<void> }): (()=>void)
33+
```
34+
35+
> ### Params
36+
>
37+
> - __options__: _Object_
38+
> - __options.listener__: _(evt: PointerEvent, type: "zoomIn"|"zoomOut") => void | Promise<void>_
39+
listener to be executed on pinch zoom event.
40+
> - __options.target=window?__: _RefObject<HTMLElement> | Window_
41+
element on which attaching eventListener.
42+
>
43+
44+
> ### Returns
45+
>
46+
> __remove__: remove listener manually.
47+
> - _()=>void_
48+
>

packages/react-tools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@
6767
- [x] useBeforeUnload
6868
- [x] useScreen (orientation and ecc)
6969
- [x] useHotKeys
70-
- [ ] useImageOnLoad
71-
- [ ] usePinchZoom
70+
- [x] usePinchZoom
7271
- [ ] useInfiniteScroll
7372
- [ ] useDragAndDrop (check for mobile usage)
7473

@@ -129,6 +128,7 @@
129128
- [ ] ErrorBoundary (??)
130129
- [ ] Suspense: Suspence compontent react-like for async component
131130
- [ ] Dynamic: This component lets you insert an arbitrary Component or tag and passes the props through to it.
131+
- [ ] ImageOpt (???)
132132

133133
## ESlint configuration
134134
To validate dependencies of custom hooks like `useMemoCompare`, configure `exhaustive-deps` with the `additionalHooks` option

packages/react-tools/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ export { useDoubleClick } from './useDoubleClick';
7070
export { useScreen } from './useScreen';
7171
export { useMergedRef } from './useMergedRef';
7272
export { useHotKeys } from './useHotKeys';
73+
export { usePinchZoom } from './usePinchZoom';

packages/react-tools/src/hooks/useEventListener.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { useMemoizedFunction } from ".";
44
/**
55
* __`useEventListener`__: Hook to simplify add and remove EventListener use. It's persist during rerendering and automatically remove eventlistener on unMount component lifecycle.
66
* @param {Object} options
7-
* @param {string} options.type - event type
8-
* @param {(evt: Event | CustomEvent) => void} options.listener - listener to be executed on specified event
9-
* @param {RefObject<HTMLElement> | Window} [options.element=window] - element on which attaching eventListener
10-
* @param {boolean | AddEventListenerOptions} [options.listenerOpts] - options for listener
7+
* @param {string} options.type - event type.
8+
* @param {(evt: Event | CustomEvent) => void} options.listener - listener to be executed on specified event.
9+
* @param {RefObject<HTMLElement> | Window} [options.element=window] - element on which attaching eventListener.
10+
* @param {boolean | AddEventListenerOptions} [options.listenerOpts] - options for listener.
1111
* @param {"normal"|"layout"} [options.effectType="normal"] - option to set which hook is used to attach event listener.
1212
* @returns {()=>void} remove - used to manually remove the eventListener, otherwise is removed when component is unmounted.
1313
*/

packages/react-tools/src/hooks/useHotKeys.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { hotKeyHandler } from "../utils";
66
* __`useHotKeys`__: Hook to listen for the keyboard press, support key combinations, built on [hotKeyHandler](#/hotKeyHandler) utility function.
77
* @param {Object} options
88
* @param {`${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}`} options.hotKey - hotKey string: _ctrlCommand_ indicates to listen __Ctrl__ (on Windows) or __Command__ (on Mac) keys.
9-
* @param {"keydown"|"keyup"} [options.type="keydown"] - event type
10-
* @param {(evt: KeyboardEvent|React.KeyboardEvent<HTMLElement>) => void | Promise<void>} options.listener - listener to be executed on specified event
11-
* @param {RefObject<HTMLElement> | Window} [options.target=window] - element on which attaching eventListener
12-
* @param {boolean | AddEventListenerOptions} [options.listenerOpts] - options for listener
9+
* @param {"keydown"|"keyup"} [options.type="keydown"] - event type.
10+
* @param {(evt: KeyboardEvent|React.KeyboardEvent<HTMLElement>) => void | Promise<void>} options.listener - listener to be executed on specified event.
11+
* @param {RefObject<HTMLElement> | Window} [options.target=window] - element on which attaching eventListener.
12+
* @param {boolean | AddEventListenerOptions} [options.listenerOpts] - options for listener.
1313
* @returns {()=>void} remove - used to manually remove the eventListener, otherwise is removed when component is unmounted.
1414
*/
1515
export const useHotKeys = ({ hotKey, type = "keydown", target = window, listener, listenerOpts }: { hotKey: `${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}` | `${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${'alt' | 'ctrl' | 'meta' | 'shift' | 'ctrlCommand'}+${string}`, type?: "keydown" | "keyup", target?: RefObject<HTMLElement> | Window, listener: (evt: KeyboardEvent | KeyEvt<HTMLElement>) => void | Promise<void>, listenerOpts?: boolean | AddEventListenerOptions }): (() => void) => {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { RefObject, useCallback, useRef } from "react";
2+
import { useEventListener } from ".";
3+
4+
/**
5+
* **`usePinchZoom`**: Hook to handle pinch zoom gestures.
6+
* @param {Object} options
7+
* @param {(evt: PointerEvent, type: "zoomIn"|"zoomOut") => void | Promise<void>} options.listener - listener to be executed on pinch zoom event.
8+
* @param {RefObject<HTMLElement> | Window} [options.target=window] - element on which attaching eventListener.
9+
* @returns {()=>void} remove - remove listener manually.
10+
*/
11+
export const usePinchZoom = ({ target = window, listener }: { target?: RefObject<HTMLElement> | Window, listener: (evt: PointerEvent, type: "zoomIn" | "zoomOut") => void | Promise<void> }): (()=>void) => {
12+
const cache = useRef<PointerEvent[]>([]);
13+
const prevDiff = useRef(-1);
14+
const removeListeners = useRef<(()=>void)[]>([]);
15+
16+
const pointerDownHandler = useRef((evt: PointerEvent) => {
17+
cache.current.push(evt);
18+
});
19+
const pointerUpHandler = useRef((evt: PointerEvent) => {
20+
const index = cache.current.findIndex(
21+
(cachedEv) => cachedEv.pointerId === evt.pointerId,
22+
);
23+
cache.current.splice(index, 1);
24+
if (cache.current.length < 2) {
25+
prevDiff.current = -1;
26+
}
27+
});
28+
29+
const pointerMoveHandler = useCallback((evt: PointerEvent) => {
30+
// Find this event in the cache and update its record with this event
31+
const index = cache.current.findIndex(
32+
(cachedEv) => cachedEv.pointerId === evt.pointerId,
33+
);
34+
cache.current[index] = evt;
35+
// If two pointers are down, check for pinch gestures
36+
if (cache.current.length === 2) {
37+
// Calculate the distance between the two pointers
38+
const curDiff = Math.abs(cache.current[0].clientX - cache.current[1].clientX);
39+
if (prevDiff.current > 0) {
40+
if (curDiff > prevDiff.current) {
41+
// The distance between the two pointers has increased: Zoom in
42+
listener(evt, "zoomIn");
43+
}
44+
if (curDiff < prevDiff.current) {
45+
// The distance between the two pointers has decreased: Zoom out
46+
listener(evt, "zoomOut");
47+
}
48+
}
49+
// Cache the distance for the next move event
50+
prevDiff.current = curDiff;
51+
}
52+
}, [listener]);
53+
54+
removeListeners.current[0] = useEventListener({
55+
type: "pointerdown",
56+
listener: pointerDownHandler.current,
57+
effectType: "normal",
58+
element: target
59+
});
60+
removeListeners.current[1] = useEventListener({
61+
type: "pointermove",
62+
listener: pointerMoveHandler,
63+
effectType: "normal",
64+
element: target
65+
});
66+
removeListeners.current[2] = useEventListener({
67+
type: "pointerup",
68+
listener: pointerUpHandler.current,
69+
effectType: "normal",
70+
element: target
71+
});
72+
removeListeners.current[3] = useEventListener({
73+
type: "pointercancel",
74+
listener: pointerUpHandler.current,
75+
effectType: "normal",
76+
element: target
77+
});
78+
removeListeners.current[4] = useEventListener({
79+
type: "pointerout",
80+
listener: pointerUpHandler.current,
81+
effectType: "normal",
82+
element: target
83+
});
84+
removeListeners.current[5] = useEventListener({
85+
type: "pointerleave",
86+
listener: pointerUpHandler.current,
87+
effectType: "normal",
88+
element: target
89+
});
90+
91+
return useCallback(() => {
92+
removeListeners.current.forEach(l => l());
93+
},[])
94+
}

0 commit comments

Comments
 (0)