Skip to content

Commit 23356a0

Browse files
committed
Create Expansion Panel.
1 parent 0e24345 commit 23356a0

22 files changed

+761
-14
lines changed

src/demo-app/demo-app-module.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
3838
import {InputDemo} from './input/input-demo';
3939
import {StyleDemo} from './style/style-demo';
4040
import {DatepickerDemo} from './datepicker/datepicker-demo';
41+
import {ExpansionDemo} from './expansion/expansion-demo';
4142
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/material';
4243
import {
4344
MdAutocompleteModule, MdButtonModule, MdButtonToggleModule, MdCardModule, MdCheckboxModule,
44-
MdChipsModule, MdCoreModule, MdDatepickerModule, MdDialogModule, MdGridListModule, MdIconModule,
45-
MdInputModule, MdListModule, MdMenuModule, MdNativeDateModule, MdProgressBarModule, MdRadioModule,
46-
MdProgressSpinnerModule, MdRippleModule, MdSelectModule, MdSidenavModule, MdSliderModule,
47-
MdSlideToggleModule, MdSnackBarModule, MdTabsModule, MdToolbarModule, MdTooltipModule
45+
MdChipsModule, MdCoreModule, MdDatepickerModule, MdDialogModule, MdExpansionModule,
46+
MdGridListModule, MdIconModule, MdInputModule, MdListModule, MdMenuModule, MdNativeDateModule,
47+
MdProgressBarModule, MdRadioModule, MdProgressSpinnerModule, MdRippleModule, MdSelectModule,
48+
MdSidenavModule, MdSliderModule, MdSlideToggleModule, MdSnackBarModule, MdTabsModule,
49+
MdToolbarModule, MdTooltipModule
4850
} from '@angular/material';
4951

5052
/**
@@ -60,6 +62,7 @@ import {
6062
MdChipsModule,
6163
MdDatepickerModule,
6264
MdDialogModule,
65+
MdExpansionModule,
6366
MdGridListModule,
6467
MdIconModule,
6568
MdInputModule,
@@ -140,6 +143,7 @@ export class DemoMaterialModule {}
140143
RainyTabContent,
141144
FoggyTabContent,
142145
PlatformDemo,
146+
ExpansionDemo,
143147
],
144148
providers: [
145149
{provide: OverlayContainer, useClass: FullscreenOverlayContainer}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class DemoApp {
4242
{name: 'Checkbox', route: 'checkbox'},
4343
{name: 'Datepicker', route: 'datepicker'},
4444
{name: 'Dialog', route: 'dialog'},
45+
{name: 'Expansion Panel', route: 'expansion'},
4546
{name: 'Gestures', route: 'gestures'},
4647
{name: 'Grid List', route: 'grid-list'},
4748
{name: 'Icon', route: 'icon'},

src/demo-app/demo-app/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
3333
import {InputDemo} from '../input/input-demo';
3434
import {StyleDemo} from '../style/style-demo';
3535
import {DatepickerDemo} from '../datepicker/datepicker-demo';
36+
import {ExpansionDemo} from '../expansion/expansion-demo';
3637

3738
export const DEMO_APP_ROUTES: Routes = [
3839
{path: '', component: Home},
@@ -68,4 +69,5 @@ export const DEMO_APP_ROUTES: Routes = [
6869
{path: 'snack-bar', component: SnackBarDemo},
6970
{path: 'platform', component: PlatformDemo},
7071
{path: 'style', component: StyleDemo},
72+
{path: 'expansion', component: ExpansionDemo},
7173
];
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<h1>Single Expansion Panel</h1>
2+
<md-expansion-panel class="md-expansion-demo-width" #myPanel>
3+
<md-expansion-panel-header>
4+
<span class="md-expansion-panel-header-description">This is a panel description.</span>
5+
<span class="md-expansion-panel-header-title">Panel Title</span>
6+
</md-expansion-panel-header>
7+
This is the content text that makes sense here.
8+
<div md-action-row>
9+
<button md-button (click)="myPanel.expanded = false">CANCEL</button>
10+
<button md-button>SAVE</button>
11+
</div>
12+
</md-expansion-panel>
13+
<br>
14+
<h1>Accordion</h1>
15+
<div>
16+
<p>Accordion Options</p>
17+
<div>
18+
<md-slide-toggle [(ngModel)]="multi">Allow Multi Expansion</md-slide-toggle>
19+
<md-slide-toggle [(ngModel)]="hideToggle">Hide Indicators</md-slide-toggle>
20+
<md-slide-toggle [(ngModel)]="showPanel3">Show Panel 3</md-slide-toggle>
21+
</div>
22+
<p>Accordion Style</p>
23+
<md-radio-group [(ngModel)]="displayMode">
24+
<md-radio-button value="default">Default</md-radio-button>
25+
<md-radio-button value="flat">Flat</md-radio-button>
26+
</md-radio-group>
27+
<p>Accordion Panel(s)</p>
28+
<div>
29+
<md-checkbox [(ngModel)]="panel1.expanded">Panel 1</md-checkbox>
30+
<md-checkbox [(ngModel)]="panel2.expanded">Panel 2</md-checkbox>
31+
</div>
32+
</div>
33+
<br>
34+
<div cdk-accordion [displayMode]="displayMode" [multi]="multi"
35+
class="md-expansion-demo-width">
36+
<md-expansion-panel #panel1 [hideToggle]="hideToggle">
37+
<md-expansion-panel-header>
38+
Section 1
39+
</md-expansion-panel-header>
40+
<p>This is the content text that makes sense here.</p>
41+
</md-expansion-panel>
42+
<md-expansion-panel #panel2 [hideToggle]="hideToggle">
43+
<md-expansion-panel-header>
44+
Section 2
45+
</md-expansion-panel-header>
46+
<p>This is the content text that makes sense here.</p>
47+
</md-expansion-panel>
48+
<md-expansion-panel #panel3 *ngIf="showPanel3" [hideToggle]="hideToggle">
49+
<md-expansion-panel-header>
50+
Section 3
51+
</md-expansion-panel-header>
52+
<md-checkbox #showButtons>Reveal Buttons Below</md-checkbox>
53+
<div md-action-row *ngIf="showButtons.checked">
54+
<button md-button (click)="panel2.expanded = true">OPEN SECTION 2</button>
55+
<button md-button (click)="panel3.expanded = false">CLOSE</button>
56+
</div>
57+
</md-expansion-panel>
58+
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.md-expansion-demo-width {
2+
width: 600px;
3+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Component, ViewEncapsulation} from '@angular/core';
2+
3+
@Component({
4+
moduleId: module.id,
5+
selector: 'expansion-demo',
6+
styleUrls: ['expansion-demo.css'],
7+
templateUrl: 'expansion-demo.html',
8+
encapsulation: ViewEncapsulation.None,
9+
})
10+
export class ExpansionDemo {
11+
displayMode: string = 'default';
12+
multi: boolean = false;
13+
hideToggle: boolean = false;
14+
showPanel3 = true;
15+
}

src/lib/core/theming/_all-theme.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
@import '../../chips/chips-theme';
99
@import '../../datepicker/datepicker-theme';
1010
@import '../../dialog/dialog-theme';
11+
@import '../../expansion/expansion-theme';
1112
@import '../../grid-list/grid-list-theme';
1213
@import '../../icon/icon-theme';
1314
@import '../../input/input-theme';
@@ -36,6 +37,7 @@
3637
@include mat-chips-theme($theme);
3738
@include mat-datepicker-theme($theme);
3839
@include mat-dialog-theme($theme);
40+
@include mat-expansion-panel-theme($theme);
3941
@include mat-grid-list-theme($theme);
4042
@include mat-icon-theme($theme);
4143
@include mat-input-theme($theme);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@import '../core/theming/palette';
2+
@import '../core/theming/theming';
3+
4+
@mixin mat-expansion-panel-theme($theme) {
5+
$background: map-get($theme, background);
6+
$foreground: map-get($theme, foreground);
7+
8+
.mat-expansion-panel {
9+
background: mat-color($background, card);
10+
color: mat-color($foreground, base);
11+
}
12+
13+
[mat-action-row],
14+
[md-action-row] {
15+
border-top-color: mat-color($foreground, divider);
16+
}
17+
18+
.mat-expansion-panel-header {
19+
&:focus,
20+
&:hover {
21+
background: mat-color($background, hover);
22+
}
23+
24+
.title {
25+
color: mat-color($foreground, text);
26+
}
27+
28+
.description {
29+
color: mat-color($foreground, secondary-text);
30+
}
31+
32+
.mat-expansion-indicator::after {
33+
color: mat-color($foreground, secondary-text);
34+
}
35+
}
36+
}

src/lib/expansion/accordion-item.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {Output, EventEmitter, OnDestroy} from '@angular/core';
2+
3+
/** Used to generate unique ID for each expansion panel. */
4+
let nextId = 0;
5+
6+
7+
/**
8+
* An abstract class to be extended and decorated as a component. Sets up all
9+
* events and attributes needed to be managed by a CdkAccordion parent.
10+
*/
11+
export abstract class CdkAccordionItem implements OnDestroy {
12+
/** Event emitted every time the MdAccordianChild is closed. */
13+
@Output() closed = new EventEmitter<void>();
14+
/** Event emitted every time the MdAccordianChild is opened. */
15+
@Output() opened = new EventEmitter<void>();
16+
/** Event emitted when the MdAccordianChild is destroyed. */
17+
@Output() destroyed = new EventEmitter<void>();
18+
/** Whether the MdAccordianChild is expanded. */
19+
expanded: boolean;
20+
/** The unique MdAccordianChild id. */
21+
readonly id = `cdk-accordion-child-${nextId++}`;
22+
23+
/** Emits an event for the accordion child being destroyed. */
24+
ngOnDestroy() {
25+
this.destroyed.emit();
26+
}
27+
}

src/lib/expansion/accordion.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {async, TestBed} from '@angular/core/testing';
2+
import {Component} from '@angular/core';
3+
import {By} from '@angular/platform-browser';
4+
import {MdExpansionModule} from './index';
5+
6+
7+
describe('CdkAccordion', () => {
8+
beforeEach(async(() => {
9+
TestBed.configureTestingModule({
10+
imports: [MdExpansionModule],
11+
declarations: [
12+
SetOfItems
13+
],
14+
});
15+
TestBed.compileComponents();
16+
}));
17+
18+
it('should ensure only one item is expanded at a time', () => {
19+
let fixture = TestBed.createComponent(SetOfItems);
20+
let items = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'));
21+
22+
fixture.componentInstance.firstPanelExpanded = true;
23+
fixture.detectChanges();
24+
expect(items[0].classes['mat-expanded']).toBeTruthy();
25+
expect(items[1].classes['mat-expanded']).toBeFalsy();
26+
27+
fixture.componentInstance.secondPanelExpanded = true;
28+
fixture.detectChanges();
29+
expect(items[0].classes['mat-expanded']).toBeFalsy();
30+
expect(items[1].classes['mat-expanded']).toBeTruthy();
31+
});
32+
33+
it('should allow multiple items to be expanded simultaneously', () => {
34+
let fixture = TestBed.createComponent(SetOfItems);
35+
let panels = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'));
36+
37+
fixture.componentInstance.multi = true;
38+
fixture.componentInstance.firstPanelExpanded = true;
39+
fixture.componentInstance.secondPanelExpanded = true;
40+
fixture.detectChanges();
41+
expect(panels[0].classes['mat-expanded']).toBeTruthy();
42+
expect(panels[1].classes['mat-expanded']).toBeTruthy();
43+
});
44+
});
45+
46+
47+
@Component({template: `
48+
<div cdk-accordion [multi]="multi">
49+
<md-expansion-panel [expanded]="firstPanelExpanded">
50+
<md-expansion-panel-header>Summary</md-expansion-panel-header>
51+
<p>Content</p>
52+
</md-expansion-panel>
53+
<md-expansion-panel [expanded]="secondPanelExpanded">
54+
<md-expansion-panel-header>Summary</md-expansion-panel-header>
55+
<p>Content</p>
56+
</md-expansion-panel>
57+
</div>`})
58+
class SetOfItems {
59+
multi: boolean = false;
60+
firstPanelExpanded: boolean = false;
61+
secondPanelExpanded: boolean = false;
62+
}

src/lib/expansion/accordion.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
Directive,
3+
ElementRef,
4+
EventEmitter,
5+
Host,
6+
Input,
7+
Output,
8+
ViewEncapsulation,
9+
AfterContentInit,
10+
OnDestroy,
11+
ContentChildren,
12+
QueryList,
13+
Optional,
14+
forwardRef,
15+
} from '@angular/core';
16+
import {
17+
trigger,
18+
state,
19+
style,
20+
transition,
21+
animate,
22+
} from '@angular/animations';
23+
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
24+
import {Subscription} from 'rxjs/Subscription';
25+
import {Subject} from 'rxjs/Subject';
26+
import 'rxjs/add/operator/takeUntil';
27+
import {CdkAccordionItem} from './accordion-item';
28+
29+
export type CdkAccordionDisplayMode = 'default' | 'flat';
30+
31+
32+
/**
33+
* Directive whose purpose is to manage the expanded state of CdkAccordionItem children.
34+
*/
35+
@Directive({
36+
selector: '[cdk-accordion]',
37+
})
38+
export class CdkAccordion implements AfterContentInit, OnDestroy {
39+
private _hideToggle: boolean = false;
40+
/** Whether the expansion indicator should be hidden. */
41+
@Input() get hideToggle(): boolean { return this._hideToggle; }
42+
set hideToggle(show: boolean) { this._hideToggle = coerceBooleanProperty(show); }
43+
44+
/** Whether the panel set should use flat styling. */
45+
@Input() displayMode: CdkAccordionDisplayMode = 'default';
46+
47+
private _multi: boolean = false;
48+
/** Whether the panel set should allow multiple open panels. */
49+
@Input() get multi(): boolean { return this._multi; }
50+
set multi(multi: boolean) { this._multi = coerceBooleanProperty(multi); }
51+
52+
/** A subject to be completed on destroy to clean up subscriptions. */
53+
private _destroySubject = new Subject();
54+
55+
/** A list of subscriptions for panel open events. */
56+
private _itemsSet: Map<CdkAccordionItem, boolean> = new Map();
57+
58+
/** QueryList of all expansion panels in the expansion panel set. */
59+
@ContentChildren(CdkAccordionItem) private _items: QueryList<CdkAccordionItem>;
60+
61+
/** Set up event subscriptions for all panels for when they open, closing the other panels. */
62+
ngAfterContentInit() {
63+
this._registerItems(this._items);
64+
// Use takeUntil to properly clean up listening to the QueryList changes observable.
65+
this._items.changes
66+
.takeUntil(this._destroySubject)
67+
.subscribe((items: QueryList<CdkAccordionItem>) => this._registerItems(items));
68+
}
69+
70+
/** Clean up panel subscriptions. */
71+
ngOnDestroy() {
72+
this._destroySubject.next();
73+
this._destroySubject.complete();
74+
}
75+
76+
/** Registers a QueryList of panels. */
77+
private _registerItems(panels: QueryList<CdkAccordionItem>) {
78+
this._items.forEach(panel => this._registerItem(panel));
79+
}
80+
81+
/**
82+
* Registers a CdkAccordionItem panel if has not already been registered. Creates subscriptions
83+
* for the open and destroy events for the panel.
84+
*/
85+
private _registerItem(item: CdkAccordionItem) {
86+
// If the accordian item is already in the map, exit method.
87+
if (this._itemsSet.has(item)) {
88+
return;
89+
}
90+
// Subscribes to an accordion item's open event emitter to coordinate the expanded states.
91+
item.opened
92+
.takeUntil(this._destroySubject)
93+
.subscribe(() => {
94+
if (!this.multi) {
95+
this._itemsSet.forEach(
96+
(_: boolean, i: CdkAccordionItem) => {
97+
i.expanded = (item.id === i.id);
98+
});
99+
}
100+
});
101+
// Subscribes to an accordian item's destory event emitter to remove the accord item from the
102+
// map when appropriate.
103+
item.destroyed
104+
.takeUntil(this._destroySubject)
105+
.subscribe(() => this._itemsSet.delete(item));
106+
// Adds the accordian item to the map.
107+
this._itemsSet.set(item, true);
108+
}
109+
}

0 commit comments

Comments
 (0)