From 131dbb6fa6a9e597182ba1c98037fb54b079ef1d Mon Sep 17 00:00:00 2001 From: LouisMazel Date: Wed, 4 Sep 2024 19:03:08 +0200 Subject: [PATCH] refactor(maz-ui): useSwipe - add documentation and SSR compatibility --- packages/docs/docs/composables/use-swipe.md | 219 +++++++++++++++++- packages/lib/modules/composables/useSwipe.ts | 19 +- packages/lib/modules/helpers/swipe-handler.ts | 77 +++--- .../tests/specs/composables/useSwipe.spec.ts | 1 + .../tests/specs/helpers/swipe-handler.spec.ts | 9 +- 5 files changed, 280 insertions(+), 45 deletions(-) diff --git a/packages/docs/docs/composables/use-swipe.md b/packages/docs/docs/composables/use-swipe.md index ce8edf8638..670533615f 100644 --- a/packages/docs/docs/composables/use-swipe.md +++ b/packages/docs/docs/composables/use-swipe.md @@ -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 + +
+

+ Swipe in any direction
+ + (You should use a real device or a mobile simulator to test the swipe functionality) + +

+ Last swipe direction: {{lastSwipeDirection || 'None'}} +

+
+ +Here's an example of using the useSwipe composable: + +```vue + + + + + +``` + + + + + +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 + /** The vertical difference between the start and end coordinates of the swipe. */ + yDiff: Ref + /** The horizontal start coordinate of the swipe. */ + xStart: Ref + /** The horizontal end coordinate of the swipe. */ + xEnd: Ref + /** The vertical start coordinate of the swipe. */ + yStart: Ref + /** The vertical end coordinate of the swipe. */ + yEnd: Ref +} +``` + +## 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. diff --git a/packages/lib/modules/composables/useSwipe.ts b/packages/lib/modules/composables/useSwipe.ts index 9c373da65a..615e6d3f61 100644 --- a/packages/lib/modules/composables/useSwipe.ts +++ b/packages/lib/modules/composables/useSwipe.ts @@ -1,7 +1,8 @@ -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) { +export function useSwipe(options: Omit & { element: MaybeRef | string | null | undefined }) { const xDiff = ref() const yDiff = ref() const xStart = ref() @@ -9,9 +10,11 @@ export function useSwipe(options: Omit) { const yStart = ref() const yEnd = ref() + 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 @@ -29,7 +32,15 @@ export function useSwipe(options: Omit) { xEnd, yStart, yEnd, - start: swiper.start, + start: () => { + if (element.value) { + swiper.options.element = element.value + swiper.start() + } + else { + swiper.start() + } + }, stop: swiper.stop, } } diff --git a/packages/lib/modules/helpers/swipe-handler.ts b/packages/lib/modules/helpers/swipe-handler.ts index cad904240e..eb420dec40 100755 --- a/packages/lib/modules/helpers/swipe-handler.ts +++ b/packages/lib/modules/helpers/swipe-handler.ts @@ -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 @@ -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 @@ -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) { @@ -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) @@ -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() } diff --git a/packages/lib/tests/specs/composables/useSwipe.spec.ts b/packages/lib/tests/specs/composables/useSwipe.spec.ts index 4db7c770e8..55aa7ae40b 100644 --- a/packages/lib/tests/specs/composables/useSwipe.spec.ts +++ b/packages/lib/tests/specs/composables/useSwipe.spec.ts @@ -1,6 +1,7 @@ import { useSwipe } from '@modules/composables/useSwipe' const swipeHandlerMock = { + options: {}, start: vi.fn(), stop: vi.fn(), onValuesChanged: vi.fn(), diff --git a/packages/lib/tests/specs/helpers/swipe-handler.spec.ts b/packages/lib/tests/specs/helpers/swipe-handler.spec.ts index 1387dc8b55..5186542726 100644 --- a/packages/lib/tests/specs/helpers/swipe-handler.spec.ts +++ b/packages/lib/tests/specs/helpers/swipe-handler.spec.ts @@ -16,10 +16,6 @@ describe('given Swipe class', () => { }) describe('when creating a new instance', () => { - it('then it should throw an error if element is not provided', () => { - expect(() => new Swipe({} as SwipeOptions)).toThrow('[SwipeHandler] Element should be provided') - }) - it('then it should accept an HTMLElement', () => { const swipe = new Swipe(defaultOptions) expect(swipe.element).toBe(mockElement) @@ -34,7 +30,10 @@ describe('given Swipe class', () => { it('then it should throw an error if string selector is not found', () => { vi.spyOn(document, 'querySelector').mockReturnValue(null) - expect(() => new Swipe({ element: '#test' })).toThrow('[SwipeHandler] String selector for element is not found') + const spy = vi.spyOn(console, 'error') + // eslint-disable-next-line unused-imports/no-unused-vars + const swipe = new Swipe({ element: '#test' }) + expect(spy).toHaveBeenCalledWith('[maz-ui][SwipeHandler](setElement) String selector for element is not found') }) it('then it should start immediately if immediate option is true', () => {