Skip to content

Commit

Permalink
refactor(maz-ui): useSwipe - add documentation and SSR compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisMazel committed Sep 4, 2024
1 parent f5e97dd commit 131dbb6
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 45 deletions.
219 changes: 214 additions & 5 deletions packages/docs/docs/composables/use-swipe.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,223 @@
---
title: useSwipe
description: Vue composable for handling mobile swipe
description: useSwipe is a Vue 3 composable that simplifies the management of "swipe" interactions on HTML elements.
---

# {{ $frontmatter.title }}

{{ $frontmatter.description }}

## Usage
## Introduction

::: warning
WIP
:::
`useSwipe` allows you to detect and react to swiping movements on an HTML element. It provides you with various information about the swipe movement, such as the direction, distance, start, and end coordinates.
You can use this information to implement specific interactions in your application, such as scrolling a carousel, opening a side menu, etc.

## Key Features

- Detects swipes in all 4 directions (left, right, up, down)
- Provides key information about the swipe movement (start/end coordinates, horizontal/vertical distance)
- Allows you to configure callbacks for each swipe direction
- Possibility to customize the swipe detection threshold
- Automatically handles the addition and removal of event listeners
- Can be used with any HTML element

## Basic Usage

<div ref="swipeContainer" class="swipe-container">
<p>
Swipe in any direction<br>
<span class="maz-text-xs maz-text-muted">
(You should use a real device or a mobile simulator to test the swipe functionality)
</span>
<br><br>
Last swipe direction: {{lastSwipeDirection || 'None'}}
</p>
</div>

Here's an example of using the useSwipe composable:

```vue
<template>
<div ref="swipeContainer" class="swipe-container">
<p>
Swipe in any direction<br>
<span class="maz-text-sm maz-text-muted">
(You should use a real device or a mobile simulator to test the swipe functionality)
</span>
<br><br>
Last swipe direction: {{lastSwipeDirection || 'None'}}
</p>
</div>
</template>
<script lang="ts" setup>
import { useSwipe } from 'maz-ui'
import { onMounted, onUnmounted, ref } from 'vue'
const swipeContainer = ref<HTMLDivElement>()
const lastSwipeDirection = ref<string>('None')
const { xDiff, yDiff, start, stop } = useSwipe({
element: swipeContainer,
onLeft: () => lastSwipeDirection.value = 'Swiped left',
onRight: () => lastSwipeDirection.value = 'Swiped right',
onUp: () => lastSwipeDirection.value = 'Swiped up',
onDown: () => lastSwipeDirection.value = 'Swiped down',
threshold: 50,
})
/**
* Start listening for swipe events
* You can also call start() directly
* But it's better to call it in onMounted specially for SSR
*/
onMounted(() => {
start()
})
// Stop listening for swipe events
onUnmounted(() => {
stop()
})
</script>
<style>
.swipe-container {
border: 1px solid #e2e2e3;
border-radius: 10px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 1.2rem;
}
</style>
```

<script lang="ts" setup>
import { useSwipe } from 'maz-ui'
import { onMounted, onUnmounted, ref } from 'vue'

const swipeContainer = ref<HTMLDivElement>()

const lastSwipeDirection = ref<string>('None')

const { xDiff, yDiff, start, stop } = useSwipe({
element: swipeContainer,
onLeft: () => lastSwipeDirection.value = 'Swiped left',
onRight: () => lastSwipeDirection.value = 'Swiped right',
onUp: () => lastSwipeDirection.value = 'Swiped up',
onDown: () => lastSwipeDirection.value = 'Swiped down',
threshold: 50,
})

onMounted(() => {
start()
})

onUnmounted(() => {
stop()
})
</script>

<style>
.swipe-container {
border: 1px solid #e2e2e3;
border-radius: 10px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 1.2rem;
}
</style>

In this example, the `useSwipe` composable is used to detect swiping movements on an HTML element with the `container` class. When a swipe is detected, the horizontal (`xDiff`) and vertical (`yDiff`) coordinates of the movement are displayed.

Additionally, callbacks are defined for each swipe direction (`onLeft`, `onRight`, `onUp`, `onDown`), which will be called when the corresponding swipe is detected.

The swipe detection threshold is also customized to 50 pixels.

## Options

`useSwipe` accepts an options object with the following properties:

```ts
interface UseSwipeOptions {
/**
* The HTML element on which the swipe events will be handled. This can be either a direct reference to the element or a CSS selector.
* @required
*/
element: HTMLElement | string
/** Callback executed when a left swipe is detected. */
onLeft?: (event: TouchEvent) => void
/** Callback executed when a right swipe is detected. */
onRight?: (event: TouchEvent) => void
/** Callback executed when an up swipe is detected. */
onUp?: (event: TouchEvent) => void
/** Callback executed when a down swipe is detected. */
onDown?: (event: TouchEvent) => void
/**
* The minimum distance the swipe must travel to be considered valid.
* @default 50
*/
threshold?: number
/**
* Whether to prevent the default behavior of the touchmove event.
* @default false
*/
preventDefaultOnTouchMove?: boolean
/**
* Whether to prevent the default behavior of the mousewheel event.
* @default false
*/
preventDefaultOnMouseWheel?: boolean
/**
* Whether to trigger the swipe event immediately on touchstart/mousedown.
* @default false
*/
immediate?: boolean
/**
* Whether to trigger the swipe event only on touchend/mouseup.
* @default false
*/
triggerOnEnd?: boolean
}
```

## Composable Return

`useSwipe` returns an object with the following properties:

```ts
interface UseSwipeReturn {
/** A function to start listening for swipe events. */
start: () => void
/** A function to stop listening for swipe events. */
stop: () => void
/** The horizontal difference between the start and end coordinates of the swipe. */
xDiff: Ref<number | undefined>
/** The vertical difference between the start and end coordinates of the swipe. */
yDiff: Ref<number | undefined>
/** The horizontal start coordinate of the swipe. */
xStart: Ref<number | undefined>
/** The horizontal end coordinate of the swipe. */
xEnd: Ref<number | undefined>
/** The vertical start coordinate of the swipe. */
yStart: Ref<number | undefined>
/** The vertical end coordinate of the swipe. */
yEnd: Ref<number | undefined>
}
```

## Notes

- Make sure to call the `start()` function to start listening for swipe events.
- You can call the `stop()` function to stop listening for swipe events.
- If you use the composable in a Vue component, make sure to call it in the `setup()` and clean up the event listeners in the `onUnmounted()`.
- The composable automatically handles the addition and removal of event listeners based on the provided options.
- You can customize the swipe detection threshold by modifying the `threshold` option.
- If you want to prevent the default behavior of touchmove or mousewheel events, you can set the `preventDefaultOnTouchMove` and `preventDefaultOnMouseWheel` options, respectively.
19 changes: 15 additions & 4 deletions packages/lib/modules/composables/useSwipe.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ref } from 'vue'
import type { MaybeRef } from 'vue'
import { computed, ref, toValue } from 'vue'
import { Swipe, type SwipeOptions } from '../helpers/swipe-handler'

export function useSwipe(options: Omit<SwipeOptions, 'onValuesChanged'>) {
export function useSwipe(options: Omit<SwipeOptions, 'onValuesChanged' | 'element'> & { element: MaybeRef<HTMLElement> | string | null | undefined }) {
const xDiff = ref<number>()
const yDiff = ref<number>()
const xStart = ref<number>()
const xEnd = ref<number>()
const yStart = ref<number>()
const yEnd = ref<number>()

const element = computed(() => toValue(options.element))

const swiper = new Swipe({
...options,
element: options.element,
element: element.value,
onValuesChanged(values) {
xDiff.value = values.xDiff
yDiff.value = values.yDiff
Expand All @@ -29,7 +32,15 @@ export function useSwipe(options: Omit<SwipeOptions, 'onValuesChanged'>) {
xEnd,
yStart,
yEnd,
start: swiper.start,
start: () => {
if (element.value) {
swiper.options.element = element.value
swiper.start()
}
else {
swiper.start()
}
},
stop: swiper.stop,
}
}
77 changes: 46 additions & 31 deletions packages/lib/modules/helpers/swipe-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface SwipeOptions {
* The element on which the swipe events will be handled.
* @default null
*/
element: HTMLElement | string
element?: HTMLElement | string | null
/**
* Callback function to be executed when a left swipe is detected.
* @default undefined
Expand Down Expand Up @@ -85,16 +85,16 @@ type DefaultSwipeOptions = Required<

type SwipeOptionsWithDefaults = SwipeOptions & DefaultSwipeOptions

export class Swipe {
private readonly defaultOptions: DefaultSwipeOptions = {
preventDefaultOnTouchMove: false,
preventDefaultOnMouseWheel: false,
threshold: 50,
immediate: false,
triggerOnEnd: false,
}
const defaultOptions: DefaultSwipeOptions = {
preventDefaultOnTouchMove: false,
preventDefaultOnMouseWheel: false,
threshold: 50,
immediate: false,
triggerOnEnd: false,
}

public readonly element: HTMLElement
export class Swipe {
public element: HTMLElement

public xStart: number | undefined
public yStart: number | undefined
Expand All @@ -108,39 +108,33 @@ export class Swipe {
private readonly onToucheEndCallback: (event: TouchEvent) => void
private readonly onMouseWheelCallback: (event: Event) => void

private options: SwipeOptionsWithDefaults
public readonly start: (element?: typeof this.options.element) => void
public readonly stop: () => void

constructor(readonly inputOption: SwipeOptions) {
this.options = { ...this.defaultOptions, ...inputOption }

if (!this.options.element) {
throw new Error(
'[SwipeHandler] Element should be provided. Its can be a string selector or an HTMLElement',
)
}
public options: SwipeOptionsWithDefaults

if (typeof this.options.element === 'string') {
const foundElement = document.querySelector(this.options.element)
if (!(foundElement instanceof HTMLElement)) {
throw new TypeError('[SwipeHandler] String selector for element is not found')
}
this.element = foundElement
}
else {
this.element = this.options.element
}
constructor(readonly inputOption: SwipeOptions) {
this.options = { ...defaultOptions, ...inputOption }

this.onToucheStartCallback = this.toucheStartHandler.bind(this)
this.onToucheMoveCallback = this.handleTouchMove.bind(this)
this.onToucheEndCallback = this.handleTouchEnd.bind(this)
this.onMouseWheelCallback = this.handleMouseWheel.bind(this)
this.start = this.startListening.bind(this)
this.stop = this.stopListening.bind(this)

if (this.options.element) {
this.setElement(this.options.element)
}

if (this.options.immediate) {
this.start()
}
}

start() {
private startListening() {
this.setElement(this.options.element)

this.element.addEventListener('touchstart', this.onToucheStartCallback, { passive: true })
this.element.addEventListener('touchmove', this.onToucheMoveCallback, { passive: true })
if (this.options.triggerOnEnd) {
Expand All @@ -151,7 +145,7 @@ export class Swipe {
}
}

public stop() {
private stopListening() {
this.element.removeEventListener('touchstart', this.onToucheStartCallback)
this.element.removeEventListener('touchmove', this.onToucheMoveCallback)
this.element.removeEventListener('touchend', this.onToucheEndCallback)
Expand All @@ -161,6 +155,27 @@ export class Swipe {
}
}

private setElement(element?: HTMLElement | string | null) {
if (!element) {
console.error(
'[maz-ui][SwipeHandler](setElement) Element should be provided. Its can be a string selector or an HTMLElement',
)
return
}

if (typeof element === 'string') {
const foundElement = document.querySelector(element)
if (!(foundElement instanceof HTMLElement)) {
console.error('[maz-ui][SwipeHandler](setElement) String selector for element is not found')
return
}
this.element = foundElement
}
else {
this.element = element
}
}

private handleMouseWheel(event: Event) {
event.preventDefault()
}
Expand Down
Loading

0 comments on commit 131dbb6

Please sign in to comment.