Repository: exadel-inc/esl
Pull request #1693 (2023-05-31 11:40:50)

feat(esl-event-listener): add event delegated target
<!--- Provide a general summary of your changes in the Title above -->
<!--- Describe your changes in detail: purpose, special notes -->

Closes: #1675

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
<!---  or -->

<!--- Checklist -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

<!--- - My change requires a change to the documentation. -->
<!--- - I have updated the documentation accordingly. -->
<!--- - I have added tests to cover my changes. (Unit tests are strongly advisable for changes under `esl-utils` scope -->


<!--- If you like ESL, please support it with a star on GitHub. Thank you! -->


codeclimate[bot]: Code Climate has analyzed commit 50b6de27 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **47.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/1693).

codeclimate[bot]: Code Climate has analyzed commit ba4c9520 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **47.4%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/1693).

codeclimate[bot]: Code Climate has analyzed commit 1b771ce3 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **47.4%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/1693).

codeclimate[bot]: Code Climate has analyzed commit 5bd2a110 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **47.4%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/1693).

codeclimate[bot]: Code Climate has analyzed commit b1544a3b and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **47.4%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/1693).


Patch:
diff --git a/src/modules/esl-event-listener/README.md b/src/modules/esl-event-listener/README.md
index 978bb4d6d..8169475c7 100644
--- a/src/modules/esl-event-listener/README.md
+++ b/src/modules/esl-event-listener/README.md
@@ -111,6 +111,18 @@ Here is the list of supported keys of `ESLEventDesriptor`:
   <u>Default Value:</u> `null`  
   <u>Description:</u> the CSS selector to filter event targets for event delegation mechanism.
 
+  ⚠ If you want to get the currently delegated event target, you can access the `$delegate` key under the received event 
+  instance. In order to have access to `$delegate` strictly typed use the `DelegatedEvent<EventType>` type decorator.
+
+  E.g.:
+  ```typescript
+  @listen({ event: 'click', selector: 'button' })
+  onClick(e: DelegatedEvent<MouseEvent> /* instead of MouseEvent */) {
+    const delegate = e.$delegate; //instaead of e.target && e.target.closest('button');
+    ...
+  }
+  ```
+
   Supports `PropertyProvider` to declare the computed value as well.
 
 - #### `capture` key
diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts
index 229d18662..bd653e416 100644
--- a/src/modules/esl-event-listener/core/listener.ts
+++ b/src/modules/esl-event-listener/core/listener.ts
@@ -103,18 +103,21 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
       ? (e: Event): void => (handlerBound(e), this.unsubscribe())
       : handlerBound;
     return this.selector
-      ? (e: Event): void => this.isDelegatedTarget(e) && handlerFull(e)
+      ? (e: Event): void => this.handleDelegation(e, handlerFull)
       : handlerFull;
   }
 
-  /** Checks if the passed event can be handled by the current event listener */
-  protected isDelegatedTarget(e: Event): boolean {
+  /** Executes 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 delegate = this.delegate;
-    if (typeof delegate !== 'string') return true;
-    if (!delegate || !(target instanceof Element) || !(current instanceof Element)) return false;
-    return current.contains(target.closest(delegate));
+
+    if (typeof delegate !== 'string' || !delegate) return;
+    if (!(target instanceof Element) || !(current instanceof Element)) return;
+
+    const $delegate = target.closest(delegate);
+    if (current.contains($delegate)) handler(Object.assign(e, {$delegate}));
   }
 
   /**
diff --git a/src/modules/esl-event-listener/core/types.ts b/src/modules/esl-event-listener/core/types.ts
index 353d519d9..18c224dc5 100644
--- a/src/modules/esl-event-listener/core/types.ts
+++ b/src/modules/esl-event-listener/core/types.ts
@@ -8,6 +8,11 @@ declare global {
   }
 }
 
+/** Extended event with a delegated event target */
+export type DelegatedEvent<EventType extends Event> = EventType & {
+  $delegate: Element | null;
+};
+
 /** String CSS selector to find the target or {@link EventTarget} object or array of {@link EventTarget}s */
 export type ESLListenerTarget = EventTarget | EventTarget[] | string | null;
 


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 d63cb134c..bbc832af3 100644
--- a/src/modules/esl-event-listener/test/listener.delegate.test.ts
+++ b/src/modules/esl-event-listener/test/listener.delegate.test.ts
@@ -64,4 +64,18 @@ describe('ESlEventListener subscription and delegation', () => {
     el.click();
     expect(handler).toBeCalledTimes(1);
   });
+
+  test('Click on the target element leads to correct delegate information', () => {
+    const handler = jest.fn();
+    ESLEventUtils.subscribe(host, {event: 'click', selector: '.btn'}, handler);
+    btn.click();
+    expect(handler).toBeCalledWith(expect.objectContaining({$delegate: btn}));
+  });
+
+  test('Click inside the target element leads to correct delegate information', () => {
+    const handler = jest.fn();
+    ESLEventUtils.subscribe(host, {event: 'click', selector: '.btn'}, handler);
+    btnSpan.click();
+    expect(handler).toBeCalledWith(expect.objectContaining({$delegate: btn}));
+  });
 });
diff --git a/src/modules/esl-utils/decorators/test/listen.test.ts b/src/modules/esl-utils/decorators/test/listen.test.ts
index 155d21ab1..de2d9e5d1 100644
--- a/src/modules/esl-utils/decorators/test/listen.test.ts
+++ b/src/modules/esl-utils/decorators/test/listen.test.ts
@@ -4,6 +4,7 @@ import {listen} from '../listen';
 import {ESLEventUtils} from '../../dom/events';
 
 import type {ESLListenerDescriptorFn} from '../../dom/events';
+import type {DelegatedEvent} from '../../../esl-event-listener/core/types';
 
 describe('Decorator: @listen', () => {
   test('Decorator listen should accept one argument call with an event type', () => {
@@ -166,4 +167,26 @@ describe('Decorator: @listen', () => {
       expect(ESLEventUtils.getAutoDescriptors(test)).not.toContain(TestChild.prototype.onEventManual);
     });
   });
+
+  test('@listen has additional information about delegated event target', () => {
+    class Test extends HTMLElement {
+      connectedCallback() {
+        const button = document.createElement('button');
+        this.appendChild(button);
+        ESLEventUtils.subscribe(this);
+      }
+
+      @listen({event: 'click', selector: 'button'})
+      onSomeEvent(e: DelegatedEvent<MouseEvent>) {
+        expect(e.$delegate).toBeInstanceOf(Element);
+      }
+    }
+
+    customElements.define('test-listen-selected-target', Test);
+    const test = new Test();
+    document.body.appendChild(test);
+
+    const button = test.querySelector('button');
+    button?.click();
+  });
 });