Repository: exadel-inc/esl (ts)

Issue:
[🔧 esl-panel-group]: internal check for `has-opened` is inconsitent
As a consumer/architect, I want to have consistent and clear conditions for `esl-panel-group` component behavior.

**Current state:**
 - initial `has-opened` state = `this.$activePanels.length > 0` which is logical
 - on panels state change, it is updated with the event panel state `this.toggleAttribute('has-opened', panel.open);` which is inconsistent


**Expected behavior:**
 - `this.$activePanels.length > 0` - the only criteria to set `has-opened` state

**Scope**
 - Cover `has-opened` marker state with unit tests
 - Make behavior consistent

Originally proposed by @andreybelous 


Pull request description:
fix(esl-panel-group): `has-opened` attribute inconsistent behavior
Closes #2082 

Patch:
diff --git a/src/modules/esl-panel-group/core/esl-panel-group.ts b/src/modules/esl-panel-group/core/esl-panel-group.ts
index ac05dbefd..240f61b2a 100644
--- a/src/modules/esl-panel-group/core/esl-panel-group.ts
+++ b/src/modules/esl-panel-group/core/esl-panel-group.ts
@@ -105,11 +105,15 @@ export class ESLPanelGroup extends ESLBaseElement {
 
     this.updateModeCls();
     this.reset();
-    this.toggleAttribute('has-opened', this.$activePanels.length > 0);
+    this.updateMarkers();
 
     if (prevMode !== currentMode) this.$$fire(this.MODE_CHANGE_EVENT, {detail: {prevMode, currentMode}});
   }
 
+  protected updateMarkers(): void {
+    this.$$attr('has-opened', this.$activePanels.length > 0);
+  }
+
   /** Updates mode class marker */
   protected updateModeCls(): void {
     const {modeCls, currentMode} = this;
@@ -304,7 +308,7 @@ export class ESLPanelGroup extends ESLBaseElement {
   protected _onStateChanged(e: CustomEvent): void {
     const panel = e.target;
     if (!this.includesPanel(panel)) return;
-    this.toggleAttribute('has-opened', panel.open);
+    this.updateMarkers();
 
     if (this.currentMode !== 'tabs') return;
 


Test patch:
diff --git a/src/modules/esl-panel-group/test/esl-panel-group.test.ts b/src/modules/esl-panel-group/test/esl-panel-group.test.ts
new file mode 100644
index 000000000..ce903fca9
--- /dev/null
+++ b/src/modules/esl-panel-group/test/esl-panel-group.test.ts
@@ -0,0 +1,71 @@
+import {ESLPanel} from '../../esl-panel/core';
+import {ESLPanelGroup} from '../../esl-panel-group/core';
+import {ESLTestTemplate} from '../../esl-utils/test/template';
+
+const REFERENCES = {
+  group: 'esl-panel-group',
+  panel: 'esl-panel',
+};
+
+describe('ESLPanelGroup has-opened attribute behavior', () => {
+  ESLPanelGroup.register();
+  ESLPanel.register();
+
+  describe('Initially opened panel group', () => {
+    const TEMPLATE = ESLTestTemplate.create(`
+      <esl-panel-group min-open-items="0">
+        <esl-panel></esl-panel>
+        <esl-panel></esl-panel>
+        <esl-panel open></esl-panel>
+      </esl-panel-group>
+    `, REFERENCES).bind('beforeall');
+
+
+    test('"has-opened" attribute should be present initially', () => expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true));
+
+    test('"has-opened" attribute should be removed after closing all panels', () => {
+      TEMPLATE.$$panel.forEach((panel: ESLPanel) => panel.hide());
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(false);
+    });
+
+    test('"has-opened" attribute should be set after opening panels again', () => {
+      (TEMPLATE.$$panel[0] as ESLPanel).show();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+      (TEMPLATE.$$panel[1] as ESLPanel).show();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+      (TEMPLATE.$$panel[2] as ESLPanel).show();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+    });
+
+    test('"has-opened" attribute should be removed only after last panel is closed', () => {
+      (TEMPLATE.$$panel[0] as ESLPanel).hide();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+      (TEMPLATE.$$panel[1] as ESLPanel).hide();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+      (TEMPLATE.$$panel[2] as ESLPanel).hide();
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(false);
+    });
+  });
+
+  describe('Initially closed panel group', () => {
+    const TEMPLATE = ESLTestTemplate.create(`
+      <esl-panel-group min-open-items="0">
+        <esl-panel></esl-panel>
+        <esl-panel></esl-panel>
+        <esl-panel></esl-panel>
+      </esl-panel-group>
+    `, REFERENCES).bind('beforeall');
+
+    test('"has-opened" attribute shouldn`t be set initially', () => expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(false));
+
+    test('"has-opened" attribute should be set after opening all panels', () => {
+      TEMPLATE.$$panel.forEach((panel: ESLPanel) => panel.show());
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(true);
+    });
+
+    test('"has-opened" attribute should be removed after closing all panels', () => {
+      TEMPLATE.$$panel.forEach((panel: ESLPanel) => panel.hide());
+      expect(TEMPLATE.$group.hasAttribute('has-opened')).toBe(false);
+    });
+  });
+});