Skip to content

Commit c18d9b7

Browse files
authored
Merge pull request #3014 from eduardmarcinco/feat/table-expand-all
feat: allow users to expand all table rows
2 parents 75422dc + 984f3c9 commit c18d9b7

File tree

8 files changed

+147
-6
lines changed

8 files changed

+147
-6
lines changed

src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default {
8989
"CHECKBOX_HEADER": "Select all rows",
9090
"CHECKBOX_ROW": "Select {{value}}",
9191
"EXPAND_BUTTON": "Expand row",
92+
"EXPAND_ALL_BUTTON": "Expand all rows",
9293
"SORT_DESCENDING": "Sort rows by this header in descending order",
9394
"SORT_ASCENDING": "Sort rows by this header in ascending order",
9495
"ROW": "row"
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,47 @@
11
import {
22
Component,
3-
HostBinding
3+
EventEmitter,
4+
HostBinding,
5+
Input,
6+
Output
47
} from "@angular/core";
8+
import { I18n } from "carbon-components-angular/i18n";
9+
import { Observable } from "rxjs";
510

611
@Component({
712
// tslint:disable-next-line: component-selector
813
selector: "[cdsTableHeadExpand], [ibmTableHeadExpand]",
914
template: `
10-
<ng-content></ng-content>
15+
<button
16+
*ngIf="showExpandAllToggle"
17+
class="cds--table-expand__button"
18+
[attr.aria-label]="getAriaLabel() | async"
19+
(click)="expandedChange.emit(!expanded)">
20+
<svg cdsIcon="chevron--right" size="16" class="cds--table-expand__svg"></svg>
21+
</button>
22+
<ng-container *ngIf="!showExpandAllToggle">
23+
<ng-content></ng-content>
24+
</ng-container>
1125
`
1226
})
1327
export class TableHeadExpand {
1428
@HostBinding("class.cds--table-expand") hostClass = true;
29+
30+
@Input() showExpandAllToggle = false;
31+
32+
@Input() expanded = false;
33+
34+
@Output() expandedChange = new EventEmitter<boolean>();
35+
36+
@HostBinding("attr.data-previous-value") get previousValue() {
37+
return this.expanded ? "collapsed" : null;
38+
}
39+
40+
protected _ariaLabel = this.i18n.getOverridable("TABLE.EXPAND_ALL_BUTTON");
41+
42+
constructor(protected i18n: I18n) { }
43+
44+
getAriaLabel(): Observable<string> {
45+
return this._ariaLabel.subject;
46+
}
1547
}

src/table/head/table-head.component.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ import { TableRowSize } from "../table.types";
3131
cdsTableHeadExpand
3232
*ngIf="model.hasExpandableRows()"
3333
scope="col"
34+
[showExpandAllToggle]="showExpandAllToggle"
3435
[ngClass]="{'cds--table-expand-v2': stickyHeader}"
35-
[id]="model.getId('expand')">
36+
[id]="model.getId('expand')"
37+
[expanded]="model.expandableRowsCount() === model.expandedRowsCount()"
38+
(expandedChange)="onExpandAllRowsChange($event)">
3639
</th>
3740
<th
3841
*ngIf="!skeleton && showSelectionColumn && enableSingleSelect"
@@ -102,6 +105,8 @@ export class TableHead implements AfterViewInit {
102105

103106
@Input() stickyHeader = false;
104107

108+
@Input() showExpandAllToggle = false;
109+
105110
/**
106111
* Setting sortable to false will disable all headers including headers which are sortable. Is is
107112
* possible to set the sortable state on the header item to disable/enable sorting for only some headers.
@@ -160,6 +165,18 @@ export class TableHead implements AfterViewInit {
160165
* @param model
161166
*/
162167
@Output() deselectAll = new EventEmitter<TableModel>();
168+
/**
169+
* Emits if all rows are expanded.
170+
*
171+
* @param model
172+
*/
173+
@Output() expandAllRows = new EventEmitter<TableModel>();
174+
/**
175+
* Emits if all rows are collapsed.
176+
*
177+
* @param model
178+
*/
179+
@Output() collapseAllRows = new EventEmitter<TableModel>();
163180

164181
public scrollbarWidth = 0;
165182

@@ -184,6 +201,14 @@ export class TableHead implements AfterViewInit {
184201
}
185202
}
186203

204+
onExpandAllRowsChange(expand: boolean) {
205+
if (expand) {
206+
this.expandAllRows.emit(this.model);
207+
} else {
208+
this.collapseAllRows.emit(this.model);
209+
}
210+
}
211+
187212
getCheckboxHeaderLabel(): Observable<string> {
188213
return this._checkboxHeaderLabel.subject;
189214
}

src/table/stories/app-expansion-table.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ class CustomHeaderItem extends TableHeaderItem {
4545
[striped]="striped"
4646
(sort)="customSort($event)"
4747
(rowClick)="onRowClick($event)"
48-
[isDataGrid]="isDataGrid">
48+
[isDataGrid]="isDataGrid"
49+
[showExpandAllToggle]="showExpandAllToggle">
4950
</cds-table>
51+
52+
<br>
53+
54+
<button cdsButton="primary" size="sm" (click)="model.expandAllRows(true)">Expand all rows</button>
55+
56+
<button cdsButton="secondary" size="sm" (click)="model.expandAllRows(false)">Collapse all rows</button>
5057
`
5158
})
5259
export class ExpansionTableStory implements AfterViewInit {
@@ -58,6 +65,7 @@ export class ExpansionTableStory implements AfterViewInit {
5865
@Input() sortable = true;
5966
@Input() stickyHeader = false;
6067
@Input() skeleton = false;
68+
@Input() showExpandAllToggle = false;
6169

6270
@ViewChild("customHeaderTemplate")
6371
protected customHeaderTemplate: TemplateRef<any>;

src/table/table-model.class.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,4 +760,34 @@ describe("Table", () => {
760760
expect(tableModel.header[1].data).toEqual("h2");
761761
expect(tableModel.header[2].data).toEqual("h3");
762762
});
763+
764+
it("should expand and collapse all rows", () => {
765+
let tableModel = new TableModel();
766+
767+
spyOn(tableModel.rowsExpandedAllChange, "emit");
768+
spyOn(tableModel.rowsCollapsedAllChange, "emit");
769+
770+
tableModel.header = [
771+
new TableHeaderItem({data: "h1"}), new TableHeaderItem({data: "h2"})
772+
];
773+
tableModel.data = [
774+
[new TableItem({data: "A", expandedData: "EX1"}), new TableItem({data: "B"})],
775+
[new TableItem({data: "C"}), new TableItem({data: "D"})],
776+
[new TableItem({data: "E", expandedData: "EX2"}), new TableItem({data: "F"})],
777+
[new TableItem({data: "G"}), new TableItem({data: "H"})]
778+
];
779+
780+
expect(tableModel.expandableRowsCount()).toBe(2);
781+
expect(tableModel.expandedRowsCount()).toBe(0);
782+
783+
tableModel.expandAllRows(true);
784+
expect(tableModel.rowsExpandedAllChange.emit).toHaveBeenCalledWith();
785+
expect(tableModel.expandedRowsCount()).toBe(2);
786+
expect(tableModel.rowsExpanded).toEqual([true, false, true, false]);
787+
788+
tableModel.expandAllRows(false);
789+
expect(tableModel.rowsCollapsedAllChange.emit).toHaveBeenCalledWith();
790+
expect(tableModel.expandedRowsCount()).toBe(0);
791+
expect(tableModel.rowsExpanded).toEqual([false, false, false, false]);
792+
});
763793
});

src/table/table-model.class.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export class TableModel implements PaginationModel {
6969
dataChange = new EventEmitter();
7070
rowsSelectedChange = new EventEmitter<number>();
7171
rowsExpandedChange = new EventEmitter<number>();
72+
rowsExpandedAllChange = new EventEmitter();
73+
rowsCollapsedAllChange = new EventEmitter();
7274
/**
7375
* Gets emitted when `selectAll` is called. Emits false if all rows are deselected and true if
7476
* all rows are selected.
@@ -413,6 +415,18 @@ export class TableModel implements PaginationModel {
413415
return this.data.some(data => data.some(d => d && d.expandedData)); // checking for some in 2D array
414416
}
415417

418+
/**
419+
* Number of rows that can be expanded.
420+
*
421+
* @returns number
422+
*/
423+
expandableRowsCount() {
424+
return this.data.reduce((counter, _, index) => {
425+
counter = (this.isRowExpandable(index)) ? counter + 1 : counter;
426+
return counter;
427+
}, 0);
428+
}
429+
416430
isRowExpandable(index: number) {
417431
return this.data[index].some(d => d && d.expandedData);
418432
}
@@ -705,6 +719,27 @@ export class TableModel implements PaginationModel {
705719
this.rowsExpandedChange.emit(index);
706720
}
707721

722+
/**
723+
* Expands / collapses all rows
724+
*
725+
* @param value expanded state of the rows. `true` is expanded and `false` is collapsed
726+
*/
727+
expandAllRows(value = true) {
728+
if (this.data.length > 0) {
729+
for (let i = 0; i < this.data.length; i++) {
730+
if (this.isRowExpandable(i)) {
731+
this.rowsExpanded[i] = value;
732+
}
733+
}
734+
735+
if (value) {
736+
this.rowsExpandedAllChange.emit();
737+
} else {
738+
this.rowsCollapsedAllChange.emit();
739+
}
740+
}
741+
}
742+
708743
/**
709744
* Gets the true index of a row based on it's relative position.
710745
* Like in Python, positive numbers start from the top and

src/table/table.component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ import { TableRowSize } from "./table.types";
187187
[sortable]="sortable"
188188
(deselectAll)="onDeselectAll()"
189189
(selectAll)="onSelectAll()"
190+
(expandAllRows)="model.expandAllRows(true)"
191+
(collapseAllRows)="model.expandAllRows(false)"
190192
(sort)="doSort($event)"
191193
[checkboxHeaderLabel]="getCheckboxHeaderLabel()"
192194
[filterTitle]="getFilterTitle()"
@@ -195,6 +197,7 @@ import { TableRowSize } from "./table.types";
195197
[selectAllCheckboxSomeSelected]="selectAllCheckboxSomeSelected"
196198
[showSelectionColumn]="showSelectionColumn"
197199
[enableSingleSelect]="enableSingleSelect"
200+
[showExpandAllToggle]="showExpandAllToggle"
198201
[skeleton]="skeleton"
199202
[sortAscendingLabel]="sortAscendingLabel"
200203
[sortDescendingLabel]="sortDescendingLabel"
@@ -384,6 +387,11 @@ export class Table implements OnInit, AfterViewInit, OnDestroy {
384387

385388
@Input() noBorder = true;
386389

390+
/**
391+
* Set to `true` to show expansion toggle when table consists of row expansions
392+
*/
393+
@Input() showExpandAllToggle = false;
394+
387395
get isDataGrid(): boolean {
388396
return this._isDataGrid;
389397
}

src/table/table.stories.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ const ExpansionTemplate = (args) => ({
482482
[stickyHeader]="stickyHeader"
483483
[skeleton]="skeleton"
484484
[striped]="striped"
485-
[isDataGrid]="isDataGrid">
485+
[isDataGrid]="isDataGrid"
486+
[showExpandAllToggle]="showExpandAllToggle">
486487
</app-expansion-table>
487488
</cds-table-container>
488489
`
@@ -491,7 +492,8 @@ export const WithExpansion = ExpansionTemplate.bind({});
491492
WithExpansion.args = {
492493
...getProps({
493494
description: "With expansion"
494-
}, "args")
495+
}, "args"),
496+
showExpandAllToggle: false
495497
};
496498

497499
const DyanmicContentTemplate = (args) => ({

0 commit comments

Comments
 (0)