Repository: exadel-inc/esl
Pull request #1511 (2023-02-28 17:57:58)

feat(esl-event-listener): extended event targets ( epic: event utils 3.0.0 )
This is epic #1199 final implementation for release 4.3.0.
It is beta functionality as of now and can still be changed in future minor releases

Documentation - #1519
Intersection Addaper - Rejected for MVP


Patch:
diff --git a/jest.config.js b/jest.config.js
index 8f08171a6..d0ccd8a35 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -11,7 +11,8 @@ module.exports = {
   coverageReporters: ['lcov', 'html'],
   setupFiles: [
     './src/modules/esl-utils/test/deviceDetector.mock.ts',
-    './src/modules/esl-utils/test/matchMedia.mock.ts'
+    './src/modules/esl-utils/test/matchMedia.mock.ts',
+    './src/modules/esl-utils/test/resizeObserver.mock.ts'
   ],
   collectCoverageFrom: [
     'src/modules/**/*.ts',
diff --git a/src/modules/esl-event-listener/README.md b/src/modules/esl-event-listener/README.md
index e565f09b2..5f018f158 100644
--- a/src/modules/esl-event-listener/README.md
+++ b/src/modules/esl-event-listener/README.md
@@ -1,8 +1,8 @@
 # ESL Event Listener module
 
-Version: *2.0.0*.
+Version: _2.1.0_.
 
-Authors: *Alexey Stsefanovich (ala'n)*.
+Authors: _Alexey Stsefanovich (ala'n)_.
 
 <a name="intro"></a>
 
@@ -12,7 +12,7 @@ Besides, the [`ESlBaseElement`](../esl-base-element/README.md) and the [`ESLMixi
 have even more pre-built syntax sugar to make the consumer's code briefer.
 
 One of the main advantages of ESL listeners is the extended control of subscriptions.
-All ESL listeners and their declarations are saved and associated with the host element. 
+All ESL listeners and their declarations are saved and associated with the host element.
 It means that ESL listeners can be subscribed or unsubscribed at any time in various ways.
 And most importantly, you do not need the original callback handler to do this.
 
@@ -41,32 +41,35 @@ The `handler` is a function that is used to process the subscription event.
 ESL declares a generic type to describe such functions - `ESLListenerHandler`;
 
 ```typescript
-export type ESLListenerHandler<EType extends Event = Event> = (event: EType) => void;
+export type ESLListenerHandler<EType extends Event = Event> = (
+  event: EType
+) => void;
 ```
 
-### <a name="the-esleventlistener-class-and-subscription">The `ESLEventListener` class and *subscription*</a>
+### <a name="the-esleventlistener-class-and-subscription">The `ESLEventListener` class and _subscription_</a>
 
 The subscriptions created by the ESL event listener module are instances of `ESLEventListener` class.
 All active subscriptions are stored in a hidden property of the `host` object.
 
 `ESLEventListener` has the following basic properties:
+
 - `event` - event type that the subscription is listening to;
 - `handler` - reference for the function to call to handle the event (see [The handler](#the-handler));
 - `host` - reference for the object that holds the subscription (see [The host](#the-host));
 - `target` - definition of `EventTarget` element (or string `TraversingQuery` to find it, see details in [`ESLEventDesriptor`](#descriptors-esleventdesriptor-esleventdesriptorfn));
-- `selector` - CSS selector to use built-in event delegation; 
+- `selector` - CSS selector to use built-in event delegation;
 - `capture` - marker to use the capture phase of the DOM event life-cycle;
 - `passive` - marker to use passive (non-blocking) subscription of the native event (if supported);
 - `once` - marker to destroy the subscription after the first event catch.
 
-All of the `ESLEventListener` instance fields are read-only; the subscription can't be changed once created. 
+All of the `ESLEventListener` instance fields are read-only; the subscription can't be changed once created.
 
 The `ESLEventListener`, as a class, describes the subscription behavior and
 contains static methods to create and manage subscriptions.
 
 ### <a name="descriptors-esleventdesriptor-esleventdesriptorfn">Descriptors (`ESLEventDesriptor`, `ESLEventDesriptorFn`)</a>
 
-The event listener *Descriptor* is an object to describe future subscriptions.
+The event listener _Descriptor_ is an object to describe future subscriptions.
 The ESL event listeners module has a few special details regarding such objects.
 
 A simple descriptor is an object that is passed to ESL event listener API to create a subscription.
@@ -78,47 +81,55 @@ In addition to that, ESL allows you to combine the `ESLEventDesriptor` data with
 Here is the list of supported keys of `ESLEventDesriptor`:
 
 - #### `event` key
+
   <u>Type:</u> `string | PropertyProvider<string>`  
-  <u>Description:</u> the event type for subscription. 
+  <u>Description:</u> the event type for subscription.
   Can be provided as a string or via provider function that will be called right before the subscription.
 
   The event string (as a literal, or returned by `PropertyProvider`) can declare multiple event types separated by space.
-  ESL will create a subscription (`ESLEventListener` object) for each event type in this case.  
+  ESL will create a subscription (`ESLEventListener` object) for each event type in this case.
 
 - #### `target` key
+
   <u>Type:</u> `string | EventTarget | EventTarget[] | PropertyProvider<string | EventTarget | EventTarget[]>`  
   <u>Default Value:</u> `host` object itself or `$host` key of the `host` object  
   <u>Description:</u> the key to declare exact EventTarget for the subscription.
-  In case the `target` key is a string it is considered as a [`TraversingQuery`](../esl-traversing-query/README.md). 
+  In case the `target` key is a string it is considered as a [`TraversingQuery`](../esl-traversing-query/README.md).
   The query finds a target relatively to `host | host.$host` object, or in bounds of the DOM tree if it is absolute.
-  The `target` key also supports an exact reference for `EventTarget`(s). 
+  The `target` key also supports an exact reference for `EventTarget`(s).
 
-  ⚠ Any `EventTarget` or even ESL `SynteticEventTarget` (including [`ESLMediaQuery`](../esl-media-query/README.md)) 
+  ⚠ Any `EventTarget` or even ESL `SynteticEventTarget` (including [`ESLMediaQuery`](../esl-media-query/README.md))
   can be a target for listener API.
-  
-  The `target` property can be declared via `PropertyProvider` as well.  
-  
+
+  ⚠ See [OOTB Extended Event Targets](#extended-event-targets) of ESL to know how to optimize handling of frequent events.
+
+  The `target` property can be declared via `PropertyProvider` as well.
+
 - #### `selector` key
+
   <u>Type:</u> `string | PropertyProvider<string>`  
   <u>Default Value:</u> `null`  
   <u>Description:</u> the CSS selector to filter event targets for event delegation mechanism.
-  
+
   Supports `PropertyProvider` to declare the computed value as well.
-  
+
 - #### `capture` key
+
   <u>Type:</u> `boolean`  
   <u>Default Value:</u> `false`  
   <u>Description:</u> marker to use capturing phase of the DOM event to handle.
- 
+
 - #### `passive` key
+
   <u>Type:</u> `boolean`  
   <u>Default Value:</u> `true` if the event type is `wheel`, `mousewheel`, `touchstart` or `touchmove`
   <u>Description:</u> marker to use passive subscription to the native event.
 
   ⚠ ESL uses passive subscription by default for `wheel`, `mousewheel`, `touchstart`, `touchmove` events.
   You need to declare `passive` key explicitly to override this behavior.
-  
+
 - #### `once` key
+
   <u>Type:</u> `boolean`  
   <u>Default Value:</u> `false`  
   <u>Description:</u> marker to unsubscribe the listener after the first successful handling of the event.
@@ -127,14 +138,11 @@ Here is the list of supported keys of `ESLEventDesriptor`:
   <u>Type:</u> `boolean`  
   <u>Default Value:</u> `false` for `ESLEventUtils.initDescriptor`, `true` for `@listen` decorator
   <u>Description:</u> marker to make an auto-subscribable descriptor. See [Automatic (collectable) descriptors](#automatic-collectable-descriptors).
-  
 - #### `inherit` key (for `ESLEventDesriptorExt` only)
   <u>Type:</u> `boolean`  
   <u>Description:</u> available in extended version of `ESLEventDesriptor` that is used in the descriptor declaration API.
-  Allows to inherit `ESLEventDesriptor` data from the `ESLEventDesriptorFn` from the prototype chain. 
+  Allows to inherit `ESLEventDesriptor` data from the `ESLEventDesriptorFn` from the prototype chain.
   See [`initDescriptor`](#-esleventutilsinitdescriptor) usages example.
-  
-
 
 ### <a name="automatic-collectable-descriptors">Automatic (collectable) descriptors</a>
 
@@ -148,7 +156,6 @@ To make an `ESLEventDesriptorFn` auto-collectable, the consumer should declare i
 The `ESLBaseElment` and the `ESLMixinElement` subscribes all auto-collectable descriptors in the `connectedCallback`.
 See the usage of [`ESLEventUtils.subscibe`](#-esleventutilssubscribe) for more details.
 
-
 ### `PropertyProvider` for `event`, `selector`, or `target`
 
 The descriptor declaration usually happens with the class declaration when the instance and its subscription
@@ -156,6 +163,7 @@ do not exist. We might have a problem if we want to pass subscription parameters
 
 To resolve such a case, the `event`, `selector`, and `target` keys of ESL event listener API support
 `PropertyProvider` mechanism:
+
 ```typescript
 type PropertyProvider<T> = (this: unknown, that: unknown) => T;
 ```
@@ -173,48 +181,52 @@ The units mentioned earlier are mostly implementation details of the module.
 Here is the module Public API:
 
 <a name="-esleventutilssubscribe"></a>
+
 ### ⚡ `ESLEventUtils.subscribe`
+
 Creates and subscribes an `ESLEventListener`.
 
 - Subscribes all auto-collectable (subscribable) descriptors of the `host` object:
-    ```typescript
-    ESLEventUtils.subscribe(host: object)
-    ```
+  ```typescript
+  ESLEventUtils.subscribe(host: object)
+  ```
 - Subscribes `handler` function to the DOM event declared by `eventType` string:
-    ```typescript
-    ESLEventUtils.subscribe(host: object, eventType: string, handler: ESLListenerHandler)
-    ```
+  ```typescript
+  ESLEventUtils.subscribe(host: object, eventType: string, handler: ESLListenerHandler)
+  ```
 - Subscribes `handler` instance of `ESLEventDescriptorFn` using embedded meta-information:
-    ```typescript
-    ESLEventUtils.subscribe(host: object, handler: ESLEventDescriptorFn)
-    ```
+  ```typescript
+  ESLEventUtils.subscribe(host: object, handler: ESLEventDescriptorFn)
+  ```
 - Subscribes `handler` function using `ESLEventDescriptor`:
-    ```typescript
-    ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLListenerHandler)
-    ```
+  ```typescript
+  ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLListenerHandler)
+  ```
 - Subscribes `handler` instance of `ESLEventDescriptorFn` with `ESLEventDescriptor` overriding meta-data:
-    ```typescript
-    ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLEventDescriptorFn)
-    ```
+  ```typescript
+  ESLEventUtils.subscribe(host: object, descriptor: ESLEventDescriptor, handler: ESLEventDescriptorFn)
+  ```
 
 **Parameters**:
+
 - `host` - host element to store subscription (event target by default);
 - `eventType` - string DOM event type;
 - `descriptor` - event description data (`ESLEventDescriptor`);
 - `handler` - function callback handler or instance of `ESLEventDescriptorFn`
 
 Examples:
-- `ESLEventUtils.subscribe(host);` -
-subscribes all auto-subscriptions of the `host`;
-- `ESLEventUtils.subscribe(host, handlerFn);` - 
-subscribes `handlerFn` method (decorated as an `ESLEventDescriptorFn`) to the `handlerFn.target`;
-- `ESLEventUtils.subscribe(host, 'click', handlerFn);` - 
-subscribes `handlerFn` function with the passed event type;
-- `ESLEventUtils.subscribe(host, {event: 'scroll', target: window}, handlerFn);` - 
-subscribes `handlerFn` function with the passed additional descriptor data.
 
+- `ESLEventUtils.subscribe(host);` -
+  subscribes all auto-subscriptions of the `host`;
+- `ESLEventUtils.subscribe(host, handlerFn);` -
+  subscribes `handlerFn` method (decorated as an `ESLEventDescriptorFn`) to the `handlerFn.target`;
+- `ESLEventUtils.subscribe(host, 'click', handlerFn);` -
+  subscribes `handlerFn` function with the passed event type;
+- `ESLEventUtils.subscribe(host, {event: 'scroll', target: window}, handlerFn);` -
+  subscribes `handlerFn` function with the passed additional descriptor data.
 
 <a name="-esleventutilsunsubscribe"></a>
+
 ### ⚡ `ESLEventUtils.unsubscribe`
 
 Allows unsubscribing existing subscriptions.
@@ -224,19 +236,22 @@ unsubscribe(host: HTMLElement, ...criteria: ESLListenerCriteria[]): ESLEventList
 ```
 
 **Parameters**:
+
 - `host` - host element to find subscriptions;
 - `criteria` - optional set of criteria to filter listeners to remove.
 
 Examples:
+
 - `ESLEventUtils.unsubscribe(host);` - unsubscribes everything bound to the `host`
 - `ESLEventUtils.unsubscribe(host, handlerFn);` - unsubscribes everything that is bound to the `host` and is handled by the `handlerFn`
 - `ESLEventUtils.unsubscribe(host, 'click');` - unsubscribes everything bound to the `host` and processing `click` event
 - `ESLEventUtils.unsubscribe(host, 'click', handlerFn);` - unsubscribes everything that is bound to the `host`, processing `click` event and is handled by the `handlerFn`
 - There can be any number of criteria.
 
-
 <a name="-esleventutilsiseventdescriptor"></a>
+
 ### ⚡ `ESLEventUtils.isEventDescriptor`
+
 Predicate to check if the passed argument is a type of `ESLListenerDescriptorFn = ESLEventHandler & Partial<ESLEventDescriptor>`.
 
 ```typescript
@@ -244,7 +259,9 @@ ESLEventUtils.isEventDescriptor(obj: any): obj is ESLListenerDescriptorFn;
 ```
 
 <a name="-esleventutilsgetautodescriptors"></a>
+
 ### ⚡ `ESLEventUtils.getAutoDescriptors`
+
 Gathers auto-subscribable (collectable) descriptors from the passed object.
 
 ```typescript
@@ -252,27 +269,31 @@ ESLEventUtils.descriptors(host?: any): ESLListenerDescriptorFn[]
 ```
 
 **Parameters**:
+
 - `host` - object to get auto-collectable descriptors from;
 
 <a name="-esleventutilsdescriptors"></a>
+
 ### ⚡ `ESLEventUtils.descriptors`
-Deprecated alias for `ESLEventUtils.getAutoDescriptors`
 
+Deprecated alias for `ESLEventUtils.getAutoDescriptors`
 
 <a name="-esleventutilsinitdescriptor"></a>
+
 ### ⚡ `ESLEventUtils.initDescriptor`
 
 Decorates the passed key of the host object as `ESLEventDescriptorFn`
 
 ```typescript
 ESLEventUtils.initDescriptor<T extends object>(
-  host: T, 
-  key: keyof T & string, 
+  host: T,
+  key: keyof T & string,
   desc: ESLEventDescriptorExt
 ): ESLEventDescriptorFn;
 ```
 
 **Parameters**:
+
 - `host` - host object holder of decorated function;
 - `key` - key of the `host` object that contains a function to decorate;
 - `desc` - `ESLEventDescriptor` (extended) meta information to describe future subscriptions.
@@ -298,8 +319,8 @@ It allows you to declare class methods as an `ESLEventDescriptorFn` using TS `ex
 Listeners described by `@listen` are auto-subscribable if they are not inherited and not declared as manual explicitly.
 In case of inheritance the `auto` marker will be inherited from the parent descriptor.
 
-
 Example:
+
 ```typescript
 class MyEl extends ESLBaseElement {
   private event: string;
@@ -315,7 +336,7 @@ class MyEl extends ESLBaseElement {
   onBodyClick(e) {}
   // Property Providers example
   @listen({
-    event: (that: MyEl) => that.event, 
+    event: (that: MyEl) => that.event,
     seletor: (that: MyEl) => that.selector
   })
   onEventProvidedExt(e) {}
@@ -323,10 +344,10 @@ class MyEl extends ESLBaseElement {
   @listen({event: 'click', auto: false})
   onClickManual(e) {}
 }
- ```
-
+```
 
 <a name="-esleventutilslisteners"></a>
+
 ### ⚡ `ESLEventUtils.listeners`
 
 Gathers listeners currently subscribed to the passed `host` object.
@@ -336,11 +357,12 @@ ESLEventUtils.listeners(host: object, ...criteria: ESLListenerCriteria[]): ESLEv
 ```
 
 **Parameters**:
+
 - `host` - object that stores and relates to the handlers;
 - `criteria` - optional set of criteria to filter the listeners list.
 
-
 <a name="-esleventutilsdispatch"></a>
+
 ### ⚡ `ESLEventUtils.dispatch`
 
 Dispatches custom DOM events.
@@ -355,17 +377,18 @@ ESLEventUtils.dispatch(
 ```
 
 **Parameters**:
+
 - `el` - `EventTarget` to dispatch event;
 - `eventName` - name of the event to dispatch;
 - `eventInit` - object that specifies characteristics of the event.
 
-
 ### Listeners Full Showcase Example
+
 ```typescript
 class TestCases {
   bind() {
     // Subcribes all auto descriptors (onEventAutoDescSugar and onEventAutoDesc)
-    ESLEventUtils.subscribe(this); 
+    ESLEventUtils.subscribe(this);
 
     // Subscribes onEventManualFn on click
     ESLEventUtils.subscribe(this, 'click', this.onEventManualFn);
@@ -377,7 +400,7 @@ class TestCases {
     ESLEventUtils.subscribe(this, this.onEventManualDesc);
 
     // Subscribes onEventManualDesc using merged embeded and passed information
-    ESLEventUtils.subscribe(this, {target: window}, this.onEventManualDesc); 
+    ESLEventUtils.subscribe(this, {target: window}, this.onEventManualDesc);
   }
 
   unbind() {
@@ -387,14 +410,14 @@ class TestCases {
     // Unsubcribes just onEventAutoDesc
     ESLEventUtils.unsubscribe(this, this.onEventAutoDesc);
   }
-  
+
   @listen('event')
   onEventAutoDescSugar() {}
-  
+
   onEventAutoDesc() {}
-  
+
   onEventManualFn() {}
-  
+
   onEventManualDesc() {}
 }
 
@@ -404,6 +427,130 @@ ESLEventUtils.initDescriptor(TestCases.prototype, 'onEventManualDesc', {event: '
 
 ---
 
+## <a name="extended-event-targets">Extended `EventTarget`s and standard optimizations</a> <i class="badge badge-sup badge-warning">beta</i>
+
+<a name="-esleventutilsdecorate"></a>
+
+### ⚡ `ESLEventUtils.decorate` and `ESLEventTargetDecorator`
+
+In cases where the original event of the target happens too frequently to be handled every time,
+it might be helpful to limit its processing. In purpose to do that ESL allows the creation of decorated `EventTargets`.
+The decorated target will process the original target events dispatching with the passed async call decoration function
+(such as debounce or throttle).
+
+The `ESLEventUtils.decorate` creates an instance of `ESLEventTargetDecorator` that decorates
+passed original `EventTarget` event emitting. The instances of `ESLEventTargetDecorator` are lazy
+and do not subscribe to the original event until they have their own subscriptions of the same event type.
+
+⚠ Note `ESLEventUtils.decorate` method is cached, so created instances will be reused if the inner cache does not
+refuse additional arguments of the decorator. The cache does not handle multiple and non-primitive arguments.
+
+```typescript
+ESLEventUtils.decorate(
+  target: EventTarget,
+  decorator: (fn: EventListener, ...args: any[]) => EventListener,
+  ...args: any[]
+): ESLEventTargetDecorator;
+```
+
+**Parameters**:
+
+- `target` - original `EventTarget` to consume events;
+- `decorator` - decoration function to decorate original target `EventListener`s;
+- `args` - optional arguments to pass to `decorator`.
+
+#### Sharing of the decorated targets
+
+As was mentioned above, the method `ESLEventUtils.decorate` (alias for `ESLEventTargetDecorator.cached`) works with
+a cache for simple cases. But in some cases, we might be interested in creating wrappers with a complex
+param, or we want to limit params usage across the project.
+
+It might sound obvious, but there are no restrictions on sharing exact instances instead of using the method cache.
+
+```typescript
+// shared-event-targets.ts
+export const DEBOUNCED_WINDOW = ESLEventUtils.decorate(window, debounce, 1000);
+```
+
+```typescript
+// module.ts
+class Component {
+  @listen({event: 'resize', target: DEBOUNCED_WINDOW})
+  onResize() {}
+}
+```
+
+#### Optimize `window.resize` handling with debouncing
+
+```typescript
+import {debounce} from '.../debounce';
+
+ESLEventUtils.subscribe(host, {
+  event: 'resize',
+  target: /* instead just window */ ESLEventUtils.decorate(window, debounce, 250)
+}, onResizeDebounced);
+```
+
+The sample above allows you to reuse debounced by 250 milliseconds version of the window,
+to receive fewer `resize` events
+(same as any other event types observed on debounced window version)
+
+#### Optimize `window.scroll` handling with throttling
+
+```typescript
+import {throttle} from '.../throttle';
+
+ESLEventUtils.subscribe(host, {
+  event: 'scroll',
+  target: /* instead just window */ ESLEventUtils.decorate(window, throttle, 250)
+}, onScrollThrottled);
+```
+
+The sample above allows you to reuse throttled by 250 milliseconds version of the window,
+to receive no more than one event per 250 milliseconds `scroll` events
+(same as any other event types observed on debounced window version)
+
+<a name="-esleventutilsresize"></a>
+
+### ⚡ `ESLEventUtils.resize` and `ESLResizeObserverTarget`
+
+When you deal with responsive interfaces, you might need to observe an element resizes instead of
+responding to the whole window change. There is a tool for this in the native DOM API - `ResizeObserver'.
+The only problem is that it does not use events, while in practice, we work with it in the same way.
+
+`ESLEventUtils.resize` creates cached `ResizeObserver` adaptation to `EventTarget` (`ESLResizeObserverTarget`)
+that allows you to get `resize` events when the observed element changes its size.
+
+```typescript
+ESLEventUtils.resize(el: Element): ESLResizeObserverTarget;
+```
+
+**Parameters**:
+
+- `el` - `Element` to observe size changes.
+
+`ESLResizeObserverTarget` creates itself once for an observed object with a weak reference-based cache.
+So any way of creating `ESLResizeObserverTarget` will always produce the same instance.
+
+`ESLEventUtils.resize(el) /**always*/ === ESLEventUtils.resize(el)`
+So there is no reason to cache it manually.
+
+Usage example:
+
+```typescript
+ESLEventUtils.subscribe(host, {
+  event: 'resize',
+  target: ESLEventUtils.resize(el)
+}, onResize);
+// or
+ESLEventUtils.subscribe(host, {
+  event: 'resize',
+  target: (host) => ESLEventUtils.resize(host.el)
+}, onResize);
+```
+
+---
+
 ## <a name="embedded-behavior-of-eslbaseelement-eslmixinelement">Embedded behavior of `ESLBaseElement` / `ESLMixinElement`</a>
 
 ### Shortcuts
@@ -416,6 +563,7 @@ The host parameter of the shortcut methods is always targeting the current eleme
 - `$$fire` ~ `ESLEventUtils.dispatch(this, ...)`
 
 Example:
+
 ```typescript
 this.$$on('click', this.onClick); // ESLEventUtils.subscribe(this, 'click', this.onClick)
 this.$$off('click'); // ESLEventUtils.unsubscribe(this, 'click')
@@ -423,14 +571,15 @@ this.$$off('click'); // ESLEventUtils.unsubscribe(this, 'click')
 
 ### Auto-subscription / Auto-unbinding
 
-All the inheritors of `ESLBaseElement` and `ESLMixinElement` automatically subscribe to all declared auto-subscribable descriptors 
+All the inheritors of `ESLBaseElement` and `ESLMixinElement` automatically subscribe to all declared auto-subscribable descriptors
 of their prototype chain.
 
 They also unsubscribe all own listeners attached via ESL automatically on `disconnectedCallback`.
 
 The following short snippet of code describes a listener that will automatically subscribe and unsubscribe
 on connected/disconnected callback inside `ESLBaseElement`:
- ```typescript
+
+```typescript
 class MyEl extends ESLBaseElement {
   // connectedCallback() {
   //   super.connectedCallback(); // - already contains ESLEventUtils.subscribe(this) call
@@ -438,17 +587,18 @@ class MyEl extends ESLBaseElement {
 
   // Will be subscribed automatically on connectedCallback and unsubscribed on disconnectedCallback
   @listen('click')
-  onClick(e) { 
-    //... 
+  onClick(e) {
+    //...
   }
 }
- ```
+```
 
 You can manage the subscription manually and link the whole meta information or part of it with the handler itself
- ```typescript
+
+```typescript
 class MyEl extends ESLBaseElement {
   @listen({event: 'click', auto: false}) // Won`t be subscribed automatically
-  onClick(e) { 
+  onClick(e) {
     // ...
   }
 
@@ -458,4 +608,4 @@ class MyEl extends ESLBaseElement {
     this.$$off(this.onClick); // Unsubscribes this.onClick method
   }
 }
- ```
+```
diff --git a/src/modules/esl-event-listener/core/api.ts b/src/modules/esl-event-listener/core/api.ts
index 6d1def4ad..7c6b814f6 100644
--- a/src/modules/esl-event-listener/core/api.ts
+++ b/src/modules/esl-event-listener/core/api.ts
@@ -3,6 +3,9 @@ import {dispatchCustomEvent} from '../../esl-utils/dom/events/misc';
 import {ESLEventListener} from './listener';
 import {getAutoDescriptors, isEventDescriptor, initDescriptor} from './descriptors';
 
+import {ESLEventTargetDecorator} from './targets/decorated.target';
+import {ESLResizeObserverTarget} from './targets/resize.adapter';
+
 import type {
   ESLListenerHandler,
   ESLListenerCriteria,
@@ -19,7 +22,6 @@ export class ESLEventUtils {
    */
   public static dispatch = dispatchCustomEvent;
 
-
   /** @deprecated alias for {@link getAutoDescriptors} */
   public static descriptors = getAutoDescriptors;
 
@@ -38,7 +40,6 @@ export class ESLEventUtils {
   /** Type guard to check if the passed function is typeof {@link ESLListenerDescriptorFn} */
   public static isEventDescriptor = isEventDescriptor;
 
-
   /**
    * Gets currently subscribed listeners of the host
    * @param host - host object (listeners context) to associate subscription
@@ -96,7 +97,10 @@ export class ESLEventUtils {
     }
     if (typeof eventDesc === 'string') eventDesc = {event: eventDesc};
     const listeners = ESLEventListener.subscribe(host, handler, eventDesc as ESLListenerDescriptor);
-    if (!listeners.length) console.warn('[ESL]: Empty subscription %o', Object.assign({}, eventDesc, {handler}));
+    if (!listeners.length) {
+      const mergedDesc = Object.assign({}, eventDesc, {handler});
+      console.warn('[ESL]: Empty subscription %o', mergedDesc);
+    }
     return listeners;
   }
 
@@ -110,6 +114,21 @@ export class ESLEventUtils {
     listeners.forEach((listener) => listener.unsubscribe());
     return listeners;
   }
+
+  // === EventTargets adapters ===
+  /**
+   * Creates an {@link EventTarget} adapter ({@link ESLResizeObserverTarget}) for {@link ResizeObserver}
+   * Note: the {@link ESLResizeObserverTarget} instances are unique for the related `targets`
+   * @deprecated it is an experimental functionality it can be changed in future releases
+   */
+  public static resize = ESLResizeObserverTarget.create;
+
+  /**
+   * Creates an {@link ESLEventTargetDecorator} decorator for any {@link EventTarget}
+   * Decorated {@link EventTarget} produces event according provided handler decoration
+   * @deprecated it is an experimental functionality it can be changed in future releases
+   */
+  public static decorate = ESLEventTargetDecorator.cached;
 }
 
 /** @deprecated alias for {@link ESLEventUtils} */
diff --git a/src/modules/esl-event-listener/core/descriptors.ts b/src/modules/esl-event-listener/core/descriptors.ts
index f1156f8e4..ce57bf475 100644
--- a/src/modules/esl-event-listener/core/descriptors.ts
+++ b/src/modules/esl-event-listener/core/descriptors.ts
@@ -12,7 +12,7 @@ const DESCRIPTORS = (window.Symbol || String)('__esl_descriptors');
  */
 function getOwnDescriptors(host: object, createIfNotExists = false): string[] {
   if (Object.hasOwnProperty.call(host, DESCRIPTORS)) return (host as any)[DESCRIPTORS];
-  const value: string[]  = [];
+  const value: string[] = [];
   if (createIfNotExists) Object.defineProperty(host, DESCRIPTORS, {value, configurable: true});
   return value;
 }
@@ -20,8 +20,8 @@ function getOwnDescriptors(host: object, createIfNotExists = false): string[] {
 /** Collects descriptors key from the whole prototype chain */
 function getDescriptorsKeysFor<T extends object>(host: T): (keyof T)[] {
   const store: Record<string, boolean> = {};
-  for (let proto = host; proto && proto !== Object.prototype ; proto = Object.getPrototypeOf(proto)) {
-    getOwnDescriptors(proto).forEach((key) => store[key] = true);
+  for (let proto = host; proto; proto = Object.getPrototypeOf(proto)) {
+    getOwnDescriptors(proto).forEach((key) => (store[key] = true));
   }
   return Object.keys(store) as (keyof T)[];
 }
diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts
index 826ea21c0..1f4e63b99 100644
--- a/src/modules/esl-event-listener/core/listener.ts
+++ b/src/modules/esl-event-listener/core/listener.ts
@@ -67,7 +67,7 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
     if (!isObject(this.host)) return [];
     const target = resolveProperty(this.target, this.host);
     if (isObject(target)) return wrap(target);
-    const $host  = '$host' in this.host ? this.host.$host : this.host;
+    const $host = '$host' in this.host ? this.host.$host : this.host;
     if (typeof target === 'string') return ESLTraversingQuery.all(target, $host);
     if (typeof target === 'undefined' && $host instanceof HTMLElement) return [$host];
     return [];
@@ -99,8 +99,12 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
   @memoize()
   public get handleEvent(): EventListener {
     const handlerBound = this.handler.bind(this.host);
-    const handlerFull = this.once ? ((e: Event): void => (handlerBound(e), this.unsubscribe())) : handlerBound;
-    return this.selector ? (e: Event): void => this.isDelegatedTarget(e) && handlerFull(e) : handlerFull;
+    const handlerFull = this.once
+      ? (e: Event): void => (handlerBound(e), this.unsubscribe())
+      : handlerBound;
+    return this.selector
+      ? (e: Event): void => this.isDelegatedTarget(e) && handlerFull(e)
+      : handlerFull;
   }
 
   /** Checks if the passed event can be handled by the current event listener */
@@ -169,7 +173,7 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
     host: object,
     handler: ESLListenerHandler | ESLListenerDescriptorFn,
     descriptor: ESLListenerDescriptor | ESLListenerDescriptorFn = handler as ESLListenerDescriptorFn
-  ): ESLEventListener[]  {
+  ): ESLEventListener[] {
     if (typeof handler !== 'function') return [];
     const eventDesc = handler !== descriptor ? Object.assign({}, handler, descriptor) : descriptor;
     const listeners = ESLEventListener.createOrResolve(host, handler, eventDesc);
@@ -177,7 +181,11 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
   }
 
   /** Creates or resolves existing event listeners by handler and descriptors */
-  public static createOrResolve(host: object, handler: ESLListenerHandler, desc: ESLListenerDescriptor): ESLEventListener[] {
+  public static createOrResolve(
+    host: object,
+    handler: ESLListenerHandler,
+    desc: ESLListenerDescriptor
+  ): ESLEventListener[] {
     if (!isObject(host)) return [];
     const eventString = resolveProperty(desc.event, host);
     const listeners: ESLEventListener[] = [];
diff --git a/src/modules/esl-event-listener/core/targets/decorated.target.ts b/src/modules/esl-event-listener/core/targets/decorated.target.ts
new file mode 100644
index 000000000..717743912
--- /dev/null
+++ b/src/modules/esl-event-listener/core/targets/decorated.target.ts
@@ -0,0 +1,91 @@
+import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target';
+
+import {ESLEventListener} from '../listener';
+import {isObject} from '../../../esl-utils/misc/object/types';
+
+type ESLListenerDecorator<Args extends any[]> = (target: EventListener, ...args: Args) => EventListener;
+
+const cache = memoizeOne((target: EventTarget) => {
+  return memoizeOne(<Args extends any[]>(decorator: ESLListenerDecorator<Args>) => {
+    return memoizeOne((...args: any[]) => {
+      return ESLEventTargetDecorator.create(target, decorator, ...args);
+    }, Map);
+  }, WeakMap);
+}, WeakMap);
+
+/**
+ * {@link EventTarget} proxy that decorates original target listening
+ */
+export class ESLEventTargetDecorator<Args extends any[]> extends SyntheticEventTarget {
+  public static cached<Args extends any[]>(
+    target: EventTarget,
+    decorator: ESLListenerDecorator<Args>,
+    ...args: Args
+  ): ESLEventTargetDecorator<Args> {
+    if (args.length > 1 || isObject(args[0])) {
+      console.debug('[ESL]: Can\'t cache multi-argument decoration or decoration with object param');
+      return this.create(target, decorator, ...args);
+    }
+    return cache(target)(decorator).call(null, ...args);
+  }
+
+  public static create<Args extends any[]>(
+    target: EventTarget,
+    decorator: ESLListenerDecorator<Args>,
+    ...args: Args
+  ): ESLEventTargetDecorator<Args> {
+    return new this(target, decorator, args);
+  }
+
+  protected constructor(
+    public readonly target: EventTarget,
+    public readonly decorator: (target: EventListener, ...args: Args) => EventListener,
+    public readonly params: Args
+  ) {
+    super();
+  }
+
+  /** Subscribes to the instance active rule change */
+  public override addEventListener(callback: EventListener): void;
+  public override addEventListener(event: string, callback: EventListener): void;
+  public override addEventListener(event: any, callback: EventListener = event): void {
+    super.addEventListener(event, callback);
+
+    if (!this.hasEventListener(event, 1)) {
+      const {target} = this;
+      ESLEventListener.subscribe(this, this.onEventDecorated, {event, target});
+    }
+  }
+
+  /** Unsubscribes from the instance active rule change */
+  public override removeEventListener(callback: EventListener): void;
+  public override removeEventListener(event: string, callback: EventListener): void;
+  public override removeEventListener(event: any, callback: EventListener = event): void {
+    super.removeEventListener(event, callback);
+
+    if (!this.hasEventListener(event)) {
+      ESLEventListener.get(this, event).forEach((listener: ESLEventListener) =>
+        listener.unsubscribe()
+      );
+    }
+  }
+
+  /** @returns decorated handler */
+  protected get onEventDecorated(): EventListener {
+    return this.decorator(this.dispatchEvent.bind(this), ...this.params);
+  }
+}
+
+// TODO: temporary before memoize update in bounds of 5th release
+export function memoizeOne<
+  C extends typeof Map | typeof WeakMap,
+  T extends typeof WeakMap extends C ? object : any,
+  R
+>(fn: (arg: T) => R, Cache: C): (arg: T) => R {
+  function memo(arg: T): any {
+    if (!memo.cache.has(arg)) memo.cache.set(arg, fn.call(this, arg));
+    return memo.cache.get(arg);
+  }
+  memo.cache = new (Cache as any)() as InstanceType<C>;
+  return memo;
+}
diff --git a/src/modules/esl-event-listener/core/targets/resize.adapter.ts b/src/modules/esl-event-listener/core/targets/resize.adapter.ts
new file mode 100644
index 000000000..f3ca4e94f
--- /dev/null
+++ b/src/modules/esl-event-listener/core/targets/resize.adapter.ts
@@ -0,0 +1,66 @@
+import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target';
+
+/** Adapter class for {@link ResizeObserver} that implements {@link EventTarget} */
+export class ESLResizeObserverTarget extends SyntheticEventTarget {
+  /** {@link ESLResizeObserverTarget} instances holder */
+  protected static readonly mapping = new WeakMap<Element, ESLResizeObserverTarget>();
+  /** {@link ResizeObserver} instance to observe DOM element related to {@link ESLResizeObserverTarget} */
+  protected static readonly observer$$ = new ResizeObserver((changes: ResizeObserverEntry[]) =>
+    changes.forEach(this.handleChange, this)
+  );
+
+  /** Internal method to handle {@link ResizeObserver} entry change */
+  protected static handleChange(
+    this: typeof ESLResizeObserverTarget,
+    detail: ResizeObserverEntry
+  ): void {
+    const adapter = this.mapping.get(detail.target);
+    if (!adapter) return;
+    const event = new CustomEvent('resize', {detail});
+    adapter.dispatchEvent(event, adapter.target);
+  }
+
+  /** Creates {@link ESLResizeObserverTarget} instance for the {@link Element} */
+  public static create(target: Element): ESLResizeObserverTarget {
+    return new ESLResizeObserverTarget(target);
+  }
+
+  /**
+   * Creates {@link ESLResizeObserverTarget} for the {@link Element}.
+   * Note the {@link ESLResizeObserverTarget} instances are singletons relatively to the {@link Element}
+   */
+  protected constructor(
+    /** Observed {@link Element} of the {@link ESLResizeObserverTarget} instance */
+    public readonly target: Element
+  ) {
+    const instance = ESLResizeObserverTarget.mapping.get(target);
+    if (instance) return instance;
+    super();
+    ESLResizeObserverTarget.mapping.set(target, this);
+  }
+
+  /** 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: any, callback: EventListener = event): void {
+    if (typeof event === 'string' && event !== 'resize') {
+      console.warn(`[ESL]: ESLResizeObserverTarget does not support '${event}' type`);
+      return;
+    }
+
+    super.addEventListener('resize', callback);
+    if (this.hasEventListener('resize', 1)) return;
+    ESLResizeObserverTarget.observer$$.observe(this.target);
+  }
+
+  /** 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: any, callback: EventListener = event): void {
+    if (typeof event === 'string' && event !== 'resize') return;
+
+    super.removeEventListener('resize', callback);
+    if (this.hasEventListener('resize')) return;
+    ESLResizeObserverTarget.observer$$.unobserve(this.target);
+  }
+}
diff --git a/src/modules/esl-event-listener/core/types.ts b/src/modules/esl-event-listener/core/types.ts
index 3d7b59844..677a876c6 100644
--- a/src/modules/esl-event-listener/core/types.ts
+++ b/src/modules/esl-event-listener/core/types.ts
@@ -49,7 +49,11 @@ export interface ESLListenerDefinition<EType extends keyof ESLListenerEventMap =
 export type ESLListenerHandler<EType extends Event = Event> = (event: EType) => void;
 
 /** Condition (criteria) to find {@link ESLListenerDescriptor} */
-export type ESLListenerCriteria = undefined | keyof ESLListenerEventMap | ESLListenerHandler | Partial<ESLListenerDefinition>;
+export type ESLListenerCriteria =
+  | undefined
+  | keyof ESLListenerEventMap
+  | ESLListenerHandler
+  | Partial<ESLListenerDefinition>;
 
 /** Function decorated as {@link ESLListenerDescriptor} */
 export type ESLListenerDescriptorFn<EType extends keyof ESLListenerEventMap = string> =
diff --git a/src/modules/esl-utils/dom/events/target.ts b/src/modules/esl-utils/dom/events/target.ts
index 3d6ad9851..e610d01e4 100644
--- a/src/modules/esl-utils/dom/events/target.ts
+++ b/src/modules/esl-utils/dom/events/target.ts
@@ -39,7 +39,11 @@ export class SyntheticEventTarget implements EventTarget {
   }
 
   public dispatchEvent(e: Event, target: EventTarget = this): boolean {
-    const targetDescriptor: PropertyDescriptor = {get: () => target, enumerable: true};
+    const targetDescriptor: PropertyDescriptor = {
+      get: () => target,
+      enumerable: true,
+      configurable: true
+    };
     Object.defineProperty(e, 'target', targetDescriptor);
     Object.defineProperty(e, 'currentTarget', targetDescriptor);
     Object.defineProperty(e, 'srcElement', targetDescriptor);


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
new file mode 100644
index 000000000..43bab1ba9
--- /dev/null
+++ b/src/modules/esl-event-listener/test/targets/decorator.test.ts
@@ -0,0 +1,137 @@
+import {debounce} from '../../../esl-utils/async/debounce';
+import {throttle} from '../../../esl-utils/async/throttle';
+import {ESLEventUtils} from '../../core/api';
+
+describe('ESLEventUtils.decorate proxy', () => {
+  const DEFAULT_TIMEOUT = 250;
+
+  describe('ESLEventUtils.decorate caching',  () => {
+    test('ESLEventUtils.decorate cached for target and fake fn', () => {
+      const fn = (arg: any) => arg;
+      expect(ESLEventUtils.decorate(window, fn) === ESLEventUtils.decorate(window, fn));
+      expect(ESLEventUtils.decorate(document, fn) !== ESLEventUtils.decorate(window, fn));
+    });
+
+    test('ESLEventUtils.decorate cached for target and debounce fn', () => {
+      expect(ESLEventUtils.decorate(window, debounce) === ESLEventUtils.decorate(window, debounce));
+      expect(ESLEventUtils.decorate(document, debounce) !== ESLEventUtils.decorate(window, debounce));
+    });
+
+    test('ESLEventUtils.decorate cached for target, fake fn and timeout', () => {
+      const fn = (arg: any, num: number) => arg;
+      expect(ESLEventUtils.decorate(window, fn, 100) === ESLEventUtils.decorate(window, fn, 100));
+      expect(ESLEventUtils.decorate(window, fn, 100) !== ESLEventUtils.decorate(window, fn, 150));
+    });
+  });
+
+  describe('ESLEventUtils.decorate attribute processing', () => {
+    const el = document.createElement('div');
+    const handler = jest.fn();
+    const dec = jest.fn(() => handler);
+    const decorated = ESLEventUtils.decorate(el, dec);
+
+    test('Creation does not cause execution', () => expect(dec).not.toBeCalled());
+
+    test('Creation happens once on subscription', () => {
+      const fn = jest.fn();
+      decorated.addEventListener(fn);
+      expect(dec).toBeCalled();
+      expect(handler).not.toBeCalled();
+      decorated.removeEventListener(fn);
+    });
+
+    test('Event is passed to the handler', () => {
+      const fn = jest.fn();
+      const event = new Event('event');
+      decorated.addEventListener('event', fn);
+
+      el.dispatchEvent(event);
+      expect(handler).lastCalledWith(event);
+
+      decorated.removeEventListener(fn);
+    });
+  });
+
+  describe('window.resize debouncing case', () => {
+    const host = {};
+    const fn = jest.fn();
+
+    beforeAll(() => {
+      ESLEventUtils.subscribe(host, {
+        event: 'resize',
+        target: ESLEventUtils.decorate(window, debounce, DEFAULT_TIMEOUT)
+      }, fn);
+      jest.useFakeTimers();
+    });
+    afterAll(() => ESLEventUtils.unsubscribe(host));
+
+    beforeEach(() => fn.mockReset());
+    afterEach(() => jest.runAllTimers());
+
+    test('Proxy resize receives the resize event',  ()=> {
+      const event = new Event('resize');
+      window.dispatchEvent(event);
+      jest.advanceTimersByTime(DEFAULT_TIMEOUT);
+      expect(fn).lastCalledWith(event);
+    });
+
+    test('Proxy resize happens debounced (multiple events in bounds of debounce range)',  ()=> {
+      window.dispatchEvent(new Event('resize'));
+      window.dispatchEvent(new Event('resize'));
+      jest.advanceTimersByTime(10);
+
+      window.dispatchEvent(new Event('resize'));
+      window.dispatchEvent(new Event('resize'));
+      expect(fn).toBeCalledTimes(0);
+
+      jest.advanceTimersByTime(DEFAULT_TIMEOUT);
+      expect(fn).toBeCalledTimes(1);
+    });
+
+    test('Proxy resize happens debounced (multiple events in bounds of two debounce range)',  ()=> {
+      window.dispatchEvent(new Event('resize'));
+      window.dispatchEvent(new Event('resize'));
+      jest.advanceTimersByTime(DEFAULT_TIMEOUT);
+
+      window.dispatchEvent(new Event('resize'));
+      window.dispatchEvent(new Event('resize'));
+      jest.advanceTimersByTime(DEFAULT_TIMEOUT);
+
+      expect(fn).toBeCalledTimes(2);
+    });
+  });
+
+  describe('window.scroll throttling case', () => {
+    const host = {};
+    const fn = jest.fn();
+
+    beforeAll(() => {
+      ESLEventUtils.subscribe(host, {
+        event: 'scroll',
+        target: ESLEventUtils.decorate(window, throttle, DEFAULT_TIMEOUT)
+      }, fn);
+      jest.useFakeTimers();
+    });
+    afterAll(() => ESLEventUtils.unsubscribe(host));
+
+    beforeEach(() => fn.mockReset());
+    afterEach(() => jest.runAllTimers());
+
+    test('Proxy scroll receives the scroll event',  ()=> {
+      const event = new Event('scroll');
+      window.dispatchEvent(event);
+      jest.advanceTimersByTime(10);
+      expect(fn).lastCalledWith(event);
+    });
+
+    test('Proxy scroll happens throttled (multiple events received once in bounds of threshold)',  ()=> {
+      expect(fn).toBeCalledTimes(0);
+      window.dispatchEvent(new Event('scroll'));
+      window.dispatchEvent(new Event('scroll'));
+      window.dispatchEvent(new Event('scroll'));
+      expect(fn).toBeCalledTimes(1);
+      jest.advanceTimersByTime(DEFAULT_TIMEOUT + 100);
+      expect(fn).toBeCalledTimes(2);
+    });
+  });
+});
diff --git a/src/modules/esl-event-listener/test/targets/resize.test.ts b/src/modules/esl-event-listener/test/targets/resize.test.ts
new file mode 100644
index 000000000..45734fbdf
--- /dev/null
+++ b/src/modules/esl-event-listener/test/targets/resize.test.ts
@@ -0,0 +1,126 @@
+import {ESLEventUtils} from '../../core/api';
+import {getLastResizeObserverMock} from '../../../esl-utils/test/resizeObserver.mock';
+
+describe('ESLEventUtils: ResizeObserver EventTarget adapter', () => {
+  describe('ESLResizeObserverTarget creation relatively to the targets', () => {
+    const el1 = document.createElement('div');
+    const el2 = document.createElement('div');
+
+    test(
+      'The same element produces the same event target',
+      () => expect(ESLEventUtils.resize(el1) === ESLEventUtils.resize(el1)).toBe(true)
+    );
+    test(
+      'Different elements produces different event targets',
+      () => expect(ESLEventUtils.resize(el1) === ESLEventUtils.resize(el2)).toBe(false)
+    );
+  });
+
+  describe('ESLResizeObserverTarget livecycle', () => {
+    const mock = getLastResizeObserverMock();
+    const el = document.createElement('div');
+    const target = ESLEventUtils.resize(el);
+
+    const cb1 = jest.fn();
+    const cb2 = jest.fn();
+
+    beforeEach(() => {
+      mock.observe.mockReset();
+      mock.unobserve.mockReset();
+    });
+
+    test('Subscription produces ResizeObserver interaction', () => {
+      target.addEventListener(cb1);
+      expect(mock.observe).lastCalledWith(el);
+      expect(mock.unobserve).not.toBeCalled();
+    });
+    test('Second subscription does not produces ResizeObserver interaction', () => {
+      target.addEventListener(cb2);
+      expect(mock.observe).not.toBeCalled();
+      expect(mock.unobserve).not.toBeCalled();
+    });
+    test('Single unsubscription does not produces internal unsubscription', () => {
+      target.removeEventListener(cb2);
+      expect(mock.observe).not.toBeCalled();
+      expect(mock.unobserve).not.toBeCalled();
+    });
+    test('Remove of all of subscriptions produce internal unsubscription', () => {
+      target.removeEventListener(cb1);
+      expect(mock.observe).not.toBeCalled();
+      expect(mock.unobserve).toBeCalled();
+    });
+  });
+
+  describe('ESLResizeObserverTarget dispatching of changes', () => {
+    const mock = getLastResizeObserverMock();
+
+    const el1 = document.createElement('div');
+    const el2 = document.createElement('div');
+
+    const cb1_1 = jest.fn();
+    ESLEventUtils.resize(el1).addEventListener(cb1_1);
+    expect(cb1_1).not.toBeCalled();
+
+    const cb1_2 = jest.fn();
+    ESLEventUtils.resize(el1).addEventListener(cb1_2);
+    expect(cb1_2).not.toBeCalled();
+
+    const cb2_1 = jest.fn();
+    ESLEventUtils.resize(el2).addEventListener(cb2_1);
+    expect(cb2_1).not.toBeCalled();
+
+    const fakeEntry = (el: Element) => ({
+      target: el,
+      contentRect: el.getBoundingClientRect(),
+      borderBoxSize: [{blockSize: 100, inlineSize: 100}],
+      contentBoxSize: [{blockSize: 100, inlineSize: 100}],
+      devicePixelContentBoxSize: [{blockSize: 100, inlineSize: 100}]
+    });
+
+    beforeEach(() => {
+      cb1_1.mockReset();
+      cb1_2.mockReset();
+      cb2_1.mockReset();
+    });
+
+    test('Dispatched change received by all subscribers', () => {
+      const entry: ResizeObserverEntry = fakeEntry(el1);
+
+      mock.callback.call(mock, [entry]);
+      expect(cb1_1).toBeCalled();
+      expect(cb1_2).toBeCalled();
+      expect(cb2_1).not.toBeCalled();
+    });
+
+    test('Dispatched change received by correct target', () => {
+      const entry: ResizeObserverEntry = fakeEntry(el2);
+
+      mock.callback.call(mock, [entry]);
+      expect(cb2_1).toBeCalled();
+      expect(cb1_1).not.toBeCalled();
+      expect(cb1_2).not.toBeCalled();
+    });
+
+    test('Dispatched change produces an Event instance', () => {
+      const entry: ResizeObserverEntry = fakeEntry(el1);
+
+      mock.callback.call(mock, [entry]);
+      expect(cb1_1).lastCalledWith(expect.any(Event));
+      expect(cb1_1).lastCalledWith(expect.objectContaining({type: 'resize'}));
+    });
+
+    test('Dispatched change passed as a detail of event', () => {
+      const entry: ResizeObserverEntry = fakeEntry(el1);
+
+      mock.callback.call(mock, [entry]);
+      expect(cb1_1).lastCalledWith(expect.objectContaining({detail: entry}));
+    });
+
+    test('Dispatched change produces correct event target', () => {
+      const entry: ResizeObserverEntry = fakeEntry(el2);
+
+      mock.callback.call(mock, [entry]);
+      expect(cb2_1).lastCalledWith(expect.objectContaining({target: el2}));
+    });
+  });
+});
diff --git a/src/modules/esl-utils/test/resizeObserver.mock.ts b/src/modules/esl-utils/test/resizeObserver.mock.ts
new file mode 100644
index 000000000..d684aa3ee
--- /dev/null
+++ b/src/modules/esl-utils/test/resizeObserver.mock.ts
@@ -0,0 +1,17 @@
+export interface ResizeObserverMock {
+  callback: ResizeObserverCallback;
+  disconnect: jest.MockedFn<any>;
+  observe: jest.MockedFn<any>;
+  unobserve: jest.MockedFn<any>;
+}
+
+export const ResizeObserverMockImpl = jest.fn().mockImplementation((callback: ResizeObserverCallback) => ({
+  callback,
+  disconnect: jest.fn(),
+  observe: jest.fn(),
+  unobserve: jest.fn()
+}));
+
+export const getLastResizeObserverMock = (): ResizeObserverMock => ResizeObserverMockImpl.mock.results.at(-1)?.value;
+
+window.ResizeObserver = window.ResizeObserver || ResizeObserverMockImpl;