Repository: exadel-inc/esl
Pull request #1592 (2023-04-04 16:38:04)

feat(esl-event-listener): improved `ESLResizeObserverTarget` API with a standardized `ESLElementResizeEvent`
`ESLListenerEventMap` moved to the global namespace.

Closes: #1559


Patch:
diff --git a/src/modules/esl-base-element/core/esl-base-element.ts b/src/modules/esl-base-element/core/esl-base-element.ts
index a22ad8c23..60f62d6d8 100644
--- a/src/modules/esl-base-element/core/esl-base-element.ts
+++ b/src/modules/esl-base-element/core/esl-base-element.ts
@@ -7,7 +7,6 @@ import type {
   ESLEventListener,
   ESLListenerHandler,
   ESLListenerCriteria,
-  ESLListenerEventMap,
   ESLListenerDescriptor
 } from '../../esl-utils/dom/events';
 
diff --git a/src/modules/esl-event-listener/core.ts b/src/modules/esl-event-listener/core.ts
index b7cdceff6..0f92ad9b9 100644
--- a/src/modules/esl-event-listener/core.ts
+++ b/src/modules/esl-event-listener/core.ts
@@ -2,7 +2,6 @@ export * from './core/api';
 export type {
   ESLListenerHandler,
   ESLListenerCriteria,
-  ESLListenerEventMap,
   ESLListenerDescriptor,
   ESLListenerDescriptorFn,
   ESLListenerDescriptorExt,
diff --git a/src/modules/esl-event-listener/core/api.ts b/src/modules/esl-event-listener/core/api.ts
index 7c6b814f6..83b213168 100644
--- a/src/modules/esl-event-listener/core/api.ts
+++ b/src/modules/esl-event-listener/core/api.ts
@@ -10,7 +10,6 @@ import type {
   ESLListenerHandler,
   ESLListenerCriteria,
   ESLListenerDescriptor,
-  ESLListenerEventMap,
   ESLListenerDescriptorFn
 } from './types';
 
diff --git a/src/modules/esl-event-listener/core/targets/resize.adapter.event.ts b/src/modules/esl-event-listener/core/targets/resize.adapter.event.ts
new file mode 100644
index 000000000..3e80cdbe0
--- /dev/null
+++ b/src/modules/esl-event-listener/core/targets/resize.adapter.event.ts
@@ -0,0 +1,67 @@
+import {overrideEvent} from '../../../esl-utils/dom/events/misc';
+
+import type {ESLResizeObserverTarget} from './resize.adapter';
+
+/** Custom event that {@link ESLResizeObserverTarget} produces */
+export class ESLElementResizeEvent extends UIEvent implements ResizeObserverEntry {
+  /** A reference to the {@link Element} or SVGElement being observed */
+  public override readonly target: Element;
+  /** A reference to the {@link ESLResizeObserverTarget} */
+  public override readonly currentTarget: ESLResizeObserverTarget;
+
+  /**
+   * A DOMRectReadOnly object containing the new size of the observed element.
+   * Note that this is better supported than `borderBoxSize` or `contentBoxSize`,
+   * but it is left over from an earlier implementation of the Resize Observer API,
+   * is still included in the spec for web compat reasons, and may be deprecated in future versions.
+   * @see ResizeObserverEntry.prototype.contentRect
+   */
+  public readonly contentRect: DOMRectReadOnly;
+  /**
+   * An object containing the new border box size of the observed element
+   * @see ResizeObserverEntry.prototype.borderBoxSize
+   */
+  public readonly borderBoxSize: readonly ResizeObserverSize[];
+  /**
+   * An object containing the new content box size of the observed element
+   * @see ResizeObserverEntry.prototype.contentBoxSize
+   */
+  public readonly contentBoxSize: readonly ResizeObserverSize[];
+  /**
+   * An object containing the new content box size in device pixels of the observed element
+   * @see ResizeObserverEntry.prototype.devicePixelContentBoxSize
+   */
+  public readonly devicePixelContentBoxSize: readonly ResizeObserverSize[];
+
+  protected constructor(target: Element) {
+    super('resize', {bubbles: false, cancelable: false});
+    overrideEvent(this, 'target', target);
+  }
+
+  /** Creates {@link ESLElementResizeEvent} from {@link ResizeObserverEntry} */
+  public static fromEntry(entry: ResizeObserverEntry): ESLElementResizeEvent {
+    const event = new ESLElementResizeEvent(entry.target);
+    const {
+      contentRect, borderBoxSize,
+      contentBoxSize, devicePixelContentBoxSize
+    } = entry;
+    Object.assign(event, {
+      contentRect, borderBoxSize,
+      contentBoxSize, devicePixelContentBoxSize
+    });
+    return event;
+  }
+
+  // /** Creates {@link ESLElementResizeEvent} from resize {@link Event} */
+  // public static fromEvent(event: UIEvent): ESLElementResizeEvent {
+  //   // TODO: converter
+  // }
+}
+
+declare global {
+  /** Extended event map with the custom event definition */
+  export interface ESLListenerEventMap {
+    /** Native resize event or {@link ESLElementResizeEvent} in case {@link ESLResizeObserverTarget} */
+    'resize': UIEvent | ESLElementResizeEvent;
+  }
+}
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 9532b8ea2..f1a593495 100644
--- a/src/modules/esl-event-listener/core/targets/resize.adapter.ts
+++ b/src/modules/esl-event-listener/core/targets/resize.adapter.ts
@@ -1,5 +1,5 @@
 import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target';
-import {overrideEvent} from '../../../esl-utils/dom/events/misc';
+import {ESLElementResizeEvent} from './resize.adapter.event';
 
 /** Adapter class for {@link ResizeObserver} that implements {@link EventTarget} */
 export class ESLResizeObserverTarget extends SyntheticEventTarget {
@@ -13,13 +13,11 @@ export class ESLResizeObserverTarget extends SyntheticEventTarget {
   /** Internal method to handle {@link ResizeObserver} entry change */
   protected static handleChange(
     this: typeof ESLResizeObserverTarget,
-    detail: ResizeObserverEntry
+    entry: ResizeObserverEntry
   ): void {
-    const adapter = this.mapping.get(detail.target);
+    const adapter = this.mapping.get(entry.target);
     if (!adapter) return;
-    const event = new CustomEvent('resize', {detail});
-    overrideEvent(event, 'target', adapter.target);
-    adapter.dispatchEvent(event);
+    adapter.dispatchEvent(ESLElementResizeEvent.fromEntry(entry));
   }
 
   /** Creates {@link ESLResizeObserverTarget} instance for the {@link Element} */
diff --git a/src/modules/esl-event-listener/core/types.ts b/src/modules/esl-event-listener/core/types.ts
index 677a876c6..353d519d9 100644
--- a/src/modules/esl-event-listener/core/types.ts
+++ b/src/modules/esl-event-listener/core/types.ts
@@ -1,14 +1,16 @@
 import type {PropertyProvider} from '../../esl-utils/misc/functions';
 
+declare global {
+  /** Extended event map with the custom event definition */
+  export interface ESLListenerEventMap extends HTMLElementEventMap {
+    /** User custom event or group of events */
+    [e: string]: Event;
+  }
+}
+
 /** String CSS selector to find the target or {@link EventTarget} object or array of {@link EventTarget}s */
 export type ESLListenerTarget = EventTarget | EventTarget[] | string | null;
 
-/** Extended event map with the custom event definition */
-export interface ESLListenerEventMap extends HTMLElementEventMap {
-  /** User custom event or group of events */
-  [e: string]: Event;
-}
-
 /** Descriptor to create {@link ESLEventListener} */
 export type ESLListenerDescriptor<EType extends keyof ESLListenerEventMap = string> = {
   /** A case-sensitive string (or provider function) representing the event type to listen for */
diff --git a/src/modules/esl-mixin-element/ui/esl-mixin-element.ts b/src/modules/esl-mixin-element/ui/esl-mixin-element.ts
index 95efa7c5b..2952152a5 100644
--- a/src/modules/esl-mixin-element/ui/esl-mixin-element.ts
+++ b/src/modules/esl-mixin-element/ui/esl-mixin-element.ts
@@ -8,7 +8,6 @@ import type {
   ESLEventListener,
   ESLListenerCriteria,
   ESLListenerDescriptor,
-  ESLListenerEventMap,
   ESLListenerHandler
 } from '../../esl-utils/dom/events';
 import type {ESLDomElementRelated} from '../../esl-utils/abstract/dom-target';
diff --git a/src/modules/esl-utils/decorators/listen.ts b/src/modules/esl-utils/decorators/listen.ts
index 1532c5b6c..ec498e371 100644
--- a/src/modules/esl-utils/decorators/listen.ts
+++ b/src/modules/esl-utils/decorators/listen.ts
@@ -1,7 +1,7 @@
 import {ESLEventUtils} from '../../esl-event-listener/core';
 
 import type {PropertyProvider} from '../misc/functions';
-import type {ESLListenerHandler, ESLListenerEventMap, ESLListenerDescriptorExt} from '../dom/events';
+import type {ESLListenerHandler, ESLListenerDescriptorExt} from '../dom/events';
 
 type ListenDecorator<EType extends Event> =
   (target: any, property: string, descriptor: TypedPropertyDescriptor<ESLListenerHandler<EType>>) => void;


Test patch:
diff --git a/src/modules/esl-event-listener/test/targets/resize.test.ts b/src/modules/esl-event-listener/test/targets/resize.test.ts
index 45734fbdf..ba49059bc 100644
--- a/src/modules/esl-event-listener/test/targets/resize.test.ts
+++ b/src/modules/esl-event-listener/test/targets/resize.test.ts
@@ -109,11 +109,15 @@ describe('ESLEventUtils: ResizeObserver EventTarget adapter', () => {
       expect(cb1_1).lastCalledWith(expect.objectContaining({type: 'resize'}));
     });
 
-    test('Dispatched change passed as a detail of event', () => {
+    test.each([
+      'contentRect',
+      'borderBoxSize',
+      'contentBoxSize',
+      'devicePixelContentBoxSize'
+    ])('Dispatched change contains correct %s', (name: keyof ResizeObserverEntry) => {
       const entry: ResizeObserverEntry = fakeEntry(el1);
-
       mock.callback.call(mock, [entry]);
-      expect(cb1_1).lastCalledWith(expect.objectContaining({detail: entry}));
+      expect(cb1_1).lastCalledWith(expect.objectContaining({[name]: entry[name]}));
     });
 
     test('Dispatched change produces correct event target', () => {