Repository: exadel-inc/esl
Pull request #1537 (2023-03-10 00:54:55)

fix(esl-event-listener): fix `ESLEventTargetDecorator` dispatched event `target`


Patch:
diff --git a/src/modules/esl-event-listener/core/targets/decorated.target.ts b/src/modules/esl-event-listener/core/targets/decorated.target.ts
index 717743912..472e7ff75 100644
--- a/src/modules/esl-event-listener/core/targets/decorated.target.ts
+++ b/src/modules/esl-event-listener/core/targets/decorated.target.ts
@@ -53,7 +53,7 @@ export class ESLEventTargetDecorator<Args extends any[]> extends SyntheticEventT
 
     if (!this.hasEventListener(event, 1)) {
       const {target} = this;
-      ESLEventListener.subscribe(this, this.onEventDecorated, {event, target});
+      ESLEventListener.subscribe(this, this.createHandler(), {event, target});
     }
   }
 
@@ -71,7 +71,7 @@ export class ESLEventTargetDecorator<Args extends any[]> extends SyntheticEventT
   }
 
   /** @returns decorated handler */
-  protected get onEventDecorated(): EventListener {
+  protected createHandler(): EventListener {
     return this.decorator(this.dispatchEvent.bind(this), ...this.params);
   }
 }
diff --git a/src/modules/esl-event-listener/core/targets/resize.adapter.ts b/src/modules/esl-event-listener/core/targets/resize.adapter.ts
index f3ca4e94f..dcff5e788 100644
--- a/src/modules/esl-event-listener/core/targets/resize.adapter.ts
+++ b/src/modules/esl-event-listener/core/targets/resize.adapter.ts
@@ -1,4 +1,5 @@
 import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target';
+import {overrideEvent} from '../../../esl-utils/dom/events/misc';
 
 /** Adapter class for {@link ResizeObserver} that implements {@link EventTarget} */
 export class ESLResizeObserverTarget extends SyntheticEventTarget {
@@ -17,7 +18,8 @@ export class ESLResizeObserverTarget extends SyntheticEventTarget {
     const adapter = this.mapping.get(detail.target);
     if (!adapter) return;
     const event = new CustomEvent('resize', {detail});
-    adapter.dispatchEvent(event, adapter.target);
+    overrideEvent(event, 'target', adapter.target);
+    adapter.dispatchEvent(event);
   }
 
   /** Creates {@link ESLResizeObserverTarget} instance for the {@link Element} */
diff --git a/src/modules/esl-utils/dom/events/misc.ts b/src/modules/esl-utils/dom/events/misc.ts
index 84aa96d75..a08125c88 100644
--- a/src/modules/esl-utils/dom/events/misc.ts
+++ b/src/modules/esl-utils/dom/events/misc.ts
@@ -58,3 +58,24 @@ export const dispatchCustomEvent = (el: EventTarget, eventName: string, eventIni
   }, eventInit || {});
   return el.dispatchEvent(new CustomEvent(eventName, init));
 };
+
+/**
+ * Overrides {@link Event} `target` property
+ * @param event - {@link Event} to override
+ * @param key - {@link Event} property
+ * @param target - {@link EventTarget} to setup
+ * @returns original event
+ */
+export const overrideEvent = (
+  event: Event,
+  key: keyof Event,
+  target: null | EventTarget | (() => null | EventTarget)
+): Event => {
+  const provider = typeof target === 'function' ? target : ((): null | EventTarget => target);
+  Object.defineProperty(event, key, {
+    get: provider,
+    enumerable: true,
+    configurable: true
+  });
+  return event;
+};
diff --git a/src/modules/esl-utils/dom/events/target.ts b/src/modules/esl-utils/dom/events/target.ts
index e610d01e4..01d87a2c4 100644
--- a/src/modules/esl-utils/dom/events/target.ts
+++ b/src/modules/esl-utils/dom/events/target.ts
@@ -1,3 +1,5 @@
+import {overrideEvent} from './misc';
+
 /**
  * Synthetic implementation of EventTarget
  * Replicates behavior of native event
@@ -38,16 +40,14 @@ export class SyntheticEventTarget implements EventTarget {
     this._listeners[type] = this._listeners[type].filter((cb) => cb !== callback);
   }
 
-  public dispatchEvent(e: Event, target: EventTarget = this): boolean {
-    const targetDescriptor: PropertyDescriptor = {
-      get: () => target,
-      enumerable: true,
-      configurable: true
-    };
-    Object.defineProperty(e, 'target', targetDescriptor);
-    Object.defineProperty(e, 'currentTarget', targetDescriptor);
-    Object.defineProperty(e, 'srcElement', targetDescriptor);
-
+  public dispatchEvent(e: Event): boolean;
+  /** @deprecated use `overrideEvent` to declare `target` */
+  public dispatchEvent(e: Event, target?: EventTarget): boolean;
+  public dispatchEvent(e: Event, target?: EventTarget): boolean {
+    overrideEvent(e, 'currentTarget', this);
+    if (target) overrideEvent(e, 'target', target); // TODO: remove in 5.0.0
+    if (!e.target) overrideEvent(e, 'target', this);
+    if (!e.srcElement) overrideEvent(e, 'srcElement', e.target); // TODO: remove in 5.0.0
     this._listeners[e.type]?.forEach((listener) => {
       if (typeof listener === 'function') listener.call(this, e);
       else listener.handleEvent.call(listener, e);


Test patch:
diff --git a/src/modules/esl-event-listener/test/targets/decorator.test.ts b/src/modules/esl-event-listener/test/targets/decorator.test.ts
index 43bab1ba9..3a88776f5 100644
--- a/src/modules/esl-event-listener/test/targets/decorator.test.ts
+++ b/src/modules/esl-event-listener/test/targets/decorator.test.ts
@@ -1,3 +1,4 @@
+import {identity} from '../../../esl-utils/misc/functions';
 import {debounce} from '../../../esl-utils/async/debounce';
 import {throttle} from '../../../esl-utils/async/throttle';
 import {ESLEventUtils} from '../../core/api';
@@ -24,6 +25,24 @@ describe('ESLEventUtils.decorate proxy', () => {
     });
   });
 
+  describe('ESLEventUtils.decorate event target',  () => {
+    const et = document.createElement('div');
+    const dec = ESLEventUtils.decorate(et, identity);
+    const handler = jest.fn();
+
+    beforeEach(() => dec.addEventListener('click', handler));
+    afterEach(() => dec.removeEventListener('click', handler));
+
+    test('ESLEventUtils.decorate does not replace event.target', () => {
+      et.dispatchEvent(new Event('click'));
+      expect(handler).lastCalledWith(expect.objectContaining({target: et}));
+    });
+    test('ESLEventUtils.decorate uses ESLEventTargetDecorator instance as `event.currentTarget`', () => {
+      et.dispatchEvent(new Event('click'));
+      expect(handler).lastCalledWith(expect.objectContaining({currentTarget: dec}));
+    });
+  });
+
   describe('ESLEventUtils.decorate attribute processing', () => {
     const el = document.createElement('div');
     const handler = jest.fn();