Skip to content

Commit f11364a

Browse files
lstoeferleantfu
andauthored
feat(useSwipe): new function (#333)
* feat: simplyfied useSwipe * chore: started with docs * chore: enhanced useSwipe demo * chore: added unit tests * fix: added passive event support, explicit scrolling prevention and minor changes * fix: further improvements and typing fixes * fix: implemented review recommendations * chore: refactor * fix: added more review recommendations * chore: refactor * chore: update docs Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent c1fdc45 commit f11364a

File tree

10 files changed

+515
-3
lines changed

10 files changed

+515
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Collection of essential Vue Composition Utilities
77
<a href="https://www.npmjs.com/package/@vueuse/core" target="__blank"><img src="https://img.shields.io/npm/v/@vueuse/core?color=a1b858&label=" alt="NPM version"></a>
88
<a href="https://www.npmjs.com/package/@vueuse/core" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@vueuse/core?color=50a36f&label="></a>
99
<a href="https://vueuse.js.org" target="__blank"><img src="https://img.shields.io/static/v1?label=&message=docs%20%26%20demos&color=1e8a7a" alt="Docs & Demos"></a>
10-
<img alt="Function Count" src="https://img.shields.io/badge/-102%20functions-13708a">
10+
<img alt="Function Count" src="https://img.shields.io/badge/-103%20functions-13708a">
1111
<br>
1212
<a href="https://github.com/vueuse/vueuse" target="__blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/vueuse/vueuse?style=social"></a>
1313
</p>

indexes.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,13 @@
645645
"category": "State",
646646
"description": "reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)/[SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)"
647647
},
648+
{
649+
"name": "useSwipe",
650+
"package": "core",
651+
"docs": "https://vueuse.js.org/core/useSwipe/",
652+
"category": "Sensors",
653+
"description": "reactive swipe detection based on [`TouchEvents`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent)"
654+
},
648655
{
649656
"name": "useTimestamp",
650657
"package": "core",

packages/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export * from './useSessionStorage'
4848
export * from './useShare'
4949
export * from './useSpeechRecognition'
5050
export * from './useStorage'
51+
export * from './useSwipe'
5152
export * from './useTimestamp'
5253
export * from './useTitle'
5354
export * from './useTransition'

packages/core/useEventListener/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export declare function useEventListener<Names extends string>(
112112
* @param options
113113
*/
114114
export declare function useEventListener<EventType = Event>(
115-
target: MaybeRef<EventTarget>,
115+
target: MaybeRef<EventTarget | null | undefined>,
116116
event: string,
117117
listener: GeneralEventListener<EventType>,
118118
options?: boolean | AddEventListenerOptions

packages/core/useEventListener/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function useEventListener<Names extends string>(target: InferEventTarget<
7777
* @param listener
7878
* @param options
7979
*/
80-
export function useEventListener<EventType = Event>(target: MaybeRef<EventTarget>, event: string, listener: GeneralEventListener<EventType>, options?: boolean | AddEventListenerOptions): Fn
80+
export function useEventListener<EventType = Event>(target: MaybeRef<EventTarget | null | undefined>, event: string, listener: GeneralEventListener<EventType>, options?: boolean | AddEventListenerOptions): Fn
8181

8282
export function useEventListener(...args: any[]) {
8383
let target: MaybeRef<EventTarget> | undefined

packages/core/useSwipe/demo.vue

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
2+
<script setup lang="ts">
3+
import { computed, ref } from 'vue'
4+
import { SwipeDirection, useSwipe } from '.'
5+
6+
const target = ref<HTMLElement | null>(null)
7+
const container = ref<HTMLElement | null>(null)
8+
const containerWidth = computed(() => container.value?.offsetWidth)
9+
const left = ref('0')
10+
const opacity = ref(1)
11+
12+
const reset = () => {
13+
left.value = '0'
14+
opacity.value = 1
15+
}
16+
17+
const onSwipe = (e: TouchEvent) => {
18+
if (containerWidth.value) {
19+
if (lengthX.value < 0) {
20+
const length = Math.abs(lengthX.value)
21+
left.value = `${length}px`
22+
opacity.value = 1.1 - length / containerWidth.value
23+
}
24+
else {
25+
left.value = '0'
26+
opacity.value = 1
27+
}
28+
}
29+
}
30+
31+
const onSwipeEnd = (e: TouchEvent, direction: SwipeDirection) => {
32+
if (lengthX.value < 0 && containerWidth.value && (Math.abs(lengthX.value) / containerWidth.value) >= 0.5) {
33+
left.value = '100%'
34+
opacity.value = 0
35+
}
36+
else {
37+
left.value = '0'
38+
opacity.value = 1
39+
}
40+
}
41+
42+
const { direction, isSwiping, lengthX, lengthY } = useSwipe(target, { passive: false, onSwipe, onSwipeEnd })
43+
</script>
44+
45+
<template>
46+
<div>
47+
<div ref="container" class="container select-none">
48+
<button @click="reset">
49+
Reset
50+
</button>
51+
<div ref="target" class="overlay" :class="{animated: !isSwiping}" :style="{left, opacity}">
52+
<p>Swipe right</p>
53+
</div>
54+
</div>
55+
<p class="status">
56+
Direction: {{ direction? direction : '-' }} <br>
57+
lengthX: {{ lengthX }} | lengthY: {{ lengthY }}
58+
</p>
59+
</div>
60+
</template>
61+
62+
<style scoped>
63+
.container {
64+
position: relative;
65+
display: flex;
66+
align-items: center;
67+
justify-content: center;
68+
border: 2px dashed #ccc;
69+
height: 100;
70+
overflow: hidden;
71+
}
72+
73+
.overlay {
74+
top: 0;
75+
left: 0;
76+
width: 100%;
77+
height: 100%;
78+
position: absolute;
79+
background: #3fb983;
80+
}
81+
82+
.overlay.animated {
83+
transition: all 0.2s ease-in-out;
84+
}
85+
86+
.overlay > p {
87+
color: #fff;
88+
font-weight: bold;
89+
text-align: center;
90+
overflow:hidden;
91+
white-space: nowrap;
92+
}
93+
94+
.status {
95+
text-align: center;
96+
}
97+
</style>

packages/core/useSwipe/index.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
category: Sensors
3+
---
4+
5+
# useSwipe
6+
7+
Reactive swipe detection based on [`TouchEvents`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent).
8+
9+
## Usage
10+
11+
```html {16-20}
12+
<template>
13+
<div ref="el">
14+
Swipe here
15+
</div>
16+
</template>
17+
18+
<script>
19+
setup() {
20+
const el = ref(null)
21+
const { isSwiping, direction } = useSwipe(el)
22+
23+
return { el, isSwiping, direction }
24+
}
25+
</script>
26+
```
27+
28+
<!--FOOTER_STARTS-->
29+
## Type Declarations
30+
31+
```typescript
32+
export declare enum SwipeDirection {
33+
UP = "UP",
34+
RIGHT = "RIGHT",
35+
DOWN = "DOWN",
36+
LEFT = "LEFT",
37+
}
38+
export interface SwipeOptions extends ConfigurableWindow {
39+
/**
40+
* Register events as passive
41+
*
42+
* @default true
43+
*/
44+
passive?: boolean
45+
/**
46+
* @default 50
47+
*/
48+
threshold?: number
49+
/**
50+
* Callback on swipe start
51+
*/
52+
onSwipeStart?: (e: TouchEvent) => void
53+
/**
54+
* Callback on swipe moves
55+
*/
56+
onSwipe?: (e: TouchEvent) => void
57+
/**
58+
* Callback on swipe ends
59+
*/
60+
onSwipeEnd?: (e: TouchEvent, direction: SwipeDirection) => void
61+
}
62+
export interface SwipeReturn {
63+
isPassiveEventSupported: boolean
64+
isSwiping: ComputedRef<boolean>
65+
direction: ComputedRef<SwipeDirection | null>
66+
coordsStart: {
67+
readonly x: number
68+
readonly y: number
69+
}
70+
coordsEnd: {
71+
readonly x: number
72+
readonly y: number
73+
}
74+
lengthX: ComputedRef<number>
75+
lengthY: ComputedRef<number>
76+
stop: () => void
77+
}
78+
/**
79+
* Reactive swipe detection.
80+
*
81+
* @see {@link https://vueuse.js.org/useSwipe}
82+
* @param target
83+
* @param options
84+
*/
85+
export declare function useSwipe(
86+
target: MaybeRef<EventTarget | null | undefined>,
87+
options?: SwipeOptions
88+
): {
89+
isPassiveEventSupported: boolean
90+
isSwiping: Ref<boolean>
91+
direction: ComputedRef<SwipeDirection | null>
92+
coordsStart: {
93+
x: number
94+
y: number
95+
}
96+
coordsEnd: {
97+
x: number
98+
y: number
99+
}
100+
lengthX: ComputedRef<number>
101+
lengthY: ComputedRef<number>
102+
stop: () => void
103+
}
104+
```
105+
106+
## Source
107+
108+
[Source](https://github.com/vueuse/vueuse/blob/main/packages/core/useSwipe/index.ts)[Demo](https://github.com/vueuse/vueuse/blob/main/packages/core/useSwipe/demo.vue)[Docs](https://github.com/vueuse/vueuse/blob/main/packages/core/useSwipe/index.md)
109+
110+
111+
<!--FOOTER_ENDS-->

packages/core/useSwipe/index.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { SwipeDirection } from './index'
2+
import { useSetup } from '../../.test'
3+
import { useSwipe } from '.'
4+
import each from 'jest-each'
5+
6+
describe('useSwipe', () => {
7+
const target = document.createElement('div')
8+
target.id = 'target'
9+
document.body.appendChild(target)
10+
11+
const mockTouchEventInit = (x: number, y: number): TouchEventInit => ({
12+
touches: [{
13+
clientX: x,
14+
clientY: y,
15+
altitudeAngle: 0,
16+
azimuthAngle: 0,
17+
force: 0,
18+
identifier: 0,
19+
pageX: 0,
20+
pageY: 0,
21+
radiusX: 0,
22+
radiusY: 0,
23+
rotationAngle: 0,
24+
screenX: 0,
25+
screenY: 0,
26+
target,
27+
touchType: 'direct',
28+
}],
29+
})
30+
31+
const mockTouchStart = (x: number, y: number) => new TouchEvent('touchstart', mockTouchEventInit(x, y))
32+
const mockTouchMove = (x: number, y: number) => new TouchEvent('touchmove', mockTouchEventInit(x, y))
33+
const mockTouchEnd = (x: number, y: number) => new TouchEvent('touchend', mockTouchEventInit(x, y))
34+
35+
const mockTouchEvents = (target: EventTarget, coords: Array<number[]>) => {
36+
coords.forEach(([x, y], i) => {
37+
if (i === 0) target.dispatchEvent(mockTouchStart(x, y))
38+
else if (i === coords.length - 1) target.dispatchEvent(mockTouchEnd(x, y))
39+
else
40+
target.dispatchEvent(mockTouchMove(x, y))
41+
})
42+
}
43+
44+
let onSwipe: jest.Mock
45+
let onSwipeEnd: jest.Mock
46+
const threshold = 30
47+
48+
beforeEach(() => {
49+
onSwipe = jest.fn((e: TouchEvent) => {})
50+
onSwipeEnd = jest.fn((e: TouchEvent, direction: SwipeDirection) => {})
51+
})
52+
53+
it('threshold not exceeded', () => {
54+
useSetup(() => {
55+
useSwipe(target, { threshold, onSwipe, onSwipeEnd })
56+
57+
mockTouchEvents(target, [[0, 0], [threshold - 1, 0], [threshold - 1, 0]])
58+
59+
expect(onSwipe.mock.calls.length).toBe(0)
60+
expect(onSwipeEnd.mock.calls.length).toBe(0)
61+
})
62+
})
63+
64+
it('threshold exceeded', () => {
65+
useSetup(() => {
66+
useSwipe(target, { threshold, onSwipe, onSwipeEnd })
67+
68+
mockTouchEvents(target, [[0, 0], [threshold / 2, 0], [threshold, 0], [threshold, 0]])
69+
70+
expect(onSwipe.mock.calls.length).toBe(1)
71+
expect(onSwipeEnd.mock.calls.length).toBe(1)
72+
})
73+
})
74+
75+
it('reactivity', () => {
76+
useSetup(() => {
77+
const { isSwiping, direction, lengthX, lengthY } = useSwipe(target, { threshold, onSwipe, onSwipeEnd })
78+
79+
target.dispatchEvent(mockTouchStart(0, 0))
80+
expect(isSwiping.value).toBeFalsy()
81+
expect(direction.value).toBeNull()
82+
expect(lengthX.value).toBe(0)
83+
expect(lengthY.value).toBe(0)
84+
85+
target.dispatchEvent(mockTouchMove(threshold, 5))
86+
expect(isSwiping.value).toBeTruthy()
87+
expect(direction.value).toBe(SwipeDirection.RIGHT)
88+
expect(lengthX.value).toBe(-threshold)
89+
expect(lengthY.value).toBe(-5)
90+
91+
target.dispatchEvent(mockTouchEnd(threshold, 5))
92+
})
93+
})
94+
95+
each([
96+
[SwipeDirection.UP, [[0, 2 * threshold], [0, threshold], [0, threshold]]],
97+
[SwipeDirection.DOWN, [[0, 0], [0, threshold], [0, threshold]]],
98+
[SwipeDirection.LEFT, [[2 * threshold, 0], [threshold, 0], [threshold, 0]]],
99+
[SwipeDirection.RIGHT, [[0, 0], [threshold, 0], [threshold, 0]]],
100+
]).it('swipe %s', (expected, coords) => {
101+
useSetup(() => {
102+
const { direction } = useSwipe(target, { threshold, onSwipe, onSwipeEnd })
103+
104+
mockTouchEvents(target, [[0, 2 * threshold], [0, threshold], [0, threshold]])
105+
106+
expect(direction.value).toBe(SwipeDirection.UP)
107+
expect(onSwipeEnd.mock.calls[0][1]).toBe(SwipeDirection.UP)
108+
})
109+
})
110+
})

0 commit comments

Comments
 (0)