Skip to content

Commit 66820fe

Browse files
committed
Create Expansion Panel.
1 parent 3bc82f6 commit 66820fe

22 files changed

+775
-6
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {DataTableDemo} from './data-table/data-table-demo';
4141
import {PeopleDatabase} from './data-table/people-database';
4242
import {DatepickerDemo} from './datepicker/datepicker-demo';
4343
import {TypographyDemo} from './typography/typography-demo';
44+
import {ExpansionDemo} from './expansion/expansion-demo';
4445
import {
4546
CdkDataTableModule,
4647
FullscreenOverlayContainer,
@@ -53,6 +54,7 @@ import {
5354
MdCoreModule,
5455
MdDatepickerModule,
5556
MdDialogModule,
57+
MdExpansionModule,
5658
MdGridListModule,
5759
MdIconModule,
5860
MdInputModule,
@@ -87,6 +89,7 @@ import {
8789
MdChipsModule,
8890
MdDatepickerModule,
8991
MdDialogModule,
92+
MdExpansionModule,
9093
MdGridListModule,
9194
MdIconModule,
9295
MdInputModule,
@@ -170,6 +173,7 @@ export class DemoMaterialModule {}
170173
FoggyTabContent,
171174
PlatformDemo,
172175
TypographyDemo,
176+
ExpansionDemo,
173177
],
174178
providers: [
175179
{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
@@ -43,6 +43,7 @@ export class DemoApp {
4343
{name: 'Data Table', route: 'data-table'},
4444
{name: 'Datepicker', route: 'datepicker'},
4545
{name: 'Dialog', route: 'dialog'},
46+
{name: 'Expansion Panel', route: 'expansion'},
4647
{name: 'Gestures', route: 'gestures'},
4748
{name: 'Grid List', route: 'grid-list'},
4849
{name: 'Icon', route: 'icon'},

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {StyleDemo} from '../style/style-demo';
3535
import {DatepickerDemo} from '../datepicker/datepicker-demo';
3636
import {DataTableDemo} from '../data-table/data-table-demo';
3737
import {TypographyDemo} from '../typography/typography-demo';
38+
import {ExpansionDemo} from '../expansion/expansion-demo';
3839

3940
export const DEMO_APP_ROUTES: Routes = [
4041
{path: '', component: Home},
@@ -71,5 +72,6 @@ export const DEMO_APP_ROUTES: Routes = [
7172
{path: 'snack-bar', component: SnackBarDemo},
7273
{path: 'platform', component: PlatformDemo},
7374
{path: 'style', component: StyleDemo},
74-
{path: 'typography', component: TypographyDemo}
75+
{path: 'typography', component: TypographyDemo},
76+
{path: 'expansion', component: ExpansionDemo},
7577
];
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<h1>Single Expansion Panel</h1>
2+
<md-expansion-panel class="md-expansion-demo-width" #myPanel>
3+
<md-expansion-panel-header>
4+
<mat-panel-description>This is a panel description.</mat-panel-description>
5+
<mat-panel-title>Panel Title</mat-panel-title>
6+
</md-expansion-panel-header>
7+
This is the content text that makes sense here.
8+
<md-action-row>
9+
<button md-button (click)="myPanel.expanded = false">CANCEL</button>
10+
<button md-button>SAVE</button>
11+
</md-action-row>
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+
<md-accordion [displayMode]="displayMode" [multi]="multi"
35+
class="md-expansion-demo-width">
36+
<md-expansion-panel #panel1 [hideToggle]="hideToggle">
37+
<md-expansion-panel-header>Section 1</md-expansion-panel-header>
38+
<p>This is the content text that makes sense here.</p>
39+
</md-expansion-panel>
40+
<md-expansion-panel #panel2 [hideToggle]="hideToggle">
41+
<md-expansion-panel-header>Section 2</md-expansion-panel-header>
42+
<p>This is the content text that makes sense here.</p>
43+
</md-expansion-panel>
44+
<md-expansion-panel #panel3 *ngIf="showPanel3" [hideToggle]="hideToggle">
45+
<md-expansion-panel-header>Section 3</md-expansion-panel-header>
46+
<md-checkbox #showButtons>Reveal Buttons Below</md-checkbox>
47+
<md-action-row *ngIf="showButtons.checked">
48+
<button md-button (click)="panel2.expanded = true">OPEN SECTION 2</button>
49+
<button md-button (click)="panel3.expanded = false">CLOSE</button>
50+
</md-action-row>
51+
</md-expansion-panel>
52+
</md-accordion>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.md-expansion-demo-width {
2+
width: 600px;
3+
display: block;
4+
}
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';
@@ -37,6 +38,7 @@
3738
@include mat-chips-theme($theme);
3839
@include mat-datepicker-theme($theme);
3940
@include mat-dialog-theme($theme);
41+
@include mat-expansion-panel-theme($theme);
4042
@include mat-grid-list-theme($theme);
4143
@include mat-icon-theme($theme);
4244
@include mat-input-theme($theme);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
border-top-color: mat-color($foreground, divider);
15+
}
16+
17+
.mat-expansion-panel-header:focus,
18+
.mat-expansion-panel-header:hover {
19+
background: mat-color($background, hover);
20+
}
21+
.mat-expansion-panel-header-title {
22+
color: mat-color($foreground, text);
23+
}
24+
25+
.mat-expansion-panel-header-description {
26+
color: mat-color($foreground, secondary-text);
27+
}
28+
29+
.mat-expansion-indicator::after {
30+
color: mat-color($foreground, secondary-text);
31+
}
32+
}

src/lib/expansion/accordion-item.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {Output, EventEmitter, Input, Injectable, OnDestroy, Optional} from '@angular/core';
2+
import {UniqueSelectionDispatcher} from '../core';
3+
import {CdkAccordion} from './accordion';
4+
5+
/** Used to generate unique ID for each expansion panel. */
6+
let nextId = 0;
7+
8+
/**
9+
* An abstract class to be extended and decorated as a component. Sets up all
10+
* events and attributes needed to be managed by a CdkAccordion parent.
11+
*/
12+
@Injectable()
13+
export class AccordionItem implements OnDestroy {
14+
/** Event emitted every time the MdAccordianChild is closed. */
15+
@Output() closed = new EventEmitter<void>();
16+
/** Event emitted every time the MdAccordianChild is opened. */
17+
@Output() opened = new EventEmitter<void>();
18+
/** Event emitted when the MdAccordianChild is destroyed. */
19+
@Output() destroyed = new EventEmitter<void>();
20+
/** The unique MdAccordianChild id. */
21+
readonly id = `cdk-accordion-child-${nextId++}`;
22+
/** Whether the MdAccordianChild is expanded. */
23+
@Input() get expanded(): boolean { return this._expanded; }
24+
set expanded(expanded: boolean) {
25+
// Only emit events and update the internal value if the value changes.
26+
if (this._expanded !== expanded) {
27+
this._expanded = expanded;
28+
if (expanded) {
29+
this.opened.emit();
30+
/**
31+
* In the unique selection dispatcher, the id parameter is the id of the CdkAccordonItem,
32+
* the name value is the id of the accordion.
33+
*/
34+
let accordionId = this.accordion ? this.accordion.id : this.id;
35+
this._expansionDispatcher.notify(this.id, accordionId);
36+
} else {
37+
this.closed.emit();
38+
}
39+
}
40+
}
41+
private _expanded: boolean;
42+
43+
constructor(@Optional() public accordion: CdkAccordion,
44+
protected _expansionDispatcher: UniqueSelectionDispatcher) {
45+
_expansionDispatcher.listen((id: string, accordionId: string) => {
46+
if (this.accordion && !this.accordion.multi &&
47+
this.accordion.id === accordionId && this.id !== id) {
48+
this.expanded = false;
49+
}
50+
});
51+
}
52+
53+
/** Emits an event for the accordion item being destroyed. */
54+
ngOnDestroy() {
55+
this.destroyed.emit();
56+
}
57+
58+
/** Toggles the expanded state of the accordion item. */
59+
toggle(): void {
60+
this.expanded = !this.expanded;
61+
}
62+
63+
/** Sets the expanded state of the accordion item to false. */
64+
close(): void {
65+
this.expanded = false;
66+
}
67+
68+
/** Sets the expanded state of the accordion item to true. */
69+
open(): void {
70+
this.expanded = true;
71+
}
72+
}

src/lib/expansion/accordion.spec.ts

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

src/lib/expansion/accordion.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {Directive, Input} from '@angular/core';
2+
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
3+
4+
/** MdAccordion's display modes. */
5+
export type MdAccordionDisplayMode = 'default' | 'flat';
6+
7+
/** Unique ID counter */
8+
let nextId = 0;
9+
10+
/**
11+
* Directive whose purpose is to manage the expanded state of CdkAccordionItem children.
12+
*/
13+
@Directive({
14+
selector: '[cdk-accordion]',
15+
})
16+
export class CdkAccordion {
17+
/** A readonly id value to use for unique selection coordination. */
18+
readonly id = `cdk-accordion-${nextId++}`;
19+
20+
/** Whether the accordion should allow multiple expanded accordion items simulateously. */
21+
@Input() get multi(): boolean { return this._multi; }
22+
set multi(multi: boolean) { this._multi = coerceBooleanProperty(multi); }
23+
private _multi: boolean = false;
24+
25+
/** Whether the expansion indicator should be hidden. */
26+
@Input() get hideToggle(): boolean { return this._hideToggle; }
27+
set hideToggle(show: boolean) { this._hideToggle = coerceBooleanProperty(show); }
28+
private _hideToggle: boolean = false;
29+
30+
/**
31+
* The display mode used for all expansion panels in the accordion. Currently two display
32+
* modes exist:
33+
* default - a gutter-like spacing is placed around any expanded panel, placing the expanded
34+
* panel at a different elevation from the reset of the accordion.
35+
* flat - no spacing is placed around expanded panels, showing all panels at the same
36+
* elevation.
37+
*/
38+
@Input() displayMode: MdAccordionDisplayMode = 'default';
39+
}
40+
41+
/**
42+
* Directive for a Material Design Accordion.
43+
*/
44+
@Directive({
45+
selector: 'mat-accordion, md-accordion',
46+
host: {
47+
class: 'mat-accordion'
48+
}
49+
})
50+
export class MdAccordion extends CdkAccordion {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<span class="mat-content">
2+
<ng-content select="md-panel-title, mat-panel-title"></ng-content>
3+
<ng-content select="md-panel-description, mat-panel-description"></ng-content>
4+
<ng-content></ng-content>
5+
</span>
6+
<span [@indicatorRotate]="_getExpandedState()" *ngIf="!_getHideToggle()"
7+
class="mat-expansion-indicator"></span>

0 commit comments

Comments
 (0)