Repository: exadel-inc/esl
Pull request #158 (2021-04-22 19:55:53)

Bugfix/toggleable delays


Patch:
diff --git a/src/modules/esl-toggleable/core/esl-toggleable-dispatcher.ts b/src/modules/esl-toggleable/core/esl-toggleable-dispatcher.ts
index 4788dfd25..9bc600805 100644
--- a/src/modules/esl-toggleable/core/esl-toggleable-dispatcher.ts
+++ b/src/modules/esl-toggleable/core/esl-toggleable-dispatcher.ts
@@ -70,14 +70,20 @@ export class ESLToggleableDispatcher extends ESLBaseElement {
   }
 
   /** Hide active element in group */
-  public hideActive(groupName: string) {
-    this.getActive(groupName)?.hide();
+  public hideActive(groupName: string, activator?: HTMLElement) {
+    const active = this.getActive(groupName);
+    if (!active || active === activator) return;
+    active.hide({
+      initiator: 'dispatcher',
+      dispatcher: this,
+      activator
+    });
   }
 
   /** Set active element in group */
   public setActive(groupName: string, popup: ESLToggleable) {
     if (!groupName) return;
-    this.hideActive(groupName);
+    this.hideActive(groupName, popup);
     this._popups.set(groupName, popup);
   }
 
@@ -97,7 +103,7 @@ export class ESLToggleableDispatcher extends ESLBaseElement {
   protected _onBeforeShow(e: CustomEvent) {
     const target = EventUtils.source(e);
     if (!this.isAcceptable(target)) return;
-    this.hideActive(target.groupName);
+    this.hideActive(target.groupName, target);
   }
 
   /** Update active element after a new element is shown */
diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts
index 0258e59ea..61b4eb041 100644
--- a/src/modules/esl-toggleable/core/esl-toggleable.ts
+++ b/src/modules/esl-toggleable/core/esl-toggleable.ts
@@ -2,7 +2,7 @@ import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {ESC} from '../../esl-utils/dom/keys';
 import {CSSClassUtils} from '../../esl-utils/dom/class';
 import {bind} from '../../esl-utils/decorators/bind';
-import {defined} from '../../esl-utils/misc/object';
+import {defined, copyDefinedKeys} from '../../esl-utils/misc/object';
 import {DeviceDetector} from '../../esl-utils/environment/device-detector';
 import {DelayedTask} from '../../esl-utils/async/delayed-task';
 import {ESLBaseElement, attr, jsonAttr, boolAttr} from '../../esl-base-element/core';
@@ -79,7 +79,8 @@ export class ESLToggleable extends ESLBaseElement {
     if (!this.connected || newVal === oldVal) return;
     switch (attrName) {
       case 'open':
-        this.toggle(this.open, Object.assign({initiator: 'attribute'}, this.defaultParams));
+        if (this._open === this.open) return;
+        this.toggle(this.open, {initiator: 'attribute', showDelay: 0, hideDelay: 0});
         break;
       case 'group':
         this.$$fire('change:group',  {
@@ -144,7 +145,7 @@ export class ESLToggleable extends ESLBaseElement {
 
   /** Function to merge the result action params */
   protected mergeDefaultParams(params?: ToggleableActionParams): ToggleableActionParams {
-    return Object.assign({}, this.defaultParams, params || {});
+    return Object.assign({}, this.defaultParams, copyDefinedKeys(params));
   }
 
   /** Toggle the element state */
@@ -229,7 +230,7 @@ export class ESLToggleable extends ESLBaseElement {
   protected _onClick(e: MouseEvent) {
     const target = e.target as HTMLElement;
     if (this.closeTrigger && target.closest(this.closeTrigger)) {
-      this.hide({initiator: 'close', activator: target});
+      this.hide({initiator: 'close', activator: target, event: e});
     }
   }
   @bind
@@ -238,22 +239,22 @@ export class ESLToggleable extends ESLBaseElement {
     if (this.contains(target)) return;
     if (this.activator && this.activator.contains(target)) return;
     // Used 0 delay to decrease priority of the request
-    this.hide({initiator: 'outsideaction', activator: target, hideDelay: 0});
+    this.hide({initiator: 'outsideaction', activator: target, hideDelay: 0, event: e});
   }
 
   @bind
   protected _onKeyboardEvent(e: KeyboardEvent) {
     if (this.closeOnEsc && e.key === ESC) {
-      this.hide({initiator: 'keyboard'});
+      this.hide({initiator: 'keyboard', event: e});
     }
   }
 
   @bind
-  protected _onMouseEnter() {
-    this.show({initiator: 'mouseenter', trackHover: true});
+  protected _onMouseEnter(e: MouseEvent) {
+    this.show({initiator: 'mouseenter', trackHover: true, activator: this, event: e});
   }
   @bind
-  protected _onMouseLeave() {
-    this.hide({initiator: 'mouseleave', trackHover: true});
+  protected _onMouseLeave(e: MouseEvent) {
+    this.hide({initiator: 'mouseleave', trackHover: true, activator: this, event: e});
   }
 }
diff --git a/src/modules/esl-utils/misc/object.ts b/src/modules/esl-utils/misc/object.ts
index 4dbc0eeea..9f8df5f39 100644
--- a/src/modules/esl-utils/misc/object.ts
+++ b/src/modules/esl-utils/misc/object.ts
@@ -24,15 +24,22 @@ export function getPropertyDescriptor(o: any, prop: PropertyKey) {
   }
 }
 
-/**
- * Find the first defined param
- */
+/** @returns first defined param */
 export function defined<T>(...params: T[]) {
   for (const param of params) {
     if (param !== undefined) return param;
   }
 }
 
+/** Makes a flat copy without undefined keys */
+export function copyDefinedKeys<T>(obj?: T): Partial<T> {
+  const result: any = Object.assign({}, obj || {});
+  Object.keys(result).forEach((key) => {
+    (result[key] === void 0) && delete result[key];
+  });
+  return result;
+}
+
 /**
  * Set object property using "path" key
  *


Test patch:
diff --git a/src/modules/esl-utils/misc/test/object.test.ts b/src/modules/esl-utils/misc/test/object.test.ts
index 04dd49cdb..f873dc957 100644
--- a/src/modules/esl-utils/misc/test/object.test.ts
+++ b/src/modules/esl-utils/misc/test/object.test.ts
@@ -1,4 +1,4 @@
-import {defined, deepCompare, getPropertyDescriptor, get, set} from '../object';
+import {defined, deepCompare, getPropertyDescriptor, get, set, copyDefinedKeys} from '../object';
 
 describe('misc/object', () => {
   describe('deepCompare', () => {
@@ -97,6 +97,20 @@ describe('misc/object', () => {
     });
   });
 
+  describe('copyDefinedKeys', () => {
+    test.each([
+      [undefined, {}],
+      [null, {}],
+      [{}, {}],
+      [{a: 1, b: undefined}, {a: 1}],
+      [{a: undefined, b: undefined}, {}],
+      [{a: 1, b: 2}, {a: 1, b: 2}],
+      [{a: 1, b: {}}, {a: 1, b: {}}]
+    ])('%p to %p', (inp, out) => {
+      expect(copyDefinedKeys(inp)).toEqual(out);
+    })
+  });
+
   describe('get', () => {
     test.each([
       ['', {a: 1}, undefined],