diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 2055507a32..5a1ba5bbd6 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -89,6 +89,7 @@ export default { "CHECKBOX_HEADER": "Select all rows", "CHECKBOX_ROW": "Select {{value}}", "EXPAND_BUTTON": "Expand row", + "EXPAND_ALL_BUTTON": "Expand all rows", "SORT_DESCENDING": "Sort rows by this header in descending order", "SORT_ASCENDING": "Sort rows by this header in ascending order", "ROW": "row" diff --git a/src/table/head/table-head-expand.component.ts b/src/table/head/table-head-expand.component.ts index f322843bb4..2c0e94448b 100644 --- a/src/table/head/table-head-expand.component.ts +++ b/src/table/head/table-head-expand.component.ts @@ -1,15 +1,47 @@ import { Component, - HostBinding + EventEmitter, + HostBinding, + Input, + Output } from "@angular/core"; +import { I18n } from "carbon-components-angular/i18n"; +import { Observable } from "rxjs"; @Component({ // tslint:disable-next-line: component-selector selector: "[cdsTableHeadExpand], [ibmTableHeadExpand]", template: ` - + + + + ` }) export class TableHeadExpand { @HostBinding("class.cds--table-expand") hostClass = true; + + @Input() showExpandAllToggle = false; + + @Input() expanded = false; + + @Output() expandedChange = new EventEmitter(); + + @HostBinding("attr.data-previous-value") get previousValue() { + return this.expanded ? "collapsed" : null; + } + + protected _ariaLabel = this.i18n.getOverridable("TABLE.EXPAND_ALL_BUTTON"); + + constructor(protected i18n: I18n) { } + + getAriaLabel(): Observable { + return this._ariaLabel.subject; + } } diff --git a/src/table/head/table-head.component.ts b/src/table/head/table-head.component.ts index b758b4e045..569ae0872e 100644 --- a/src/table/head/table-head.component.ts +++ b/src/table/head/table-head.component.ts @@ -31,8 +31,11 @@ import { TableRowSize } from "../table.types"; cdsTableHeadExpand *ngIf="model.hasExpandableRows()" scope="col" + [showExpandAllToggle]="showExpandAllToggle" [ngClass]="{'cds--table-expand-v2': stickyHeader}" - [id]="model.getId('expand')"> + [id]="model.getId('expand')" + [expanded]="model.expandableRowsCount() === model.expandedRowsCount()" + (expandedChange)="onExpandAllRowsChange($event)"> (); + /** + * Emits if all rows are expanded. + * + * @param model + */ + @Output() expandAllRows = new EventEmitter(); + /** + * Emits if all rows are collapsed. + * + * @param model + */ + @Output() collapseAllRows = new EventEmitter(); public scrollbarWidth = 0; @@ -184,6 +201,14 @@ export class TableHead implements AfterViewInit { } } + onExpandAllRowsChange(expand: boolean) { + if (expand) { + this.expandAllRows.emit(this.model); + } else { + this.collapseAllRows.emit(this.model); + } + } + getCheckboxHeaderLabel(): Observable { return this._checkboxHeaderLabel.subject; } diff --git a/src/table/stories/app-expansion-table.component.ts b/src/table/stories/app-expansion-table.component.ts index eacc33d92b..a7a0e289b3 100644 --- a/src/table/stories/app-expansion-table.component.ts +++ b/src/table/stories/app-expansion-table.component.ts @@ -45,8 +45,15 @@ class CustomHeaderItem extends TableHeaderItem { [striped]="striped" (sort)="customSort($event)" (rowClick)="onRowClick($event)" - [isDataGrid]="isDataGrid"> + [isDataGrid]="isDataGrid" + [showExpandAllToggle]="showExpandAllToggle"> + +
+ + + + ` }) export class ExpansionTableStory implements AfterViewInit { @@ -58,6 +65,7 @@ export class ExpansionTableStory implements AfterViewInit { @Input() sortable = true; @Input() stickyHeader = false; @Input() skeleton = false; + @Input() showExpandAllToggle = false; @ViewChild("customHeaderTemplate") protected customHeaderTemplate: TemplateRef; diff --git a/src/table/table-model.class.spec.ts b/src/table/table-model.class.spec.ts index 5aa297e289..7919f17dcc 100644 --- a/src/table/table-model.class.spec.ts +++ b/src/table/table-model.class.spec.ts @@ -760,4 +760,34 @@ describe("Table", () => { expect(tableModel.header[1].data).toEqual("h2"); expect(tableModel.header[2].data).toEqual("h3"); }); + + it("should expand and collapse all rows", () => { + let tableModel = new TableModel(); + + spyOn(tableModel.rowsExpandedAllChange, "emit"); + spyOn(tableModel.rowsCollapsedAllChange, "emit"); + + tableModel.header = [ + new TableHeaderItem({data: "h1"}), new TableHeaderItem({data: "h2"}) + ]; + tableModel.data = [ + [new TableItem({data: "A", expandedData: "EX1"}), new TableItem({data: "B"})], + [new TableItem({data: "C"}), new TableItem({data: "D"})], + [new TableItem({data: "E", expandedData: "EX2"}), new TableItem({data: "F"})], + [new TableItem({data: "G"}), new TableItem({data: "H"})] + ]; + + expect(tableModel.expandableRowsCount()).toBe(2); + expect(tableModel.expandedRowsCount()).toBe(0); + + tableModel.expandAllRows(true); + expect(tableModel.rowsExpandedAllChange.emit).toHaveBeenCalledWith(); + expect(tableModel.expandedRowsCount()).toBe(2); + expect(tableModel.rowsExpanded).toEqual([true, false, true, false]); + + tableModel.expandAllRows(false); + expect(tableModel.rowsCollapsedAllChange.emit).toHaveBeenCalledWith(); + expect(tableModel.expandedRowsCount()).toBe(0); + expect(tableModel.rowsExpanded).toEqual([false, false, false, false]); + }); }); diff --git a/src/table/table-model.class.ts b/src/table/table-model.class.ts index 3af4828bad..73be6d1bc7 100644 --- a/src/table/table-model.class.ts +++ b/src/table/table-model.class.ts @@ -69,6 +69,8 @@ export class TableModel implements PaginationModel { dataChange = new EventEmitter(); rowsSelectedChange = new EventEmitter(); rowsExpandedChange = new EventEmitter(); + rowsExpandedAllChange = new EventEmitter(); + rowsCollapsedAllChange = new EventEmitter(); /** * Gets emitted when `selectAll` is called. Emits false if all rows are deselected and true if * all rows are selected. @@ -413,6 +415,18 @@ export class TableModel implements PaginationModel { return this.data.some(data => data.some(d => d && d.expandedData)); // checking for some in 2D array } + /** + * Number of rows that can be expanded. + * + * @returns number + */ + expandableRowsCount() { + return this.data.reduce((counter, _, index) => { + counter = (this.isRowExpandable(index)) ? counter + 1 : counter; + return counter; + }, 0); + } + isRowExpandable(index: number) { return this.data[index].some(d => d && d.expandedData); } @@ -705,6 +719,27 @@ export class TableModel implements PaginationModel { this.rowsExpandedChange.emit(index); } + /** + * Expands / collapses all rows + * + * @param value expanded state of the rows. `true` is expanded and `false` is collapsed + */ + expandAllRows(value = true) { + if (this.data.length > 0) { + for (let i = 0; i < this.data.length; i++) { + if (this.isRowExpandable(i)) { + this.rowsExpanded[i] = value; + } + } + + if (value) { + this.rowsExpandedAllChange.emit(); + } else { + this.rowsCollapsedAllChange.emit(); + } + } + } + /** * Gets the true index of a row based on it's relative position. * Like in Python, positive numbers start from the top and diff --git a/src/table/table.component.ts b/src/table/table.component.ts index 9e847c6fe6..b804e50ff7 100644 --- a/src/table/table.component.ts +++ b/src/table/table.component.ts @@ -187,6 +187,8 @@ import { TableRowSize } from "./table.types"; [sortable]="sortable" (deselectAll)="onDeselectAll()" (selectAll)="onSelectAll()" + (expandAllRows)="model.expandAllRows(true)" + (collapseAllRows)="model.expandAllRows(false)" (sort)="doSort($event)" [checkboxHeaderLabel]="getCheckboxHeaderLabel()" [filterTitle]="getFilterTitle()" @@ -195,6 +197,7 @@ import { TableRowSize } from "./table.types"; [selectAllCheckboxSomeSelected]="selectAllCheckboxSomeSelected" [showSelectionColumn]="showSelectionColumn" [enableSingleSelect]="enableSingleSelect" + [showExpandAllToggle]="showExpandAllToggle" [skeleton]="skeleton" [sortAscendingLabel]="sortAscendingLabel" [sortDescendingLabel]="sortDescendingLabel" @@ -384,6 +387,11 @@ export class Table implements OnInit, AfterViewInit, OnDestroy { @Input() noBorder = true; + /** + * Set to `true` to show expansion toggle when table consists of row expansions + */ + @Input() showExpandAllToggle = false; + get isDataGrid(): boolean { return this._isDataGrid; } diff --git a/src/table/table.stories.ts b/src/table/table.stories.ts index 1864f04e54..1260862ab6 100644 --- a/src/table/table.stories.ts +++ b/src/table/table.stories.ts @@ -482,7 +482,8 @@ const ExpansionTemplate = (args) => ({ [stickyHeader]="stickyHeader" [skeleton]="skeleton" [striped]="striped" - [isDataGrid]="isDataGrid"> + [isDataGrid]="isDataGrid" + [showExpandAllToggle]="showExpandAllToggle"> ` @@ -491,7 +492,8 @@ export const WithExpansion = ExpansionTemplate.bind({}); WithExpansion.args = { ...getProps({ description: "With expansion" - }, "args") + }, "args"), + showExpandAllToggle: false }; const DyanmicContentTemplate = (args) => ({