Skip to content

Commit 835b6fb

Browse files
committed
feat(expansion): add accordion expand/collapse all (#6929)
Add expandAll()/collapseAll() to perform expand/collapse all on multiple expandable accordions. Closes #6929
1 parent 01622b1 commit 835b6fb

File tree

9 files changed

+188
-7
lines changed

9 files changed

+188
-7
lines changed

src/demo-app/expansion/expansion-demo.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ <h1>Accordion</h1>
3636
<mat-radio-button value="default">Default</mat-radio-button>
3737
<mat-radio-button value="flat">Flat</mat-radio-button>
3838
</mat-radio-group>
39+
<p>Accordion Actions <sup>('Multi Expansion' mode only)</sup></p>
40+
<div>
41+
<button mat-button (click)="accordion.expandAll()" [disabled]="!multi">Expand All</button>&nbsp;
42+
<button mat-button (click)="accordion.collapseAll()" [disabled]="!multi">Collapse All</button>
43+
</div>
3944
<p>Accordion Panel(s)</p>
4045
<div>
4146
<mat-checkbox [(ngModel)]="panel1.expanded">Panel 1</mat-checkbox>

src/demo-app/expansion/expansion-demo.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import {Component, ViewEncapsulation} from '@angular/core';
1+
import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
2+
import {MatAccordion} from '@angular/material';
23

34
@Component({
45
moduleId: module.id,
56
selector: 'expansion-demo',
67
styleUrls: ['expansion-demo.css'],
78
templateUrl: 'expansion-demo.html',
89
encapsulation: ViewEncapsulation.None,
9-
preserveWhitespaces: false,
10+
preserveWhitespaces: true,
1011
})
1112
export class ExpansionDemo {
13+
@ViewChild(MatAccordion) accordion: MatAccordion;
14+
1215
displayMode: string = 'default';
1316
multi = false;
1417
hideToggle = false;

src/lib/expansion/accordion.spec.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {async, TestBed} from '@angular/core/testing';
2-
import {Component} from '@angular/core';
2+
import {Component, ViewChild} from '@angular/core';
33
import {By} from '@angular/platform-browser';
44
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
5-
import {MatExpansionModule} from './index';
6-
5+
import {MatExpansionModule, MatAccordion} from './index';
76

87
describe('CdkAccordion', () => {
98
beforeEach(async(() => {
@@ -45,6 +44,45 @@ describe('CdkAccordion', () => {
4544
expect(panels[0].classes['mat-expanded']).toBeTruthy();
4645
expect(panels[1].classes['mat-expanded']).toBeTruthy();
4746
});
47+
48+
it('should expand or collapse all enabled items', () => {
49+
const fixture = TestBed.createComponent(SetOfItems);
50+
const panels = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'));
51+
52+
fixture.componentInstance.multi = true;
53+
fixture.componentInstance.secondPanelExpanded = true;
54+
fixture.detectChanges();
55+
expect(panels[0].classes['mat-expanded']).toBeFalsy();
56+
expect(panels[1].classes['mat-expanded']).toBeTruthy();
57+
58+
fixture.componentInstance.accordion.expandAll();
59+
fixture.detectChanges();
60+
expect(panels[0].classes['mat-expanded']).toBeTruthy();
61+
expect(panels[1].classes['mat-expanded']).toBeTruthy();
62+
63+
fixture.componentInstance.accordion.collapseAll();
64+
fixture.detectChanges();
65+
expect(panels[0].classes['mat-expanded']).toBeFalsy();
66+
expect(panels[1].classes['mat-expanded']).toBeFalsy();
67+
});
68+
69+
it('should not expand or collapse disabled items', () => {
70+
const fixture = TestBed.createComponent(SetOfItems);
71+
const panels = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'));
72+
73+
fixture.componentInstance.multi = true;
74+
fixture.componentInstance.secondPanelDisabled = true;
75+
fixture.detectChanges();
76+
fixture.componentInstance.accordion.expandAll();
77+
fixture.detectChanges();
78+
expect(panels[0].classes['mat-expanded']).toBeTruthy();
79+
expect(panels[1].classes['mat-expanded']).toBeFalsy();
80+
81+
fixture.componentInstance.accordion.collapseAll();
82+
fixture.detectChanges();
83+
expect(panels[0].classes['mat-expanded']).toBeFalsy();
84+
expect(panels[1].classes['mat-expanded']).toBeFalsy();
85+
});
4886
});
4987

5088

@@ -54,13 +92,16 @@ describe('CdkAccordion', () => {
5492
<mat-expansion-panel-header>Summary</mat-expansion-panel-header>
5593
<p>Content</p>
5694
</mat-expansion-panel>
57-
<mat-expansion-panel [expanded]="secondPanelExpanded">
95+
<mat-expansion-panel [expanded]="secondPanelExpanded" [disabled]="secondPanelDisabled">
5896
<mat-expansion-panel-header>Summary</mat-expansion-panel-header>
5997
<p>Content</p>
6098
</mat-expansion-panel>
6199
</mat-accordion>`})
62100
class SetOfItems {
101+
@ViewChild(MatAccordion) accordion: MatAccordion;
102+
63103
multi: boolean = false;
64104
firstPanelExpanded: boolean = false;
65105
secondPanelExpanded: boolean = false;
106+
secondPanelDisabled: boolean = false;
66107
}

src/lib/expansion/accordion.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Directive, Input} from '@angular/core';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {Subject} from 'rxjs/Subject';
1112

1213
/** MatAccordion's display modes. */
1314
export type MatAccordionDisplayMode = 'default' | 'flat';
@@ -25,7 +26,10 @@ export class CdkAccordion {
2526
/** A readonly id value to use for unique selection coordination. */
2627
readonly id = `cdk-accordion-${nextId++}`;
2728

28-
/** Whether the accordion should allow multiple expanded accordion items simulateously. */
29+
/** Stream that emits when expandAll or collapseAll is triggered. */
30+
readonly expandCollapseAllSubject: Subject<boolean> = new Subject<boolean>();
31+
32+
/** Whether the accordion should allow multiple expanded accordion items simultaneously. */
2933
@Input() get multi(): boolean { return this._multi; }
3034
set multi(multi: boolean) { this._multi = coerceBooleanProperty(multi); }
3135
private _multi: boolean = false;
@@ -44,6 +48,26 @@ export class CdkAccordion {
4448
* elevation.
4549
*/
4650
@Input() displayMode: MatAccordionDisplayMode = 'default';
51+
52+
/**
53+
* Expands all enabled expansion panels in an accordion where multi is enabled
54+
*/
55+
expandAll() {
56+
this._expandCollapseAll(true);
57+
}
58+
59+
/**
60+
* Collapses all enabled expansion panels in an accordion where multi is enabled
61+
*/
62+
collapseAll() {
63+
this._expandCollapseAll(false);
64+
}
65+
66+
_expandCollapseAll(expanded: boolean) {
67+
if (this.multi) {
68+
this.expandCollapseAllSubject.next(expanded);
69+
}
70+
}
4771
}
4872

4973
/**

src/lib/expansion/expansion-panel-header.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {EXPANSION_PANEL_ANIMATION_TIMING, MatExpansionPanel} from './expansion-p
8383
})
8484
export class MatExpansionPanelHeader implements OnDestroy {
8585
private _parentChangeSubscription = Subscription.EMPTY;
86+
private _expandCollapseAllSubscription = Subscription.EMPTY;
8687

8788
constructor(
8889
renderer: Renderer2,
@@ -101,6 +102,17 @@ export class MatExpansionPanelHeader implements OnDestroy {
101102
.subscribe(() => this._changeDetectorRef.markForCheck());
102103

103104
_focusMonitor.monitor(_element.nativeElement, renderer, false);
105+
106+
// When a panel is hosted in an accordion, subscribe to the expand/collapse subject
107+
if (this.panel.accordion) {
108+
this._expandCollapseAllSubscription =
109+
this.panel.accordion.expandCollapseAllSubject.subscribe((expanded: boolean) => {
110+
// Only toggle if current state is not the intended state
111+
if (!!this._isExpanded() !== expanded) {
112+
this._toggle();
113+
}
114+
});
115+
}
104116
}
105117

106118
/** Height of the header while the panel is expanded. */
@@ -152,6 +164,7 @@ export class MatExpansionPanelHeader implements OnDestroy {
152164

153165
ngOnDestroy() {
154166
this._parentChangeSubscription.unsubscribe();
167+
this._expandCollapseAllSubscription.unsubscribe();
155168
this._focusMonitor.stopMonitoring(this._element.nativeElement);
156169
}
157170
}

src/material-examples/example-module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {DialogContentExampleDialog,DialogContentExample} from './dialog-content/
3939
import {DialogDataExampleDialog,DialogDataExample} from './dialog-data/dialog-data-example';
4040
import {DialogElementsExampleDialog,DialogElementsExample} from './dialog-elements/dialog-elements-example';
4141
import {DialogOverviewExampleDialog,DialogOverviewExample} from './dialog-overview/dialog-overview-example';
42+
import {ExpansionExpandCollapseAllExample} from './expansion-expand-collapse-all/expansion-expand-collapse-all-example';
4243
import {ExpansionOverviewExample} from './expansion-overview/expansion-overview-example';
4344
import {ExpansionStepsExample} from './expansion-steps/expansion-steps-example';
4445
import {GridListDynamicExample} from './grid-list-dynamic/grid-list-dynamic-example';
@@ -248,6 +249,12 @@ export const EXAMPLE_COMPONENTS = {
248249
additionalFiles: ["dialog-overview-example-dialog.html"],
249250
selectorName: 'DialogOverviewExample, DialogOverviewExampleDialog'
250251
},
252+
'expansion-expand-collapse-all': {
253+
title: 'Accordion with expand/collapse all',
254+
component: ExpansionExpandCollapseAllExample,
255+
additionalFiles: null,
256+
selectorName: null
257+
},
251258
'expansion-overview': {
252259
title: 'Basic expansion panel',
253260
component: ExpansionOverviewExample,
@@ -583,6 +590,7 @@ export const EXAMPLE_LIST = [
583590
DialogDataExampleDialog,DialogDataExample,
584591
DialogElementsExampleDialog,DialogElementsExample,
585592
DialogOverviewExampleDialog,DialogOverviewExample,
593+
ExpansionExpandCollapseAllExample,
586594
ExpansionOverviewExample,
587595
ExpansionStepsExample,
588596
GridListDynamicExample,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.example-action-buttons {
2+
padding-bottom: 20px;
3+
}
4+
5+
.example-headers-align .mat-expansion-panel-header-title,
6+
.example-headers-align .mat-expansion-panel-header-description {
7+
flex-basis: 0;
8+
}
9+
10+
.example-headers-align .mat-expansion-panel-header-description {
11+
justify-content: space-between;
12+
align-items: center;
13+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<div class="example-action-buttons">
2+
<button mat-button (click)="accordion.expandAll()">Expand All</button>
3+
<button mat-button (click)="accordion.collapseAll()">Collapse All</button>
4+
</div>
5+
<mat-accordion class="example-headers-align" [multi]="true">
6+
<mat-expansion-panel>
7+
<mat-expansion-panel-header>
8+
<mat-panel-title>
9+
Personal data
10+
</mat-panel-title>
11+
<mat-panel-description>
12+
Type your name and age
13+
<mat-icon>account_circle</mat-icon>
14+
</mat-panel-description>
15+
</mat-expansion-panel-header>
16+
17+
<mat-form-field>
18+
<input matInput placeholder="First name">
19+
</mat-form-field>
20+
21+
<mat-form-field>
22+
<input matInput type="number" min="1" placeholder="Age">
23+
</mat-form-field>
24+
25+
</mat-expansion-panel>
26+
27+
<mat-expansion-panel [disabled]="true">
28+
<mat-expansion-panel-header>
29+
<mat-panel-title>
30+
Destination
31+
</mat-panel-title>
32+
<mat-panel-description>
33+
Type the country name
34+
<mat-icon>map</mat-icon>
35+
</mat-panel-description>
36+
</mat-expansion-panel-header>
37+
38+
<mat-form-field>
39+
<input matInput placeholder="Country">
40+
</mat-form-field>
41+
</mat-expansion-panel>
42+
43+
<mat-expansion-panel>
44+
<mat-expansion-panel-header>
45+
<mat-panel-title>
46+
Day of the trip
47+
</mat-panel-title>
48+
<mat-panel-description>
49+
Inform the date you wish to travel
50+
<mat-icon>date_range</mat-icon>
51+
</mat-panel-description>
52+
</mat-expansion-panel-header>
53+
54+
<mat-form-field>
55+
<input matInput placeholder="Date" [matDatepicker]="picker" (focus)="picker.open()" readonly>
56+
</mat-form-field>
57+
<mat-datepicker #picker></mat-datepicker>
58+
</mat-expansion-panel>
59+
60+
</mat-accordion>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component, ViewChild} from '@angular/core';
2+
import {MatAccordion} from '@angular/material';
3+
4+
/**
5+
* @title Accordion with expand/collapse all
6+
*/
7+
@Component({
8+
selector: 'expansion-toggle-all-example',
9+
templateUrl: 'expansion-expand-collapse-all-example.html',
10+
styleUrls: ['expansion-expand-collapse-all-example.css']
11+
})
12+
export class ExpansionExpandCollapseAllExample {
13+
@ViewChild(MatAccordion) accordion: MatAccordion;
14+
}

0 commit comments

Comments
 (0)