From 83c795acbbc0e26b5985e63751f46fdef9fa55e2 Mon Sep 17 00:00:00 2001 From: Kate Latypova Date: Tue, 22 Aug 2023 11:46:10 +0200 Subject: [PATCH] feat(notifications): remove tinygesture (#94) --- package-lock.json | 8 +- package.json | 3 +- .../HorizontalSwiper/HorizontalSwiper.ts | 197 ++++++++++++++++++ .../Notification/NotificationWithSwipe.tsx | 20 +- 4 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 src/components/Notification/HorizontalSwiper/HorizontalSwiper.ts diff --git a/package-lock.json b/package-lock.json index 29349914..d1b31db3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,7 @@ "@gravity-ui/i18n": "^1.1.0", "@gravity-ui/icons": "^2.4.0", "lodash": "^4.17.21", - "resize-observer-polyfill": "^1.5.1", - "tinygesture": "^2.0.0" + "resize-observer-polyfill": "^1.5.1" }, "devDependencies": { "@babel/preset-env": "^7.22.6", @@ -22277,11 +22276,6 @@ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "dev": true }, - "node_modules/tinygesture": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinygesture/-/tinygesture-2.0.0.tgz", - "integrity": "sha512-Xhpo6tCvUOyVq7BmJh/WDi+9qFh5AtuUqbKoMG5vpG+PT6JLPGE9D5hl9kZlj1ZqsgHuGb1OrNAXoHh8qIDpjA==" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index a0099ff4..34dcc804 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,7 @@ "@gravity-ui/i18n": "^1.1.0", "@gravity-ui/icons": "^2.4.0", "lodash": "^4.17.21", - "resize-observer-polyfill": "^1.5.1", - "tinygesture": "^2.0.0" + "resize-observer-polyfill": "^1.5.1" }, "devDependencies": { "@babel/preset-env": "^7.22.6", diff --git a/src/components/Notification/HorizontalSwiper/HorizontalSwiper.ts b/src/components/Notification/HorizontalSwiper/HorizontalSwiper.ts new file mode 100644 index 00000000..80bbd900 --- /dev/null +++ b/src/components/Notification/HorizontalSwiper/HorizontalSwiper.ts @@ -0,0 +1,197 @@ +/* Source https://github.com/sciactive/tinygesture/blob/master/src/TinyGesture.ts */ + +export interface Options { + pressThreshold: number; + velocityThreshold: number; +} + +export interface Events { + panend: MouseEvent | TouchEvent; + panmove: MouseEvent | TouchEvent; + panstart: MouseEvent | TouchEvent; + swipeleft: MouseEvent | TouchEvent; + swiperight: MouseEvent | TouchEvent; +} + +export type Handler = (event: E) => void; + +export type Handlers = { + [E in keyof Events]: Handler[]; +}; + +const DEFAULT_OPTIONS: Options = { + velocityThreshold: 10, + pressThreshold: 8, +}; + +export class HorizontalSwiper { + touchMoveX: number | null = null; + element: Element; + + private opts: Options; + private touchStartX: number | null = null; + private touchStartY: number | null = null; + private touchEndX: number | null = null; + private touchEndY: number | null = null; + private velocityX: number | null = null; + + private thresholdX = 0; + private disregardVelocityThresholdX = 0; + + private swipedHorizontal = false; + + private handlers: Handlers = { + panstart: [], + panmove: [], + panend: [], + swipeleft: [], + swiperight: [], + }; + + private _onTouchStart: (typeof this)['onTouchStart'] = this.onTouchStart.bind(this); + private _onTouchMove: (typeof this)['onTouchMove'] = this.onTouchMove.bind(this); + private _onTouchEnd: (typeof this)['onTouchEnd'] = this.onTouchEnd.bind(this); + + constructor(elem: Element, options?: Partial) { + this.element = elem; + this.opts = Object.assign({}, DEFAULT_OPTIONS, options); + + this.element.addEventListener('touchstart', this._onTouchStart); + this.element.addEventListener('touchmove', this._onTouchMove); + this.element.addEventListener('touchend', this._onTouchEnd); + + if (!('ontouchstart' in window)) { + this.element.addEventListener('mousedown', this._onTouchStart); + + document.addEventListener('mousemove', this._onTouchMove); + document.addEventListener('mouseup', this._onTouchEnd); + } + } + + get getTouchMoveX() { + return this.touchMoveX; + } + + destroy() { + this.element.removeEventListener('touchstart', this._onTouchStart); + this.element.removeEventListener('touchmove', this._onTouchMove); + this.element.removeEventListener('touchend', this._onTouchEnd); + this.element.removeEventListener('mousedown', this._onTouchStart); + + document.removeEventListener('mousemove', this._onTouchMove); + document.removeEventListener('mouseup', this._onTouchEnd); + } + + on(type: E, fn: Handler) { + if (!this.handlers[type]) return; + + this.handlers[type].push(fn); + + return { + type, + fn, + cancel: () => this.off(type, fn), + }; + } + + off(type: E, fn: Handler) { + if (this.handlers[type]) { + const idx = this.handlers[type].indexOf(fn); + if (idx !== -1) { + this.handlers[type].splice(idx, 1); + } + } + } + + fire(type: E, event: Events[E]) { + for (let i = 0; i < this.handlers[type].length; i++) { + this.handlers[type][i](event); + } + } + + onTouchStart(event: MouseEvent | TouchEvent) { + this.thresholdX = this.threshold(); + this.disregardVelocityThresholdX = this.disregardVelocityThreshold(this); + this.touchStartX = + event.type === 'mousedown' + ? (event as MouseEvent).screenX + : (event as TouchEvent).changedTouches[0].screenX; + this.touchStartY = + event.type === 'mousedown' + ? (event as MouseEvent).screenY + : (event as TouchEvent).changedTouches[0].screenY; + this.touchMoveX = null; + this.touchEndX = null; + this.touchEndY = null; + + this.fire('panstart', event); + } + + onTouchMove(event: MouseEvent | TouchEvent) { + if (event.type === 'mousemove' && (!this.touchStartX || this.touchEndX !== null)) { + return; + } + const touchMoveX = + (event.type === 'mousemove' + ? (event as MouseEvent).screenX + : (event as TouchEvent).changedTouches[0].screenX) - (this.touchStartX ?? 0); + this.velocityX = touchMoveX - (this.touchMoveX ?? 0); + this.touchMoveX = touchMoveX; + + this.fire('panmove', event); + } + + onTouchEnd(event: MouseEvent | TouchEvent) { + if (event.type === 'mouseup' && (!this.touchStartX || this.touchEndX !== null)) { + return; + } + + this.touchEndX = + event.type === 'mouseup' + ? (event as MouseEvent).screenX + : (event as TouchEvent).changedTouches[0].screenX; + this.touchEndY = + event.type === 'mouseup' + ? (event as MouseEvent).screenY + : (event as TouchEvent).changedTouches[0].screenY; + + this.fire('panend', event); + + const x = this.touchEndX - (this.touchStartX ?? 0); + const absX = Math.abs(x); + const y = this.touchEndY - (this.touchStartY ?? 0); + const absY = Math.abs(y); + + if (absX > this.thresholdX) { + this.swipedHorizontal = absX >= absY && absX > this.thresholdX; + + if (this.swipedHorizontal) { + if (x < 0) { + // Left swipe. + if ( + (this.velocityX ?? 0) < -this.opts.velocityThreshold || + x < -this.disregardVelocityThresholdX + ) { + this.fire('swipeleft', event); + } + } else { + // Right swipe. + if ( + (this.velocityX ?? 0) > this.opts.velocityThreshold || + x > this.disregardVelocityThresholdX + ) { + this.fire('swiperight', event); + } + } + } + } + } + + threshold() { + return Math.max(25, Math.floor(0.15 * (window.innerWidth || document.body.clientWidth))); + } + + disregardVelocityThreshold(self: HorizontalSwiper) { + return Math.floor(0.5 * self.element.clientWidth); + } +} diff --git a/src/components/Notification/NotificationWithSwipe.tsx b/src/components/Notification/NotificationWithSwipe.tsx index ce5ebecc..9bfc1f6c 100644 --- a/src/components/Notification/NotificationWithSwipe.tsx +++ b/src/components/Notification/NotificationWithSwipe.tsx @@ -1,10 +1,10 @@ import React from 'react'; import clamp from 'lodash/clamp'; -import TinyGesture from 'tinygesture'; import {block} from '../utils/cn'; +import {HorizontalSwiper} from './HorizontalSwiper/HorizontalSwiper'; import {Notification} from './Notification'; import {NotificationProps, NotificationSwipeActionProps} from './definitions'; @@ -67,27 +67,23 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p else if (position === 'notification') startX = notificationX; else startX = rightActionX; - const gesture = new TinyGesture(element, { - velocityThreshold: 10, - mouseSupport: true, - diagonalSwipes: false, - }); + const swiper = new HorizontalSwiper(element); element.style.transform = `translateX(${startX}px)`; - gesture.on('panstart', () => { + swiper.on('panstart', () => { notificationWrapperElement.style.opacity = `0.5`; element.style.transition = 'transform 0s'; }); - gesture.on('panmove', () => { + swiper.on('panmove', () => { const x = getX(); if (x === undefined) return; element.style.transform = `translateX(${x}px)`; }); - gesture.on('panend', () => { + swiper.on('panend', () => { element.style.transition = 'transform 0.2s'; const x = getX(); @@ -114,13 +110,13 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p }); function getX() { - if (!gesture.touchMoveX) return undefined; + if (!swiper.getTouchMoveX) return undefined; - return clamp(startX + gesture.touchMoveX, rightActionX, leftActionX); + return clamp(startX + swiper.getTouchMoveX, rightActionX, leftActionX); } return () => { - gesture.destroy(); + swiper.destroy(); }; }, [leftAction, position, rightAction, swipeThreshold]);