Repository: exadel-inc/esl
Pull request #2025 (2023-11-02 20:17:39)

fix(esl-event-utils): `ESLSwipeGestureTarget` target prevents inside element actions
Closes: #2024


Patch:
diff --git a/pages/views/draft/swipe-demo.njk b/pages/views/draft/swipe-demo.njk
index 45857619d..7a7c7ff4c 100644
--- a/pages/views/draft/swipe-demo.njk
+++ b/pages/views/draft/swipe-demo.njk
@@ -24,7 +24,9 @@ tags: [draft]
     {% endcode %}
 
     <div class="d-flex">
-      <esl-d-swipe-area class="m-auto" placeholder="Make swipes in that area"></esl-d-swipe-area>
+      <esl-d-swipe-area class="m-auto" placeholder="Make swipes in that area">
+        <button class="btn btn-sec-blue" onclick="alert('Clicked')">Click</button>
+      </esl-d-swipe-area>
     </div>
 
   </div>
diff --git a/src/modules/esl-event-listener/README.md b/src/modules/esl-event-listener/README.md
index c4bf4b0b7..6119cd93a 100644
--- a/src/modules/esl-event-listener/README.md
+++ b/src/modules/esl-event-listener/README.md
@@ -615,9 +615,6 @@ ESLSwipeGestureTarget.for(el: Element, settings?: ESLSwipeGestureSetting): ESLSw
 - `el` - `Element` to listen for swipe events on.
 - `settings` - optional settings (`ESLSwipeGestureSetting`)
 
-**Note**: `ESLSwipeGestureTarget` uses Pointer Events API and requires corresponding `touch-action` CSS 
-property to be specified on the target element.
-
 Usage example:
 
 ```typescript
diff --git a/src/modules/esl-event-listener/core/targets/swipe.target.event.ts b/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
index 465f90ddc..2df01e1d2 100644
--- a/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
+++ b/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
@@ -48,7 +48,7 @@ export class ESLSwipeGestureEvent extends UIEvent implements ESLSwipeGestureEven
   public readonly duration: number;
 
   protected constructor(eventName: SwipeEventName, target: Element, swipeInfo: ESLSwipeGestureEventInfo) {
-    super(eventName, {bubbles: true, cancelable: true});
+    super(eventName, {bubbles: false, cancelable: true});
     overrideEvent(this, 'target', target);
     Object.assign(this, swipeInfo);
   }
diff --git a/src/modules/esl-event-listener/core/targets/swipe.target.ts b/src/modules/esl-event-listener/core/targets/swipe.target.ts
index d021bbff9..35efeef4b 100644
--- a/src/modules/esl-event-listener/core/targets/swipe.target.ts
+++ b/src/modules/esl-event-listener/core/targets/swipe.target.ts
@@ -1,7 +1,7 @@
 import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target';
+import {getTouchPoint, isMouseEvent, isTouchEvent} from '../../../esl-utils/dom/events/misc';
 import {resolveDomTarget} from '../../../esl-utils/abstract/dom-target';
 import {isElement} from '../../../esl-utils/dom/api';
-import {bind} from '../../../esl-utils/decorators/bind';
 import {resolveCSSSize} from '../../../esl-utils/dom/units';
 
 import {ESLEventListener} from '../listener';
@@ -58,14 +58,24 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
     this.config = Object.assign({}, ESLSwipeGestureTarget.defaultConfig, settings);
   }
 
+  /** @returns opposite to the current startEvent event name */
+  protected get endEventName(): string {
+    if (isTouchEvent(this.startEvent)) return 'touchend';
+    if (isMouseEvent(this.startEvent)) return 'mouseup';
+    return 'pointerup';
+  }
+
   /**
    * Saves swipe start event target, time when swipe started, pointerdown event and coordinates.
    * @param startEvent - initial pointer event
    */
-  @bind
   protected handleStart(startEvent: PointerEvent): void {
     this.startEvent = startEvent;
-    this.target.setPointerCapture && this.target.setPointerCapture(startEvent.pointerId);
+    ESLEventListener.subscribe(this, this.handleEnd, {
+      event: this.endEventName,
+      once: true,
+      target: window
+    });
   }
 
   /**
@@ -74,9 +84,13 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
    */
   protected resolveEventDetails(endEvent: PointerEvent): ESLSwipeGestureEventInfo {
     const {startEvent} = this;
+    const startPoint = getTouchPoint(startEvent);
+    const endPoint = getTouchPoint(endEvent);
+
     const duration = endEvent.timeStamp - startEvent.timeStamp;
-    const distanceX = Math.round(endEvent.clientX - startEvent.clientX);
-    const distanceY = Math.round(endEvent.clientY - startEvent.clientY);
+    const distanceX = Math.round(endPoint.x - startPoint.x);
+    const distanceY = Math.round(endPoint.y - startPoint.y);
+
     const distance = Math.round(Math.hypot(distanceX, distanceY, 2));
     const angle = Math.round((Math.atan2(distanceY, distanceX) * 180 / Math.PI + 90 + 360) % 360);
     const direction = this.resolveDirection(distanceX, distanceY);
@@ -100,17 +114,15 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
    * occurred and the result gestures accepts {@link ESLSwipeGestureTarget} configuration
    * @param endEvent - `pointerup` event
    */
-  @bind
   protected handleEnd(endEvent: PointerEvent): void {
-    this.target.releasePointerCapture && this.target.releasePointerCapture(endEvent.pointerId);
-
     const eventDetails = this.resolveEventDetails(endEvent);
 
     // return if swipe took too long or distance is too short
     if (!this.isGestureAcceptable(eventDetails)) return;
 
+    const event = ESLSwipeGestureEvent.fromConfig('swipe', this.target, eventDetails);
     // fire `swipe` event on the element that started the swipe
-    this.dispatchEvent(ESLSwipeGestureEvent.fromConfig('swipe', this.target, eventDetails));
+    if (!this.dispatchEvent(event)) endEvent.preventDefault();
   }
 
   /**
@@ -132,8 +144,7 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
 
     if (this.getEventListeners().length > 1) return;
     const {target} = this;
-    ESLEventListener.subscribe(this, this.handleStart, {event: 'pointerdown', capture: false, target});
-    ESLEventListener.subscribe(this, this.handleEnd, {event: 'pointerup', capture: false, target});
+    ESLEventListener.subscribe(this, this.handleStart, {event: 'mousedown touchstart', target});
   }
 
   /**


Test patch:
diff --git a/src/modules/esl-event-listener/test/targets/swipe.test.ts b/src/modules/esl-event-listener/test/targets/swipe.test.ts
index 0b76d9310..8ccf06cc7 100644
--- a/src/modules/esl-event-listener/test/targets/swipe.test.ts
+++ b/src/modules/esl-event-listener/test/targets/swipe.test.ts
@@ -1,5 +1,6 @@
 import {ESLSwipeGestureEvent, ESLSwipeGestureTarget} from '../../core/targets/swipe.target';
 import {promisifyTimeout} from '../../../esl-utils/async/promise/timeout';
+import {overrideEvent} from '../../../esl-utils/dom/events/misc';
 
 describe('ESLSwipeGestureTarget EventTarget', () => {
   describe('ESLSwipeGestureTarget do not throws error on incorrect input (silent processing)', () => {
@@ -59,6 +60,15 @@ describe('ESLSwipeGestureTarget EventTarget', () => {
   });
 
   describe('ESLSwipeGestureTarget detects swipe gesture', () => {
+    const START_EVENT = 'mousedown';
+    const END_EVENT = 'mouseup';
+
+    const createEvent = (type: string, details: Record<string, any>): Event => {
+      const event = new MouseEvent(type, {bubbles: true});
+      Object.keys(details).forEach((key: keyof Event) => overrideEvent(event, key, details[key]));
+      return event;
+    };
+
     const $el = document.createElement('div');
     const target = ESLSwipeGestureTarget.for($el, {timeout: 50});
     const listener = jest.fn();
@@ -68,70 +78,70 @@ describe('ESLSwipeGestureTarget EventTarget', () => {
     beforeEach(() => listener.mockReset());
 
     test('ESLSwipeGestureTarget detects left swipe', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 50, clientY: 100}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 50, pageY: 100, target: $el}));
       expect(listener).lastCalledWith(expect.any(ESLSwipeGestureEvent));
       expect(listener).lastCalledWith(expect.objectContaining({direction: 'left'}));
       expect(listener).lastCalledWith(expect.objectContaining({angle: 270}));
     });
 
     test('ESLSwipeGestureTarget detects right swipe', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 150, clientY: 100}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 150, pageY: 100, target: $el}));
       expect(listener).lastCalledWith(expect.any(ESLSwipeGestureEvent));
       expect(listener).lastCalledWith(expect.objectContaining({direction: 'right'}));
       expect(listener).lastCalledWith(expect.objectContaining({angle: 90}));
     });
 
     test('ESLSwipeGestureTarget detects up swipe', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 100, clientY: 50}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 100, pageY: 50, target: $el}));
       expect(listener).lastCalledWith(expect.any(ESLSwipeGestureEvent));
       expect(listener).lastCalledWith(expect.objectContaining({direction: 'up'}));
       expect(listener).lastCalledWith(expect.objectContaining({angle: 0}));
     });
 
     test('ESLSwipeGestureTarget detects down swipe', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 100, clientY: 150}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 100, pageY: 150, target: $el}));
       expect(listener).lastCalledWith(expect.any(ESLSwipeGestureEvent));
       expect(listener).lastCalledWith(expect.objectContaining({direction: 'down'}));
       expect(listener).lastCalledWith(expect.objectContaining({angle: 180}));
     });
 
     test('ESLSwipeGestureTarget ignores short horizontal swipes', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 110, clientY: 105}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 110, pageY: 105, target: $el}));
       expect(listener).not.toBeCalled();
     });
 
     test('ESLSwipeGestureTarget ignores short vertical swipes', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 105, clientY: 110}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 105, pageY: 110, target: $el}));
       expect(listener).not.toBeCalled();
     });
 
     test('ESLSwipeGestureTarget ignores long - lasting swipe', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
       await promisifyTimeout(150);
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 210, clientY: 105}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 210, pageY: 105, target: $el}));
       expect(listener).not.toBeCalled();
     });
 
     test('ESLSwipeGestureTarget ignores unlinked events', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 210, clientY: 105}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 210, pageY: 105, target: $el}));
       expect(listener).not.toBeCalled();
     });
 
     test('ESLSwipeGestureTarget processes diagonal swipes', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 150, clientY: 150}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 150, pageY: 150, target: $el}));
       expect(listener).toHaveBeenCalledWith(expect.objectContaining({direction: 'right'}));
     });
 
     test('ESLSwipeGestureTarget processes diagonal swipes', async () => {
-      $el.dispatchEvent(Object.assign(new Event('pointerdown'), {clientX: 100, clientY: 100}));
-      $el.dispatchEvent(Object.assign(new Event('pointerup'), {clientX: 50, clientY: 50}));
+      $el.dispatchEvent(createEvent(START_EVENT, {pageX: 100, pageY: 100}));
+      window.dispatchEvent(createEvent(END_EVENT, {pageX: 50, pageY: 50, target: $el}));
       expect(listener).toHaveBeenCalledWith(expect.objectContaining({direction: 'left'}));
     });
   });