Repository: exadel-inc/esl
Pull request #2080 (2023-11-30 02:15:51)

fix(esl-event-listener): batch of esl-event-listener fixes and utilities unification


Patch:
diff --git a/src/modules/esl-event-listener/core/api.ts b/src/modules/esl-event-listener/core/api.ts
index ae3ab98b6..963afa600 100644
--- a/src/modules/esl-event-listener/core/api.ts
+++ b/src/modules/esl-event-listener/core/api.ts
@@ -110,12 +110,9 @@ export class ESLEventUtils {
 }
 
 function emptySubscriptionWarning(host: object, descriptor: ESLListenerDescriptor, handler: ESLListenerHandler): void {
-  console.warn('[ESL]: Empty subscription %o', {
-    descriptor,
-    handler,
-    event: resolveProperty(descriptor.event, host),
-    target: resolveProperty(descriptor.target, host)
-  });
+  const event = resolveProperty(descriptor.event, host);
+  const target = resolveProperty(descriptor.target, host);
+  console.warn('[ESL]: Empty subscription %o', {host, descriptor, handler, event, target});
 }
 
 /** @deprecated alias for {@link ESLEventUtils} */
diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts
index 7bdffdd84..17bff4ac3 100644
--- a/src/modules/esl-event-listener/core/listener.ts
+++ b/src/modules/esl-event-listener/core/listener.ts
@@ -110,14 +110,15 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
   /** Executes a handler if the passed event is accepted by the selector */
   protected handleDelegation(e: Event, handler: EventListener): void {
     const {delegate} = this;
-    const target = e.target;
-    const current = e.currentTarget;
+    const {target, currentTarget} = e;
 
     if (typeof delegate !== 'string' || !delegate) return;
-    if (!isElement(target) || !isElement(current)) return;
+    if (!isElement(target)) return;
 
     const $delegate = target.closest(delegate);
-    if (current.contains($delegate)) handler(Object.assign(e, {$delegate}));
+    if (isElement(currentTarget) && !currentTarget.contains($delegate)) return;
+
+    handler(Object.assign(e, {$delegate}));
   }
 
   /**
diff --git a/src/modules/esl-event-listener/core/targets/intersection.target.ts b/src/modules/esl-event-listener/core/targets/intersection.target.ts
index c9e62073d..1594fc107 100644
--- a/src/modules/esl-event-listener/core/targets/intersection.target.ts
+++ b/src/modules/esl-event-listener/core/targets/intersection.target.ts
@@ -56,7 +56,7 @@ export class ESLIntersectionTarget extends SyntheticEventTarget {
 
   /** Subscribes to the observed target {@link Element} changes */
   public override addEventListener(callback: EventListener): void;
-  public override addEventListener(event: 'intersects', callback: EventListener): void;
+  public override addEventListener(event: typeof ESLIntersectionEvent.type, callback: EventListener): void;
   public override addEventListener(event: any, callback: EventListener = event): void {
     if (typeof event !== 'string') event = ESLIntersectionEvent.type;
     if (event !== ESLIntersectionEvent.type) {
@@ -70,7 +70,7 @@ export class ESLIntersectionTarget extends SyntheticEventTarget {
 
   /** Unsubscribes from the observed target {@link Element} changes */
   public override removeEventListener(callback: EventListener): void;
-  public override removeEventListener(event: 'intersects', callback: EventListener): void;
+  public override removeEventListener(event: typeof ESLIntersectionEvent.type, callback: EventListener): void;
   public override removeEventListener(event: any, callback: EventListener = event): void {
     if (typeof event !== 'string') event = ESLIntersectionEvent.type;
     if (event !== ESLIntersectionEvent.type) return;
diff --git a/src/modules/esl-event-listener/core/targets/resize.target.event.ts b/src/modules/esl-event-listener/core/targets/resize.target.event.ts
index a8b9d0e9b..4770cccf5 100644
--- a/src/modules/esl-event-listener/core/targets/resize.target.event.ts
+++ b/src/modules/esl-event-listener/core/targets/resize.target.event.ts
@@ -4,6 +4,8 @@ import type {ESLResizeObserverTarget} from './resize.target';
 
 /** Custom event that {@link ESLResizeObserverTarget} produces */
 export class ESLElementResizeEvent extends UIEvent implements ResizeObserverEntry {
+  public static readonly type = 'resize';
+
   /** A reference to the {@link Element} or SVGElement being observed */
   public override readonly target: Element;
   /** A reference to the {@link ESLResizeObserverTarget} */
@@ -34,7 +36,7 @@ export class ESLElementResizeEvent extends UIEvent implements ResizeObserverEntr
   public readonly devicePixelContentBoxSize: readonly ResizeObserverSize[];
 
   protected constructor(target: Element) {
-    super('resize', {bubbles: false, cancelable: false});
+    super(ESLElementResizeEvent.type, {bubbles: false, cancelable: false});
     overrideEvent(this, 'target', target);
   }
 
diff --git a/src/modules/esl-event-listener/core/targets/resize.target.ts b/src/modules/esl-event-listener/core/targets/resize.target.ts
index 9fb3e265a..bcefeb0b9 100644
--- a/src/modules/esl-event-listener/core/targets/resize.target.ts
+++ b/src/modules/esl-event-listener/core/targets/resize.target.ts
@@ -54,7 +54,7 @@ export class ESLResizeObserverTarget extends SyntheticEventTarget {
 
   /** Subscribes to the observed target {@link Element} changes */
   public override addEventListener(callback: EventListener): void;
-  public override addEventListener(event: 'resize', callback: EventListener): void;
+  public override addEventListener(event: typeof ESLElementResizeEvent.type, callback: EventListener): void;
   public override addEventListener(event: any, callback: EventListener = event): void {
     if (typeof event === 'string' && event !== 'resize') {
       console.warn(`[ESL]: ESLResizeObserverTarget does not support '${event}' type`);
@@ -68,7 +68,7 @@ export class ESLResizeObserverTarget extends SyntheticEventTarget {
 
   /** Unsubscribes from the observed target {@link Element} changes */
   public override removeEventListener(callback: EventListener): void;
-  public override removeEventListener(event: 'resize', callback: EventListener): void;
+  public override removeEventListener(event: typeof ESLElementResizeEvent.type, callback: EventListener): void;
   public override removeEventListener(event: any, callback: EventListener = event): void {
     if (typeof event === 'string' && event !== 'resize') return;
 
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 2df01e1d2..96b641fb7 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
@@ -5,11 +5,6 @@ import {overrideEvent} from '../../../esl-utils/dom/events/misc';
  */
 export type SwipeDirection = 'left' | 'right' | 'up' | 'down';
 
-/**
- * Event names that could be triggered by {@link ESLSwipeGestureTarget}
- */
-export type SwipeEventName = 'swipe';
-
 /**
  * Describes swipe information provided with {@link ESLSwipeGestureEvent}
  */
@@ -36,6 +31,8 @@ export interface ESLSwipeGestureEventInfo {
  * Swipe event dispatched by {@link ESLSwipeGestureTarget}
  */
 export class ESLSwipeGestureEvent extends UIEvent implements ESLSwipeGestureEventInfo {
+  public static readonly type = 'swipe';
+
   public override readonly target: Element;
 
   public readonly direction: SwipeDirection;
@@ -47,14 +44,14 @@ export class ESLSwipeGestureEvent extends UIEvent implements ESLSwipeGestureEven
   public readonly startEvent: PointerEvent;
   public readonly duration: number;
 
-  protected constructor(eventName: SwipeEventName, target: Element, swipeInfo: ESLSwipeGestureEventInfo) {
-    super(eventName, {bubbles: false, cancelable: true});
+  protected constructor(target: Element, swipeInfo: ESLSwipeGestureEventInfo) {
+    super(ESLSwipeGestureEvent.type, {bubbles: false, cancelable: true});
     overrideEvent(this, 'target', target);
     Object.assign(this, swipeInfo);
   }
 
   /** Creates {@link ESLSwipeGestureEvent} based on {@link ESLSwipeGestureEventInfo} */
-  public static fromConfig(eventName: SwipeEventName, target: Element, swipeInfo: ESLSwipeGestureEventInfo): ESLSwipeGestureEvent {
-    return new ESLSwipeGestureEvent(eventName, target, swipeInfo);
+  public static fromConfig(target: Element, swipeInfo: ESLSwipeGestureEventInfo): ESLSwipeGestureEvent {
+    return new ESLSwipeGestureEvent(target, 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 35efeef4b..f8396c3a6 100644
--- a/src/modules/esl-event-listener/core/targets/swipe.target.ts
+++ b/src/modules/esl-event-listener/core/targets/swipe.target.ts
@@ -8,7 +8,7 @@ import {ESLEventListener} from '../listener';
 import {ESLSwipeGestureEvent} from './swipe.target.event';
 
 import type {CSSSize} from '../../../esl-utils/dom/units';
-import type {SwipeDirection, SwipeEventName, ESLSwipeGestureEventInfo} from './swipe.target.event';
+import type {SwipeDirection, ESLSwipeGestureEventInfo} from './swipe.target.event';
 import type {ESLDomElementTarget} from '../../../esl-utils/abstract/dom-target';
 
 export {ESLSwipeGestureEvent};
@@ -120,7 +120,7 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
     // return if swipe took too long or distance is too short
     if (!this.isGestureAcceptable(eventDetails)) return;
 
-    const event = ESLSwipeGestureEvent.fromConfig('swipe', this.target, eventDetails);
+    const event = ESLSwipeGestureEvent.fromConfig(this.target, eventDetails);
     // fire `swipe` event on the element that started the swipe
     if (!this.dispatchEvent(event)) endEvent.preventDefault();
   }
@@ -138,7 +138,7 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
    * Subscribes to pointerup and pointerdown event
    */
   public override addEventListener(callback: EventListener): void;
-  public override addEventListener(event: SwipeEventName, callback: EventListener): void;
+  public override addEventListener(event: typeof ESLSwipeGestureEvent.type, callback: EventListener): void;
   public override addEventListener(event: any, callback: EventListener = event): void {
     super.addEventListener(event, callback);
 
@@ -151,7 +151,7 @@ export class ESLSwipeGestureTarget extends SyntheticEventTarget {
    * Unsubscribes from the observed target {@link Element} changes
    */
   public override removeEventListener(callback: EventListener): void;
-  public override removeEventListener(event: SwipeEventName, callback: EventListener): void;
+  public override removeEventListener(event: typeof ESLSwipeGestureEvent.type, callback: EventListener): void;
   public override removeEventListener(event: any, callback: EventListener = event): void {
     super.removeEventListener(event, callback);
     if (!this.hasEventListener(event)) ESLEventListener.unsubscribe(this);


Test patch:
diff --git a/src/modules/esl-event-listener/test/listener.delegate.test.ts b/src/modules/esl-event-listener/test/listener.delegate.test.ts
index bbc832af3..99a0289e1 100644
--- a/src/modules/esl-event-listener/test/listener.delegate.test.ts
+++ b/src/modules/esl-event-listener/test/listener.delegate.test.ts
@@ -78,4 +78,23 @@ describe('ESlEventListener subscription and delegation', () => {
     btnSpan.click();
     expect(handler).toBeCalledWith(expect.objectContaining({$delegate: btn}));
   });
+
+  describe('Delegation on non DOM target works correctly', () => {
+    const $btn = document.createElement('button');
+    beforeEach(() => document.body.appendChild($btn));
+
+    test('Delegation of the click event on the document works correct', () => {
+      const handler = jest.fn();
+      ESLEventUtils.subscribe(host, {event: 'click', target: document, selector: 'button'}, handler);
+      $btn.click();
+      expect(handler).toBeCalledWith(expect.objectContaining({$delegate: $btn}));
+    });
+
+    test('Delegation of the click event on the window works correct', () => {
+      const handler = jest.fn();
+      ESLEventUtils.subscribe(host, {event: 'click', target: window, selector: 'button'}, handler);
+      $btn.click();
+      expect(handler).toBeCalledWith(expect.objectContaining({$delegate: $btn}));
+    });
+  });
 });