Repository: exadel-inc/esl
Pull request #1225 (2022-09-17 21:34:10)

feat: `esl-toggleable`, `esl-panel`, `esl-alert` migration to `esl-event-listener`


Patch:
diff --git a/pages/src/navigation/header/header-search.ts b/pages/src/navigation/header/header-search.ts
index 8ddd600fd..5f3c4d31f 100644
--- a/pages/src/navigation/header/header-search.ts
+++ b/pages/src/navigation/header/header-search.ts
@@ -6,7 +6,7 @@ import {TraversingQuery} from '../../../../src/modules/esl-traversing-query/core
 import {ESLToggleable} from '../../../../src/modules/esl-toggleable/core';
 import {requestGss} from '../../search/search-script';
 
-import type {ToggleableActionParams} from '../../../../src/modules/esl-toggleable/core';
+import type {ESLToggleableActionParams} from '../../../../src/modules/esl-toggleable/core';
 
 export class ESLDemoSearchBox extends ESLToggleable {
   static is = 'esl-d-search-box';
@@ -19,12 +19,12 @@ export class ESLDemoSearchBox extends ESLToggleable {
 
   @prop() public closeOnOutsideAction = true;
 
-  public onShow(params: ToggleableActionParams): void {
+  public onShow(params: ESLToggleableActionParams): void {
     CSSClassUtils.add(this, this.postCls);
     requestGss().then(() => this.showSearchElements(params));
   }
 
-  private showSearchElements(params: ToggleableActionParams): void {
+  private showSearchElements(params: ESLToggleableActionParams): void {
     afterNextRender(() => super.onShow(params));
     if (this.autofocus) {
       const $focusEl = TraversingQuery.first(this.firstFocusable, this) as HTMLElement;
@@ -37,7 +37,7 @@ export class ESLDemoSearchBox extends ESLToggleable {
     }
   }
 
-  public onHide(params: ToggleableActionParams): void {
+  public onHide(params: ESLToggleableActionParams): void {
     super.onHide(params);
     window.setTimeout(() => {
       CSSClassUtils.remove(this, this.postCls);
diff --git a/pages/src/navigation/sidebar/sidebar.ts b/pages/src/navigation/sidebar/sidebar.ts
index e24b81f12..088932a18 100644
--- a/pages/src/navigation/sidebar/sidebar.ts
+++ b/pages/src/navigation/sidebar/sidebar.ts
@@ -1,10 +1,10 @@
-import {boolAttr, listen, prop, bind, ready} from '../../../../src/modules/esl-utils/decorators';
+import {boolAttr, listen, prop, ready} from '../../../../src/modules/esl-utils/decorators';
 import {ESLToggleable} from '../../../../src/modules/esl-toggleable/core/esl-toggleable';
 import {ESLMediaQuery} from '../../../../src/modules/esl-media-query/core/esl-media-query';
 
-import type {ToggleableActionParams} from '../../../../src/modules/esl-toggleable/core/esl-toggleable';
+import type {ESLToggleableActionParams} from '../../../../src/modules/esl-toggleable/core/esl-toggleable';
 
-interface SidebarActionParams extends ToggleableActionParams {
+interface SidebarActionParams extends ESLToggleableActionParams {
   /** Change state without animation */
   immediate: boolean;
 }
@@ -81,7 +81,7 @@ export class ESLDemoSidebar extends ESLToggleable {
     this.toggle(isDesktop && isStoredOpen, {force: true, initiator: 'bpchange', immediate: !isDesktop});
   }
 
-  @bind
+  @listen({inherit: true})
   protected _onOutsideAction(e: Event): void {
     if (ESLMediaQuery.for('@+MD').matches) return;
     super._onOutsideAction(e);
diff --git a/src/modules/esl-alert/core/esl-alert.ts b/src/modules/esl-alert/core/esl-alert.ts
index 85adbe256..4778a989d 100644
--- a/src/modules/esl-alert/core/esl-alert.ts
+++ b/src/modules/esl-alert/core/esl-alert.ts
@@ -1,14 +1,15 @@
 import {ExportNs} from '../../esl-utils/environment/export-ns';
-import {bind, attr, jsonAttr} from '../../esl-utils/decorators';
+import {attr, jsonAttr, prop, listen} from '../../esl-utils/decorators';
+import {isMatches} from '../../esl-utils/dom/traversing';
 import {ESLToggleable} from '../../esl-toggleable/core';
 import {DeviceDetector} from '../../esl-utils/environment/device-detector';
 import {CSSClassUtils} from '../../esl-utils/dom/class';
 import {createZIndexIframe} from '../../esl-utils/fixes/ie-fixes';
 import {TraversingQuery} from '../../esl-traversing-query/core';
 
-import type {ToggleableActionParams} from '../../esl-toggleable/core';
+import type {ESLToggleableActionParams, ESLToggleableRequestDetails} from '../../esl-toggleable/core';
 
-export interface AlertActionParams extends ToggleableActionParams {
+export interface AlertActionParams extends ESLToggleableRequestDetails {
   /** text to be shown; passes empty string or null to hide */
   text?: string;
   /** html content */
@@ -29,7 +30,6 @@ export interface AlertActionParams extends ToggleableActionParams {
 @ExportNs('Alert')
 export class ESLAlert extends ESLToggleable {
   public static is = 'esl-alert';
-  public static eventNs = 'esl:alert';
   public static observedAttributes = ['target'];
 
   /** Default show/hide params for all ESLAlert instances */
@@ -38,6 +38,11 @@ export class ESLAlert extends ESLToggleable {
     hideDelay: 2500
   };
 
+  /** Event to show alert component */
+  @prop('esl:alert:show') public override SHOW_REQUEST_EVENT: string;
+  /** Event to hide alert component */
+  @prop('esl:alert:hide') public override HIDE_REQUEST_EVENT: string;
+
   /**
    * Defines the scope (using {@link TraversingQuery} syntax) element to listen for an activation event.
    * Parent element by default
@@ -46,7 +51,7 @@ export class ESLAlert extends ESLToggleable {
 
   /** Default show/hide params for current ESLAlert instance */
   @jsonAttr<AlertActionParams>()
-  public defaultParams: AlertActionParams;
+  public override defaultParams: AlertActionParams;
 
   protected $content: HTMLElement;
   protected activeCls?: string;
@@ -65,19 +70,19 @@ export class ESLAlert extends ESLToggleable {
     return alert;
   }
 
-  protected mergeDefaultParams(params?: ToggleableActionParams): ToggleableActionParams {
+  protected override mergeDefaultParams(params?: ESLToggleableActionParams): ESLToggleableActionParams {
     const type = this.constructor as typeof ESLAlert;
     return Object.assign({}, type.defaultConfig, this.defaultParams || {}, params || {});
   }
 
-  protected attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void {
+  protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void {
     if (!this.connected) return;
     if (attrName === 'target') {
       this.$target = TraversingQuery.first(this.target) as EventTarget;
     }
   }
 
-  protected connectedCallback(): void {
+  protected override connectedCallback(): void {
     super.connectedCallback();
     this.setAttribute('role', this.getAttribute('role') || 'alert');
     this.$content = document.createElement('div');
@@ -90,33 +95,17 @@ export class ESLAlert extends ESLToggleable {
     }
   }
 
-  protected unbindEvents(): void {
-    super.unbindEvents();
-    this.unbindTargetEvents();
-  }
-
   /** Target element to listen to activation events */
   public get $target(): EventTarget {
     return this._$target;
   }
   public set $target($el: EventTarget) {
-    this.unbindTargetEvents();
     this._$target = $el;
-    this.bindTargetEvents();
+    this.$$on(this._onShowRequest);
+    this.$$on(this._onHideRequest);
   }
 
-  protected bindTargetEvents(): void {
-    if (!this.$target || !this.connected) return;
-    this.$target.addEventListener(`${ESLAlert.eventNs}:show`, this._onTargetEvent);
-    this.$target.addEventListener(`${ESLAlert.eventNs}:hide`, this._onTargetEvent);
-  }
-  protected unbindTargetEvents(): void {
-    if (!this.$target) return;
-    this.$target.removeEventListener(`${ESLAlert.eventNs}:show`, this._onTargetEvent);
-    this.$target.removeEventListener(`${ESLAlert.eventNs}:hide`, this._onTargetEvent);
-  }
-
-  protected onShow(params: AlertActionParams): void {
+  protected override onShow(params: AlertActionParams): void {
     if (this._clearTimeout) window.clearTimeout(this._clearTimeout);
     if (params.html || params.text) {
       this.render(params);
@@ -124,7 +113,7 @@ export class ESLAlert extends ESLToggleable {
     }
     this.hide(params);
   }
-  protected onHide(params: AlertActionParams): void {
+  protected override onHide(params: AlertActionParams): void {
     super.onHide(params);
     this._clearTimeout = window.setTimeout(() => this.clear(), params.hideTime);
   }
@@ -140,16 +129,23 @@ export class ESLAlert extends ESLToggleable {
     CSSClassUtils.remove(this, this.activeCls);
   }
 
-  @bind
-  protected _onTargetEvent(e: CustomEvent): void {
-    if (e.type === `${ESLAlert.eventNs}:show`) {
-      const params = Object.assign({}, e.detail, {force: true});
-      this.show(params);
-    }
-    if (e.type === `${ESLAlert.eventNs}:hide`) {
-      const params = Object.assign({}, {hideDelay: 0}, e.detail, {force: true});
-      this.hide(params);
-    }
+  protected override buildRequestParams(e: CustomEvent<ESLToggleableRequestDetails>): AlertActionParams | null {
+    const detail = e.detail || {};
+    if (!isMatches(this, detail.match)) return null;
+    if (e.type === this.SHOW_REQUEST_EVENT) return Object.assign({}, detail, {force: true});
+    if (e.type === this.HIDE_REQUEST_EVENT) return Object.assign({hideDelay: 0}, detail, {force: true});
+    return null;
+  }
+
+  @listen({inherit: true, target: (el: ESLAlert) => el.$target})
+  protected override _onHideRequest(e: CustomEvent<ESLToggleableRequestDetails>): void {
+    super._onHideRequest(e);
+    e.stopPropagation();
+  }
+
+  @listen({inherit: true, target: (el: ESLAlert) => el.$target})
+  protected override _onShowRequest(e: CustomEvent<ESLToggleableRequestDetails>): void {
+    super._onShowRequest(e);
     e.stopPropagation();
   }
 }
diff --git a/src/modules/esl-animate/README.md b/src/modules/esl-animate/README.md
index 2b5b40947..210237bbf 100644
--- a/src/modules/esl-animate/README.md
+++ b/src/modules/esl-animate/README.md
@@ -1,6 +1,6 @@
 # [ESL](https://exadel-inc.github.io/esl/) Animate
 
-Version: _1.0.0-beta_.
+Version: *1.0.0*.
 
 Authors: *Anna-Mariia Petryk*, *Alexey Stsefanovich (ala'n)*, *Julia Murashko*.
 
diff --git a/src/modules/esl-event-listener/core/listener.ts b/src/modules/esl-event-listener/core/listener.ts
index 8d619f996..12989db8b 100644
--- a/src/modules/esl-event-listener/core/listener.ts
+++ b/src/modules/esl-event-listener/core/listener.ts
@@ -15,7 +15,7 @@ import type {
 } from './descriptor';
 
 /** Key to store listeners on the host */
-const STORE = '__listeners';
+const STORE = '__esl_listeners';
 
 /**
  * Splits and deduplicates event string
@@ -102,7 +102,7 @@ export class ESLEventListener implements ESLListenerDefinition, EventListenerObj
     const current = e.currentTarget;
     const delegate = this.delegate;
     if (typeof delegate !== 'string') return true;
-    if (!(target instanceof HTMLElement) || !(current instanceof HTMLElement)) return false;
+    if (!delegate || !(target instanceof HTMLElement) || !(current instanceof HTMLElement)) return false;
     return current.contains(target.closest(delegate));
   }
 
diff --git a/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts b/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
index bf8626623..9da157436 100644
--- a/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
+++ b/src/modules/esl-forms/esl-select/core/esl-select-dropdown.ts
@@ -1,11 +1,11 @@
 import {ESLToggleable} from '../../../esl-toggleable/core/esl-toggleable';
-import {bind, prop} from '../../../esl-utils/decorators';
+import {bind, prop, listen} from '../../../esl-utils/decorators';
 import {TAB} from '../../../esl-utils/dom/keys';
 import {rafDecorator} from '../../../esl-utils/async/raf';
 import {ESLSelectList} from '../../esl-select-list/core';
 
 import type {ESLSelect} from './esl-select';
-import type {ToggleableActionParams} from '../../../esl-toggleable/core/esl-toggleable';
+import type {ESLToggleableActionParams} from '../../../esl-toggleable/core/esl-toggleable';
 
 /**
  * ESLSelectDropdown component
@@ -48,16 +48,7 @@ export class ESLSelectDropdown extends ESLToggleable {
     this.removeChild(this.$list);
   }
 
-  protected bindEvents(): void {
-    super.bindEvents();
-    window.addEventListener('resize', this._deferredUpdatePosition);
-  }
-  protected unbindEvents(): void {
-    super.unbindEvents();
-    window.removeEventListener('resize', this._deferredUpdatePosition);
-  }
-
-  protected onShow(params: ToggleableActionParams): void {
+  protected onShow(params: ESLToggleableActionParams): void {
     document.body.appendChild(this);
     this._disposeTimeout && window.clearTimeout(this._disposeTimeout);
 
@@ -71,7 +62,7 @@ export class ESLSelectDropdown extends ESLToggleable {
     focusable?.focus({preventScroll: true});
     this.updatePosition();
   }
-  protected onHide(params: ToggleableActionParams): void {
+  protected onHide(params: ESLToggleableActionParams): void {
     const select = this.activator;
     super.onHide(params);
     this._disposeTimeout = window.setTimeout(() => {
@@ -81,7 +72,7 @@ export class ESLSelectDropdown extends ESLToggleable {
     select && setTimeout(() => select.focus({preventScroll: true}), 0);
   }
 
-  @bind
+  @listen('keydown')
   protected _onKeyboardEvent(e: KeyboardEvent): void {
     super._onKeyboardEvent(e);
     if (e.key === TAB) this._onTabKey(e);
@@ -95,6 +86,12 @@ export class ESLSelectDropdown extends ESLToggleable {
     if (last && e.target === first && e.shiftKey) last.focus();
   }
 
+  @listen({event: 'resize', target: window})
+  protected _onResize(): void {
+    if (!this.activator) return;
+    this._deferredUpdatePosition();
+  }
+
   @bind
   public updatePosition(): void {
     if (!this.activator) return;
diff --git a/src/modules/esl-panel/core/esl-panel.ts b/src/modules/esl-panel/core/esl-panel.ts
index d5d1b7be4..f46b8bdad 100644
--- a/src/modules/esl-panel/core/esl-panel.ts
+++ b/src/modules/esl-panel/core/esl-panel.ts
@@ -1,14 +1,14 @@
 import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {CSSClassUtils} from '../../esl-utils/dom/class';
-import {bind, attr, boolAttr, jsonAttr} from '../../esl-utils/decorators';
+import {attr, boolAttr, jsonAttr, listen} from '../../esl-utils/decorators';
 import {afterNextRender, skipOneRender} from '../../esl-utils/async/raf';
 import {ESLToggleable} from '../../esl-toggleable/core';
 
 import type {ESLPanelGroup} from '../../esl-panel-group/core';
-import type {ToggleableActionParams} from '../../esl-toggleable/core';
+import type {ESLToggleableActionParams} from '../../esl-toggleable/core';
 
 /** {@link ESLPanel} action params interface */
-export interface PanelActionParams extends ToggleableActionParams {
+export interface PanelActionParams extends ESLToggleableActionParams {
   /** Panel group */
   capturedBy?: ESLPanelGroup;
   /** Prevents collapsing/expanding animation */
@@ -58,16 +58,6 @@ export class ESLPanel extends ESLToggleable {
     return this.closest(this.panelGroupSel);
   }
 
-  protected bindEvents(): void {
-    super.bindEvents();
-    this.addEventListener('transitionend', this._onTransitionEnd);
-  }
-
-  protected unbindEvents(): void {
-    super.unbindEvents();
-    this.removeEventListener('transitionend', this._onTransitionEnd);
-  }
-
   /** Process show action */
   protected onShow(params: PanelActionParams): void {
     this._initialHeight = this.scrollHeight;
@@ -132,7 +122,7 @@ export class ESLPanel extends ESLToggleable {
     this.clearAnimation();
     // Prevent fallback calls from being tracked
     if (!animating) return;
-    this.$$fire(this.open ? 'esl:after:show' : 'esl:after:hide');
+    this.$$fire(this.open ? this.AFTER_SHOW_EVENT : this.AFTER_HIDE_EVENT);
   }
 
   /** Clear animation properties */
@@ -144,7 +134,7 @@ export class ESLPanel extends ESLToggleable {
   }
 
   /** Catching CSS transition end event to start post-animate processing */
-  @bind
+  @listen('transitionend')
   protected _onTransitionEnd(e?: TransitionEvent): void {
     if (!e || (e.propertyName === 'max-height' && e.target === this)) {
       this.afterAnimate();
@@ -152,7 +142,7 @@ export class ESLPanel extends ESLToggleable {
   }
 
   /** Merge params that are used by panel group for actions */
-  protected mergeDefaultParams(params?: ToggleableActionParams): ToggleableActionParams {
+  protected mergeDefaultParams(params?: ESLToggleableActionParams): ESLToggleableActionParams {
     const stackConfig = this.$group?.panelConfig || {};
     return Object.assign({}, stackConfig, this.defaultParams, params || {});
   }
diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts
index 851f94aa9..756c52585 100644
--- a/src/modules/esl-popup/core/esl-popup.ts
+++ b/src/modules/esl-popup/core/esl-popup.ts
@@ -12,7 +12,7 @@ import {parseNumber} from '../../esl-utils/misc/format';
 import {calcPopupPosition, isMajorAxisHorizontal} from './esl-popup-position';
 import {ESLPopupPlaceholder} from './esl-popup-placeholder';
 
-import type {ToggleableActionParams} from '../../esl-toggleable/core';
+import type {ESLToggleableActionParams} from '../../esl-toggleable/core';
 import type {PositionType, IntersectionRatioRect} from './esl-popup-position';
 
 const INTERSECTION_LIMIT_FOR_ADJACENT_AXIS = 0.7;
@@ -24,7 +24,7 @@ const parsePercent = (value: string | number, nanValue: number = 0): number => {
   return Math.max(0, Math.min(rawValue !== undefined ? rawValue : nanValue, 100));
 };
 
-export interface PopupActionParams extends ToggleableActionParams {
+export interface PopupActionParams extends ESLToggleableActionParams {
   /** popup position relative to trigger */
   position?: PositionType;
   /** popup behavior if it does not fit in the window */
diff --git a/src/modules/esl-toggleable/README.md b/src/modules/esl-toggleable/README.md
index 18e5f3017..3898eac3c 100644
--- a/src/modules/esl-toggleable/README.md
+++ b/src/modules/esl-toggleable/README.md
@@ -1,6 +1,6 @@
 # [ESL](https://esl-ui.com/) Toggleable
 
-Version: *2.2.1*
+Version: *3.0.0*
 
 Authors: *Julia Murashko*, *Alexey Stsefanovich (ala'n)*
 
@@ -9,7 +9,7 @@ Authors: *Julia Murashko*, *Alexey Stsefanovich (ala'n)*
 ESLToggleable - a custom element, that is used as a base for "Popup-like" components creation.
 
 ESLToggleableDispatcher - plugin component, that prevents activation of multiple ESLToggleable instances in bounds of managed container.
-Usually (and by default) binds to document.body. Use native DOM events to manage controlled instances state.
+Usually (and by default) binds to `document.body`. Uses native DOM events to manage controlled instances state.
 
 Use `ESLToggleableDispatcher.init()` to initialize (and bind) ESLToggleableDispatcher.
 
@@ -39,6 +39,7 @@ Use `ESLToggleableDispatcher.init()` to initialize (and bind) ESLToggleableDispa
  - `toggle` - toggle element state
 
 ### Events
-
+- `esl:before:show` - thrown when toggleable change is going to be activated(open)
+- `esl:before:hide` - thrown when toggleable change is going to be deactivated
 - `esl:show` - thrown when toggleable change its state to active(open)
 - `esl:hide` - thrown when toggleable change its state to inactive
diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts
index 6f1510cc5..8ce3c55b6 100644
--- a/src/modules/esl-toggleable/core/esl-toggleable.ts
+++ b/src/modules/esl-toggleable/core/esl-toggleable.ts
@@ -1,7 +1,7 @@
 import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {ESC, SYSTEM_KEYS} from '../../esl-utils/dom/keys';
 import {CSSClassUtils} from '../../esl-utils/dom/class';
-import {bind, attr, jsonAttr, boolAttr} from '../../esl-utils/decorators';
+import {prop, attr, jsonAttr, boolAttr, listen} from '../../esl-utils/decorators';
 import {defined, copyDefinedKeys} from '../../esl-utils/misc/object';
 import {sequentialUID} from '../../esl-utils/misc/uid';
 import {DeviceDetector} from '../../esl-utils/environment/device-detector';
@@ -9,17 +9,8 @@ import {DelayedTask} from '../../esl-utils/async/delayed-task';
 import {ESLBaseElement} from '../../esl-base-element/core';
 import {isMatches} from '../../esl-utils/dom/traversing';
 
-export interface ESLShowRequestDetails {
-  // Selector to ignore or exact predicate to check if the target should process request
-  ignore?: string | ((target: Element) => boolean);
-  // Delay to show targets
-  delay?: number;
-  // Custom params to pass
-  params?: Record<string, ToggleableActionParams>;
-}
-
 /** Default Toggleable action params type definition */
-export interface ToggleableActionParams {
+export interface ESLToggleableActionParams {
   /** Initiator string identifier */
   initiator?: string;
   /** Delay timeout for both show and hide actions */
@@ -43,6 +34,14 @@ export interface ToggleableActionParams {
   [key: string]: any;
 }
 
+export interface ESLToggleableRequestDetails extends ESLToggleableActionParams {
+  // Selector to match or exact predicate to check if the target should process request
+  match?: string | ((target: Element) => boolean);
+}
+
+/** @deprecated alias for ESLToggleableActionParams */
+export type ToggleableActionParams = ESLToggleableActionParams;
+
 const activators: WeakMap<ESLToggleable, HTMLElement | undefined> = new WeakMap();
 
 /**
@@ -56,6 +55,29 @@ export class ESLToggleable extends ESLBaseElement {
   public static is = 'esl-toggleable';
   public static observedAttributes = ['open', 'group'];
 
+  /** Event to dispatch when toggleable is going to be activated */
+  @prop('esl:before:show') public BEFORE_SHOW_EVENT: string;
+  /** Event to dispatch when toggleable is going to be deactivated */
+  @prop('esl:before:hide') public BEFORE_HIDE_EVENT: string;
+
+  /** Event to dispatch when toggleable is activated */
+  @prop('esl:show') public SHOW_EVENT: string;
+  /** Event to dispatch when toggleable is deactivated */
+  @prop('esl:hide') public HIDE_EVENT: string;
+
+  /** Event to dispatch when toggleable has end activation process */
+  @prop('esl:after:show') public AFTER_SHOW_EVENT: string;
+  /** Event to dispatch when toggleable has end deactivation process */
+  @prop('esl:after:hide') public AFTER_HIDE_EVENT: string;
+
+  /** Event to activate toggleables on event way */
+  @prop('esl:show:request') public SHOW_REQUEST_EVENT: string;
+  /** Event to deactivate toggleables on event way */
+  @prop('esl:hide:request') public HIDE_REQUEST_EVENT: string;
+
+  /** Event to dispatch when toggleable group has changed */
+  @prop('esl:change:group') public GROUP_CHANGED_EVENT: string;
+
   /** CSS class to add on the body element */
   @attr() public bodyClass: string;
   /** CSS class to add when the Toggleable is active */
@@ -74,14 +96,14 @@ export class ESLToggleable extends ESLBaseElement {
   @boolAttr() public closeOnOutsideAction: boolean;
 
   /** Initial params to pass to show/hide action on the start */
-  @jsonAttr<ToggleableActionParams>({defaultValue: {force: true, initiator: 'init'}})
-  public initialParams: ToggleableActionParams;
+  @jsonAttr<ESLToggleableActionParams>({defaultValue: {force: true, initiator: 'init'}})
+  public initialParams: ESLToggleableActionParams;
   /** Default params to merge into passed action params */
-  @jsonAttr<ToggleableActionParams>({defaultValue: {}})
-  public defaultParams: ToggleableActionParams;
+  @jsonAttr<ESLToggleableActionParams>({defaultValue: {}})
+  public defaultParams: ESLToggleableActionParams;
   /** Hover params to pass from track hover listener */
-  @jsonAttr<ToggleableActionParams>({defaultValue: {}})
-  public trackHoverParams: ToggleableActionParams;
+  @jsonAttr<ESLToggleableActionParams>({defaultValue: {}})
+  public trackHoverParams: ESLToggleableActionParams;
 
   /** Marker of initially opened toggleable instance */
   public initiallyOpened: boolean;
@@ -103,13 +125,11 @@ export class ESLToggleable extends ESLBaseElement {
       this.id = sequentialUID(tag, tag + '-');
     }
     this.initiallyOpened = this.hasAttribute('open');
-    this.bindEvents();
     this.setInitialState();
   }
 
   protected disconnectedCallback(): void {
     super.disconnectedCallback();
-    this.unbindEvents();
     activators.delete(this);
   }
 
@@ -121,7 +141,7 @@ export class ESLToggleable extends ESLBaseElement {
         this.toggle(this.open, {initiator: 'attribute', showDelay: 0, hideDelay: 0});
         break;
       case 'group':
-        this.$$fire('esl:change:group',  {
+        this.$$fire(this.GROUP_CHANGED_EVENT,  {
           detail: {oldGroupName: oldVal, newGroupName: newVal}
         });
         break;
@@ -135,30 +155,9 @@ export class ESLToggleable extends ESLBaseElement {
     }
   }
 
-  protected bindEvents(): void {
-    this.addEventListener('click', this._onClick);
-    this.addEventListener('keydown', this._onKeyboardEvent);
-    this.addEventListener('esl:show:request', this._onShowRequest);
-  }
-
-  protected unbindEvents(): void {
-    this.removeEventListener('click', this._onClick);
-    this.removeEventListener('keydown', this._onKeyboardEvent);
-    this.removeEventListener('esl:show:request', this._onShowRequest);
-    this.bindOutsideEventTracking(false);
-    this.bindHoverStateTracking(false);
-  }
-
   /** Bind outside action event listeners */
   protected bindOutsideEventTracking(track: boolean): void {
-    document.body.removeEventListener('keydown', this._onOutsideAction, true);
-    document.body.removeEventListener('mouseup', this._onOutsideAction, true);
-    document.body.removeEventListener('touchend', this._onOutsideAction, true);
-    if (track) {
-      document.body.addEventListener('keydown', this._onOutsideAction, true);
-      document.body.addEventListener('mouseup', this._onOutsideAction, true);
-      document.body.addEventListener('touchend', this._onOutsideAction, true);
-    }
+    track ? this.$$on(this._onOutsideAction) : this.$$off(this._onOutsideAction);
   }
   /** Bind hover events listeners for the Toggleable itself */
   protected bindHoverStateTracking(track: boolean, hideDelay?: number | string): void {
@@ -167,26 +166,22 @@ export class ESLToggleable extends ESLBaseElement {
     if (this._trackHover === track) return;
     this._trackHover = track;
 
-    this.removeEventListener('mouseenter', this._onMouseEnter);
-    this.removeEventListener('mouseleave', this._onMouseLeave);
-    if (this._trackHover) {
-      this.addEventListener('mouseenter', this._onMouseEnter);
-      this.addEventListener('mouseleave', this._onMouseLeave);
-    }
+    track ? this.$$on(this._onMouseEnter) : this.$$off(this._onMouseEnter);
+    track ? this.$$on(this._onMouseLeave) : this.$$off(this._onMouseLeave);
   }
 
   /** Function to merge the result action params */
-  protected mergeDefaultParams(params?: ToggleableActionParams): ToggleableActionParams {
+  protected mergeDefaultParams(params?: ESLToggleableActionParams): ESLToggleableActionParams {
     return Object.assign({}, this.defaultParams, copyDefinedKeys(params));
   }
 
   /** Toggle the element state */
-  public toggle(state: boolean = !this.open, params?: ToggleableActionParams): ESLToggleable {
+  public toggle(state: boolean = !this.open, params?: ESLToggleableActionParams): ESLToggleable {
     return state ? this.show(params) : this.hide(params);
   }
 
   /** Change the element state to active */
-  public show(params?: ToggleableActionParams): ESLToggleable {
+  public show(params?: ESLToggleableActionParams): ESLToggleable {
     params = this.mergeDefaultParams(params);
     this._task.put(this.showTask.bind(this, params), defined(params.showDelay, params.delay));
     this.bindOutsideEventTracking(this.closeOnOutsideAction);
@@ -194,7 +189,7 @@ export class ESLToggleable extends ESLBaseElement {
     return this;
   }
   /** Change the element state to inactive */
-  public hide(params?: ToggleableActionParams): ESLToggleable {
+  public hide(params?: ESLToggleableActionParams): ESLToggleable {
     params = this.mergeDefaultParams(params);
     this._task.put(this.hideTask.bind(this, params), defined(params.hideDelay, params.delay));
     this.bindOutsideEventTracking(false);
@@ -203,22 +198,22 @@ export class ESLToggleable extends ESLBaseElement {
   }
 
   /** Actual show task to execute by toggleable task manger ({@link DelayedTask} out of the box) */
-  protected showTask(params: ToggleableActionParams): void {
+  protected showTask(params: ESLToggleableActionParams): void {
     if (!params.force && this.open) return;
-    if (!params.silent && !this.$$fire('esl:before:show', {detail: {params}})) return;
+    if (!params.silent && !this.$$fire(this.BEFORE_SHOW_EVENT, {detail: {params}})) return;
     this.activator = params.activator;
     this.open = true;
     this.onShow(params);
-    if (!params.silent) this.$$fire('esl:show', {detail: {params}, cancelable: false});
+    if (!params.silent) this.$$fire(this.SHOW_EVENT, {detail: {params}, cancelable: false});
   }
   /** Actual hide task to execute by toggleable task manger ({@link DelayedTask} out of the box) */
-  protected hideTask(params: ToggleableActionParams): void {
+  protected hideTask(params: ESLToggleableActionParams): void {
     if (!params.force && !this.open) return;
-    if (!params.silent && !this.$$fire('esl:before:hide', {detail: {params}})) return;
+    if (!params.silent && !this.$$fire(this.BEFORE_HIDE_EVENT, {detail: {params}})) return;
     this.open = false;
     this.onHide(params);
     this.bindOutsideEventTracking(false);
-    if (!params.silent) this.$$fire('esl:hide', {detail: {params}, cancelable: false});
+    if (!params.silent) this.$$fire(this.HIDE_EVENT, {detail: {params}, cancelable: false});
   }
 
   /**
@@ -226,7 +221,7 @@ export class ESLToggleable extends ESLBaseElement {
    * Inner state and 'open' attribute are not affected and updated before `onShow` execution.
    * Adds CSS classes, update a11y and fire {@link ESLToggleable.REFRESH_EVENT} event by default.
    */
-  protected onShow(params: ToggleableActionParams): void {
+  protected onShow(params: ESLToggleableActionParams): void {
     CSSClassUtils.add(this, this.activeClass);
     CSSClassUtils.add(document.body, this.bodyClass, this);
     this.updateA11y();
@@ -238,7 +233,7 @@ export class ESLToggleable extends ESLBaseElement {
    * Inner state and 'open' attribute are not affected and updated before `onShow` execution.
    * Removes CSS classes and update a11y by default.
    */
-  protected onHide(params: ToggleableActionParams): void {
+  protected onHide(params: ESLToggleableActionParams): void {
     CSSClassUtils.remove(this, this.activeClass);
     CSSClassUtils.remove(document.body, this.bodyClass, this);
     this.updateA11y();
@@ -252,7 +247,7 @@ export class ESLToggleable extends ESLBaseElement {
     this.toggleAttribute('open', this._open = value);
   }
 
-  /** Last component that has activated the element. Uses {@link ToggleableActionParams.activator}*/
+  /** Last component that has activated the element. Uses {@link ESLToggleableActionParams.activator}*/
   public get activator(): HTMLElement | null | undefined {
     return activators.get(this);
   }
@@ -285,50 +280,64 @@ export class ESLToggleable extends ESLBaseElement {
     return !(e instanceof KeyboardEvent && SYSTEM_KEYS.includes(e.key));
   }
 
-  @bind
-  protected _onClick(e: MouseEvent): void {
-    const target = e.target as HTMLElement;
-    if (this.closeTrigger && target.closest(this.closeTrigger)) {
-      this.hide({initiator: 'close', activator: target, event: e});
-    }
+  @listen({
+    event: 'click',
+    selector: (el: ESLToggleable) => el.closeTrigger || ''
+  })
+  protected _onCloseClick(e: MouseEvent): void {
+    this.hide({initiator: 'close', activator: e.target as HTMLElement, event: e});
   }
 
-  @bind
+  @listen({
+    auto: false,
+    event: 'keydown mouseup touchend',
+    target: document.body,
+    capture: true
+  })
   protected _onOutsideAction(e: Event): void {
     if (!this.isOutsideAction(e)) return;
     // Used 0 delay to decrease priority of the request
     this.hide({initiator: 'outsideaction', hideDelay: 0, event: e});
   }
 
-  @bind
+  @listen('keydown')
   protected _onKeyboardEvent(e: KeyboardEvent): void {
     if (this.closeOnEsc && e.key === ESC) {
       this.hide({initiator: 'keyboard', event: e});
     }
   }
 
-  @bind
+  @listen({auto: false, event: 'mouseenter'})
   protected _onMouseEnter(e: MouseEvent): void {
     const hideDelay = this._trackHoverDelay;
-    const baseParams: ToggleableActionParams = {initiator: 'mouseenter', trackHover: true, activator: this.activator, event: e, hideDelay};
+    const baseParams: ESLToggleableActionParams = {initiator: 'mouseenter', trackHover: true, activator: this.activator, event: e, hideDelay};
     this.show(Object.assign(baseParams, this.trackHoverParams));
   }
-  @bind
+  @listen({auto: false, event: 'mouseleave'})
   protected _onMouseLeave(e: MouseEvent): void {
     const hideDelay = this._trackHoverDelay;
-    const baseParams: ToggleableActionParams = {initiator: 'mouseleave', trackHover: true, activator: this.activator, event: e, hideDelay};
+    const baseParams: ESLToggleableActionParams = {initiator: 'mouseleave', trackHover: true, activator: this.activator, event: e, hideDelay};
     this.hide(Object.assign(baseParams, this.trackHoverParams));
   }
 
+  /** Prepares toggle request events param */
+  protected buildRequestParams(e: CustomEvent<ESLToggleableRequestDetails>): ESLToggleableActionParams | null {
+    const detail = e.detail || {};
+    if (!isMatches(this, detail.match)) return null;
+    return Object.assign({}, detail, {event: e});
+  }
+
   /** Actions to execute on show request */
-  @bind
-  protected _onShowRequest(e: CustomEvent<ESLShowRequestDetails>): void {
-    const detail = e.detail;
-    if (isMatches(this, detail?.ignore)) return;
-    const params = {event: e};
-    if (detail && typeof detail.delay === 'number') Object.assign(params, {showDelay: detail.delay});
-    if (detail && typeof detail.params === 'object') Object.assign(params, detail.params || {});
-    this.show(params);
+  @listen((el: ESLToggleable) => el.SHOW_REQUEST_EVENT)
+  protected _onShowRequest(e: CustomEvent<ESLToggleableRequestDetails>): void {
+    const params = this.buildRequestParams(e);
+    params && this.show(params);
+  }
+  /** Actions to execute on hide request */
+  @listen((el: ESLToggleable) => el.HIDE_REQUEST_EVENT)
+  protected _onHideRequest(e: CustomEvent<ESLToggleableRequestDetails>): void {
+    const params = this.buildRequestParams(e);
+    params && this.hide(params);
   }
 }
 
diff --git a/src/modules/esl-trigger/core/esl-trigger.ts b/src/modules/esl-trigger/core/esl-trigger.ts
index d46ecb38d..7554cc046 100644
--- a/src/modules/esl-trigger/core/esl-trigger.ts
+++ b/src/modules/esl-trigger/core/esl-trigger.ts
@@ -10,7 +10,7 @@ import {DeviceDetector} from '../../esl-utils/environment/device-detector';
 import {ESLMediaQuery} from '../../esl-media-query/core';
 import {ESLToggleablePlaceholder} from '../../esl-toggleable/core';
 
-import type {ESLToggleable, ToggleableActionParams} from '../../esl-toggleable/core/esl-toggleable';
+import type {ESLToggleable, ESLToggleableActionParams} from '../../esl-toggleable/core/esl-toggleable';
 
 @ExportNs('Trigger')
 export class ESLTrigger extends ESLBaseElement {
@@ -135,7 +135,7 @@ export class ESLTrigger extends ESLBaseElement {
   }
 
   /** Merge params to pass to the toggleable */
-  protected mergeToggleableParams(this: ESLTrigger, ...params: ToggleableActionParams[]): ToggleableActionParams {
+  protected mergeToggleableParams(this: ESLTrigger, ...params: ESLToggleableActionParams[]): ESLToggleableActionParams {
     return Object.assign({
       initiator: 'trigger',
       activator: this
@@ -143,7 +143,7 @@ export class ESLTrigger extends ESLBaseElement {
   }
 
   /** Show target toggleable with passed params */
-  public showTarget(params: ToggleableActionParams = {}): void {
+  public showTarget(params: ESLToggleableActionParams = {}): void {
     const actionParams = this.mergeToggleableParams({
       delay: parseNumber(this.showDelay)
     }, params);
@@ -152,7 +152,7 @@ export class ESLTrigger extends ESLBaseElement {
     }
   }
   /** Hide target toggleable with passed params */
-  public hideTarget(params: ToggleableActionParams = {}): void {
+  public hideTarget(params: ESLToggleableActionParams = {}): void {
     const actionParams = this.mergeToggleableParams({
       delay: parseNumber(this.hideDelay)
     }, params);
@@ -161,7 +161,7 @@ export class ESLTrigger extends ESLBaseElement {
     }
   }
   /** Toggles target toggleable with passed params */
-  public toggleTarget(params: ToggleableActionParams = {}, state: boolean = !this.active): void {
+  public toggleTarget(params: ESLToggleableActionParams = {}, state: boolean = !this.active): void {
     state ? this.showTarget(params) : this.hideTarget(params);
   }
 
diff --git a/src/modules/esl-utils/dom/traversing.ts b/src/modules/esl-utils/dom/traversing.ts
index d55ceb374..9ca6c8349 100644
--- a/src/modules/esl-utils/dom/traversing.ts
+++ b/src/modules/esl-utils/dom/traversing.ts
@@ -22,7 +22,7 @@ export const createSequenceFinder = (next: IteratorFn, includeSelf: boolean = fa
 export const isMatches = (el: Element, matcher?: string | ((el: Element) => boolean)): boolean => {
   if (typeof matcher === 'string') return el.matches(matcher);
   if (typeof matcher === 'function') return matcher(el);
-  return false;
+  return typeof matcher === 'undefined';
 };
 
 /** @returns first matching next sibling or null*/


Test patch:
diff --git a/src/modules/esl-toggleable/test/toggleable.request.test.ts b/src/modules/esl-toggleable/test/toggleable.request.test.ts
new file mode 100644
index 000000000..2dae4d659
--- /dev/null
+++ b/src/modules/esl-toggleable/test/toggleable.request.test.ts
@@ -0,0 +1,75 @@
+import {ESLToggleable} from '../core/esl-toggleable';
+import {EventUtils} from '../../esl-event-listener/core/api';
+import type {ESLToggleableRequestDetails} from '../core/esl-toggleable';
+
+describe('ESLToggleable: show/hide-request events', () => {
+  beforeAll(() => ESLToggleable.register());
+
+  describe('Direct events correctly caught by toggleable', () => {
+    const $el = document.createElement(ESLToggleable.is) as ESLToggleable;
+    jest.useFakeTimers();
+    beforeAll(() => document.body.appendChild($el));
+    afterAll(() => ($el.parentElement === document.body) && document.body.removeChild($el));
+    test('Direct SHOW_REQUEST leads to show of the toggleable', () => {
+      $el.hide();
+      jest.advanceTimersByTime(1);
+      expect($el.open).toBe(false);
+      $el.dispatchEvent(new CustomEvent($el.SHOW_REQUEST_EVENT));
+      jest.advanceTimersByTime(1);
+      expect($el.open).toBe(true);
+    });
+    test('Direct HIDE_REQUEST leads to show of the toggleable', () => {
+      $el.show();
+      jest.advanceTimersByTime(1);
+      expect($el.open).toBe(true);
+      $el.dispatchEvent(new CustomEvent($el.HIDE_REQUEST_EVENT));
+      jest.advanceTimersByTime(1);
+      expect($el.open).toBe(false);
+    });
+  });
+
+  describe('ESLToggleables catch and filtering bubbling events', () => {
+    const $root = document.createElement(ESLToggleable.is) as ESLToggleable;
+    const $childTbl = document.createElement(ESLToggleable.is) as ESLToggleable;
+    const $button = document.createElement('button');
+    $root.classList.add('root');
+    $root.appendChild($childTbl);
+    $childTbl.appendChild($button);
+
+    jest.useFakeTimers();
+    beforeAll(() => document.body.appendChild($root));
+    afterAll(() => ($root.parentElement === document.body) && document.body.removeChild($root));
+
+    test('Initial hide request passed for all toggleables in hierarchy', () => {
+      EventUtils.dispatch($button, ESLToggleable.prototype.HIDE_REQUEST_EVENT);
+      jest.advanceTimersByTime(1);
+      expect($root.open).toBe(false);
+      expect($childTbl.open).toBe(false);
+    });
+    test('Show request passed for all toggleables in hierarchy', () => {
+      EventUtils.dispatch($button, ESLToggleable.prototype.SHOW_REQUEST_EVENT);
+      jest.advanceTimersByTime(1);
+      expect($root.open).toBe(true);
+      expect($childTbl.open).toBe(true);
+    });
+    test('Hide request passed for all toggleables in hierarchy', () => {
+      EventUtils.dispatch($button, ESLToggleable.prototype.HIDE_REQUEST_EVENT);
+      jest.advanceTimersByTime(1);
+      expect($root.open).toBe(false);
+      expect($childTbl.open).toBe(false);
+    });
+
+    test('Show/hide request with filter processed correctly in toggleables in hierarchy', () => {
+      const detail: ESLToggleableRequestDetails = {match: '.root'};
+      EventUtils.dispatch($button, ESLToggleable.prototype.SHOW_REQUEST_EVENT, {detail});
+      jest.advanceTimersByTime(1);
+      expect($root.open).toBe(true);
+      expect($childTbl.open).toBe(false);
+
+      EventUtils.dispatch($button, ESLToggleable.prototype.HIDE_REQUEST_EVENT, {detail});
+      jest.advanceTimersByTime(1);
+      expect($root.open).toBe(false);
+      expect($childTbl.open).toBe(false);
+    });
+  });
+});
diff --git a/src/modules/esl-utils/decorators/test/listen.test.ts b/src/modules/esl-utils/decorators/test/listen.test.ts
index 45d259903..23a18ad16 100644
--- a/src/modules/esl-utils/decorators/test/listen.test.ts
+++ b/src/modules/esl-utils/decorators/test/listen.test.ts
@@ -2,6 +2,7 @@ import '../../../../polyfills/es5-target-shim';
 
 import {listen} from '../listen';
 import {EventUtils} from '../../dom/events';
+
 import type {ESLListenerDescriptorFn} from '../../dom/events';
 
 describe('Decorator: @listen', () => {
diff --git a/src/modules/esl-utils/dom/test/traversing.test.ts b/src/modules/esl-utils/dom/test/traversing.test.ts
index 2af882a47..145f0a877 100644
--- a/src/modules/esl-utils/dom/test/traversing.test.ts
+++ b/src/modules/esl-utils/dom/test/traversing.test.ts
@@ -49,7 +49,10 @@ describe('Common: dom/traversing helper tests', () => {
     test('returns false if element does not match passed selector or exact predicate function', () => {
       expect(isMatches(div, (el) => root === el)).toBeFalsy();
       expect(isMatches(div, 'span')).toBeFalsy();
-      expect(isMatches(div)).toBeFalsy();
+    });
+    test('returns true if second argument is not passed or undefined', () => {
+      expect(isMatches(div)).toBeTruthy();
+      expect(isMatches(div, undefined)).toBeTruthy();
     });
   });