Add expand/collapse functionality to table Component

This commit will add optional expand/collapse functionality
to the shared table component. Expand/collapse is not
implemented on any existing table but will be used on the
redesigned event log table.

Tested on Chrome, Safari, Firefox, Edge, IE

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Ia7ecde7b5525c11c68ebdf9f609c8d690c312969
diff --git a/app/common/components/table/table.html b/app/common/components/table/table.html
index 6b2420d..96ca870 100644
--- a/app/common/components/table/table.html
+++ b/app/common/components/table/table.html
@@ -43,8 +43,21 @@
   <tbody class="bmc-table__body">
     <!-- Data rows -->
     <tr ng-if="$ctrl.data.length > 0"
-        ng-repeat="row in $ctrl.data"
-        class="bmc-table__row">
+        ng-repeat-start="row in $ctrl.data track by $index"
+        class="bmc-table__row"
+        ng-class="{
+          'bmc-table__row--expanded': $ctrl.expandedRows.has($index)
+        }">
+      <!-- Row expansion trigger -->
+      <td ng-if="$ctrl.expandable"
+          class="bmc-table__cell">
+        <button type="button"
+                class="btn  btn--expand"
+                aria-label="expand row"
+                ng-click="$ctrl.onClickExpand($index)">
+          <icon file="icon-chevron-right.svg" aria-hidden="true"></icon>
+        </button>
+      </td>
       <!-- Row item -->
       <td ng-repeat="item in row.uiData track by $index"
           class="bmc-table__cell">
@@ -59,9 +72,23 @@
         </table-actions>
       </td>
     </tr>
+    <!-- Expansion row -->
+    <tr ng-repeat-end
+        ng-if="$ctrl.expandedRows.has($index)"
+        class="bmc-table__expansion-row">
+      <td class="bmc-table__cell"></td>
+      <td class="bmc-table__cell"
+          colspan="{{$ctrl.header.length - 1}}">
+        <ng-bind-html
+          ng-bind-html="row.expandContent || 'No data'">
+        </ng-bind-html>
+      </td>
+    </tr>
     <!-- Empty table -->
-    <tr ng-if="$ctrl.data.length === 0">
-      <td>No data</td>
+    <tr ng-if="$ctrl.data.length === 0"
+        class="bmc-table__expansion-row">
+      <td class="bmc-table__cell"
+          colspan="{{$ctrl.header.length}}">No data</td>
     </tr>
   </tbody>
 </table>
\ No newline at end of file
diff --git a/app/common/components/table/table.js b/app/common/components/table/table.js
index 24aa0c9..5db05b6 100644
--- a/app/common/components/table/table.js
+++ b/app/common/components/table/table.js
@@ -15,6 +15,9 @@
    * Each row object in the data array can optionally have an
    * 'actions' property that should be an array of actions to provide the
    * <bmc-table-actions> component.
+   * Each row object can optionally have an 'expandContent' property
+   * that should be a string value and can contain valid HTML. To render
+   * the expanded content, set 'expandable' attribute to true.
    *
    * data = [
    *  { uiData: ['root', 'Admin', 'enabled' ] },
@@ -39,7 +42,10 @@
    *
    * The 'row-actions-enabled' attribute, should be a boolean value
    * Can be set to true to render table row actions. Defaults to false.
-   *  Row actions are defined in data.actions.
+   * Row actions are defined in data.actions.
+   *
+   * The 'expandable' attribute should be a boolean value. If true each
+   * row object in data array should contain a 'expandContent' property
    *
    * The 'size' attribute which can be set to 'small' which will
    * render a smaller font size in the table.
@@ -49,6 +55,7 @@
   const TableController = function() {
     this.sortAscending = true;
     this.activeSort;
+    this.expandedRows = new Set();
 
     /**
      * Sorts table data
@@ -69,6 +76,33 @@
     };
 
     /**
+     * Prep table
+     * Make adjustments to account for optional configurations
+     */
+    const prepTable = () => {
+      if (this.sortable) {
+        // If sort is enabled, check for undefined 'sortable'
+        // property for each item in header array
+        this.header = this.header.map((column) => {
+          column.sortable =
+              column.sortable === undefined ? true : column.sortable;
+          return column;
+        })
+      }
+      if (this.rowActionsEnabled) {
+        // If table actions are enabled push an empty
+        // string to the header array to account for additional
+        // table actions cell
+        this.header.push({label: '', sortable: false});
+      }
+      if (this.expandable) {
+        // If table is expandable, push an empty string to the
+        // header array to account for additional expansion cell
+        this.header.unshift({label: '', sortable: false});
+      }
+    };
+
+    /**
      * Callback when table row action clicked
      * Emits user desired action and associated row data to
      * parent controller
@@ -100,6 +134,18 @@
     };
 
     /**
+     * Callback when expand trigger clicked
+     * @param {number} row : index of expanded row
+     */
+    this.onClickExpand = (row) => {
+      if (this.expandedRows.has(row)) {
+        this.expandedRows.delete(row)
+      } else {
+        this.expandedRows.add(row);
+      }
+    };
+
+    /**
      * onInit Component lifecycle hook
      * Checking for undefined values
      */
@@ -110,6 +156,7 @@
       this.rowActionsEnabled =
           this.rowActionsEnabled === undefined ? false : this.rowActionsEnabled;
       this.size = this.size === undefined ? '' : this.size;
+      this.expandable = this.expandable === undefined ? false : this.expandable;
 
       // Check for undefined 'uiData' property for each item in data array
       this.data = this.data.map((row) => {
@@ -118,21 +165,7 @@
         }
         return row;
       })
-      if (this.sortable) {
-        // If sort is enabled, check for undefined 'sortable'
-        // property for each item in header array
-        this.header = this.header.map((column) => {
-          column.sortable =
-              column.sortable === undefined ? true : column.sortable;
-          return column;
-        })
-      }
-      if (this.rowActionsEnabled) {
-        // If table actions are enabled push an empty
-        // string to the header array to account for additional
-        // table actions cell
-        this.header.push({label: '', sortable: false});
-      }
+      prepTable();
     };
 
     /**
@@ -165,6 +198,7 @@
       size: '<',               // string
       sortable: '<',           // boolean
       defaultSort: '<',        // number (index of sort)
+      expandable: '<',         // boolean
       emitAction: '&'
     }
   })
diff --git a/app/common/styles/components/table.scss b/app/common/styles/components/table.scss
index 613d88a..0178486 100644
--- a/app/common/styles/components/table.scss
+++ b/app/common/styles/components/table.scss
@@ -178,6 +178,31 @@
 
 .bmc-table__row {
   border-bottom: 1px solid $border-color-01;
+  .btn {
+    padding-top: 0;
+    padding-bottom: 0;
+    .icon {
+      margin: 0;
+    }
+  }
+  .btn--expand {
+    padding: 0;
+    .icon {
+      transition: transform $duration--moderate-01;
+      transform: rotate(90deg);
+    }
+  }
+}
+
+.bmc-table__row--expanded {
+  border-style: none;
+  .btn--expand .icon {
+    transform: rotate(0deg);
+  }
+}
+
+.bmc-table__expansion-row {
+  border-bottom: 1px solid $border-color-01;
 }
 
 .bmc-table__column-header {
@@ -198,8 +223,4 @@
 
 .bmc-table__row-actions {
   text-align: right;
-  .btn {
-    padding-top: 0;
-    padding-bottom: 0;
-  }
 }
\ No newline at end of file