Skip to content

Commit 3aceb73

Browse files
henetirikimmalerba
authored andcommitted
feat(expansion): add accordion expand/collapse all (#6929) (#7461)
Adds openAll()/closeAll() to perform expand/collapse all on multiple expandable accordions. Closes #6929
1 parent 39936c6 commit 3aceb73

File tree

9 files changed

+180
-5
lines changed

9 files changed

+180
-5
lines changed

src/cdk/accordion/accordion-item.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
1919
import {CdkAccordion} from './accordion';
2020
import {coerceBooleanProperty} from '@angular/cdk/coercion';
21+
import {Subscription} from 'rxjs/Subscription';
2122

2223
/** Used to generate unique ID for each accordion item. */
2324
let nextId = 0;
@@ -31,6 +32,8 @@ let nextId = 0;
3132
exportAs: 'cdkAccordionItem',
3233
})
3334
export class CdkAccordionItem implements OnDestroy {
35+
/** Subscription to openAll/closeAll events. */
36+
private _openCloseAllSubscription = Subscription.EMPTY;
3437
/** Event emitted every time the AccordionItem is closed. */
3538
@Output() closed: EventEmitter<void> = new EventEmitter<void>();
3639
/** Event emitted every time the AccordionItem is opened. */
@@ -97,12 +100,18 @@ export class CdkAccordionItem implements OnDestroy {
97100
this.expanded = false;
98101
}
99102
});
103+
104+
// When an accordion item is hosted in an accordion, subscribe to open/close events.
105+
if (this.accordion) {
106+
this._openCloseAllSubscription = this._subscribeToOpenCloseAllActions();
107+
}
100108
}
101109

102110
/** Emits an event for the accordion item being destroyed. */
103111
ngOnDestroy() {
104112
this.destroyed.emit();
105113
this._removeUniqueSelectionListener();
114+
this._openCloseAllSubscription.unsubscribe();
106115
}
107116

108117
/** Toggles the expanded state of the accordion item. */
@@ -125,4 +134,13 @@ export class CdkAccordionItem implements OnDestroy {
125134
this.expanded = true;
126135
}
127136
}
137+
138+
private _subscribeToOpenCloseAllActions(): Subscription {
139+
return this.accordion._openCloseAllActions.subscribe(expanded => {
140+
// Only change expanded state if item is enabled
141+
if (!this.disabled) {
142+
this.expanded = expanded;
143+
}
144+
});
145+
}
128146
}

src/cdk/accordion/accordion.ts

Lines changed: 20 additions & 0 deletions
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
/** Used to generate unique ID for each accordion. */
1314
let nextId = 0;
@@ -20,6 +21,9 @@ let nextId = 0;
2021
exportAs: 'cdkAccordion',
2122
})
2223
export class CdkAccordion {
24+
/** Stream that emits true/false when openAll/closeAll is triggered. */
25+
readonly _openCloseAllActions: Subject<boolean> = new Subject<boolean>();
26+
2327
/** A readonly id value to use for unique selection coordination. */
2428
readonly id = `cdk-accordion-${nextId++}`;
2529

@@ -28,4 +32,20 @@ export class CdkAccordion {
2832
get multi(): boolean { return this._multi; }
2933
set multi(multi: boolean) { this._multi = coerceBooleanProperty(multi); }
3034
private _multi: boolean = false;
35+
36+
/** Opens all enabled accordion items in an accordion where multi is enabled. */
37+
openAll(): void {
38+
this._openCloseAll(true);
39+
}
40+
41+
/** Closes all enabled accordion items in an accordion where multi is enabled. */
42+
closeAll(): void {
43+
this._openCloseAll(false);
44+
}
45+
46+
private _openCloseAll(expanded: boolean): void {
47+
if (this.multi) {
48+
this._openCloseAllActions.next(expanded);
49+
}
50+
}
3151
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ <h1>matAccordion</h1>
4343
<mat-radio-button value="default">Default</mat-radio-button>
4444
<mat-radio-button value="flat">Flat</mat-radio-button>
4545
</mat-radio-group>
46+
<p>Accordion Actions <sup>('Multi Expansion' mode only)</sup></p>
47+
<div>
48+
<button mat-button (click)="accordion.openAll()" [disabled]="!multi">Expand All</button>
49+
<button mat-button (click)="accordion.closeAll()" [disabled]="!multi">Collapse All</button>
50+
</div>
4651
<p>Accordion Panel(s)</p>
4752
<div>
4853
<mat-checkbox [(ngModel)]="panel1.expanded">Panel 1</mat-checkbox>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, ViewEncapsulation} from '@angular/core';
9+
import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
10+
import {MatAccordion} from '@angular/material';
1011

1112
@Component({
1213
moduleId: module.id,
@@ -17,6 +18,8 @@ import {Component, ViewEncapsulation} from '@angular/core';
1718
preserveWhitespaces: false,
1819
})
1920
export class ExpansionDemo {
21+
@ViewChild(MatAccordion) accordion: MatAccordion;
22+
2023
displayMode: string = 'default';
2124
multi = false;
2225
hideToggle = false;

src/lib/expansion/accordion.spec.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +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';
5+
import {MatExpansionModule, MatAccordion} from './index';
66

77

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

5089

@@ -54,13 +93,16 @@ describe('CdkAccordion', () => {
5493
<mat-expansion-panel-header>Summary</mat-expansion-panel-header>
5594
<p>Content</p>
5695
</mat-expansion-panel>
57-
<mat-expansion-panel [expanded]="secondPanelExpanded">
96+
<mat-expansion-panel [expanded]="secondPanelExpanded" [disabled]="secondPanelDisabled">
5897
<mat-expansion-panel-header>Summary</mat-expansion-panel-header>
5998
<p>Content</p>
6099
</mat-expansion-panel>
61100
</mat-accordion>`})
62101
class SetOfItems {
102+
@ViewChild(MatAccordion) accordion: MatAccordion;
103+
63104
multi: boolean = false;
64105
firstPanelExpanded: boolean = false;
65106
secondPanelExpanded: boolean = false;
107+
secondPanelDisabled: boolean = false;
66108
}

src/lib/expansion/accordion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class MatAccordion extends CdkAccordion {
3333
/**
3434
* The display mode used for all expansion panels in the accordion. Currently two display
3535
* modes exist:
36-
* default - a gutter-like spacing is placed around any expanded panel, placing the expanded
36+
* default - a gutter-like spacing is placed around any expanded panel, placing the expanded
3737
* panel at a different elevation from the reset of the accordion.
3838
* flat - no spacing is placed around expanded panels, showing all panels at the same
3939
* elevation.
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.openAll()">Expand All</button>
3+
<button mat-button (click)="accordion.closeAll()">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 toggles
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)