diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 2cce8f9be446..38ec54b7ce1f 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -41,6 +41,7 @@ import {DataTableDemo} from './data-table/data-table-demo'; import {PeopleDatabase} from './data-table/people-database'; import {DatepickerDemo} from './datepicker/datepicker-demo'; import {TypographyDemo} from './typography/typography-demo'; +import {ExpansionDemo} from './expansion/expansion-demo'; import { CdkDataTableModule, FullscreenOverlayContainer, @@ -53,6 +54,7 @@ import { MdCoreModule, MdDatepickerModule, MdDialogModule, + MdExpansionModule, MdGridListModule, MdIconModule, MdInputModule, @@ -87,6 +89,7 @@ import { MdChipsModule, MdDatepickerModule, MdDialogModule, + MdExpansionModule, MdGridListModule, MdIconModule, MdInputModule, @@ -170,6 +173,7 @@ export class DemoMaterialModule {} FoggyTabContent, PlatformDemo, TypographyDemo, + ExpansionDemo, ], providers: [ {provide: OverlayContainer, useClass: FullscreenOverlayContainer}, diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts index 57be232775fe..ff358a37abb3 100644 --- a/src/demo-app/demo-app/demo-app.ts +++ b/src/demo-app/demo-app/demo-app.ts @@ -43,6 +43,7 @@ export class DemoApp { {name: 'Data Table', route: 'data-table'}, {name: 'Datepicker', route: 'datepicker'}, {name: 'Dialog', route: 'dialog'}, + {name: 'Expansion Panel', route: 'expansion'}, {name: 'Gestures', route: 'gestures'}, {name: 'Grid List', route: 'grid-list'}, {name: 'Icon', route: 'icon'}, diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts index a23ab93b5561..beca55e41423 100644 --- a/src/demo-app/demo-app/routes.ts +++ b/src/demo-app/demo-app/routes.ts @@ -35,6 +35,7 @@ import {StyleDemo} from '../style/style-demo'; import {DatepickerDemo} from '../datepicker/datepicker-demo'; import {DataTableDemo} from '../data-table/data-table-demo'; import {TypographyDemo} from '../typography/typography-demo'; +import {ExpansionDemo} from '../expansion/expansion-demo'; export const DEMO_APP_ROUTES: Routes = [ {path: '', component: Home}, @@ -71,5 +72,6 @@ export const DEMO_APP_ROUTES: Routes = [ {path: 'snack-bar', component: SnackBarDemo}, {path: 'platform', component: PlatformDemo}, {path: 'style', component: StyleDemo}, - {path: 'typography', component: TypographyDemo} + {path: 'typography', component: TypographyDemo}, + {path: 'expansion', component: ExpansionDemo}, ]; diff --git a/src/demo-app/expansion/expansion-demo.html b/src/demo-app/expansion/expansion-demo.html new file mode 100644 index 000000000000..fe2fa05517de --- /dev/null +++ b/src/demo-app/expansion/expansion-demo.html @@ -0,0 +1,52 @@ +

Single Expansion Panel

+ + + This is a panel description. + Panel Title + + This is the content text that makes sense here. + + + + + +
+

Accordion

+
+

Accordion Options

+
+ Allow Multi Expansion + Hide Indicators + Show Panel 3 +
+

Accordion Style

+ + Default + Flat + +

Accordion Panel(s)

+
+ Panel 1 + Panel 2 +
+
+
+ + + Section 1 +

This is the content text that makes sense here.

+
+ + Section 2 +

This is the content text that makes sense here.

+
+ + Section 3 + Reveal Buttons Below + + + + + +
\ No newline at end of file diff --git a/src/demo-app/expansion/expansion-demo.scss b/src/demo-app/expansion/expansion-demo.scss new file mode 100644 index 000000000000..20e4bbd80427 --- /dev/null +++ b/src/demo-app/expansion/expansion-demo.scss @@ -0,0 +1,4 @@ +.md-expansion-demo-width { + width: 600px; + display: block; +} \ No newline at end of file diff --git a/src/demo-app/expansion/expansion-demo.ts b/src/demo-app/expansion/expansion-demo.ts new file mode 100644 index 000000000000..170ebc3c441c --- /dev/null +++ b/src/demo-app/expansion/expansion-demo.ts @@ -0,0 +1,15 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'expansion-demo', + styleUrls: ['expansion-demo.css'], + templateUrl: 'expansion-demo.html', + encapsulation: ViewEncapsulation.None, +}) +export class ExpansionDemo { + displayMode: string = 'default'; + multi: boolean = false; + hideToggle: boolean = false; + showPanel3 = true; +} diff --git a/src/lib/core/theming/_all-theme.scss b/src/lib/core/theming/_all-theme.scss index 374225788931..bfd0523981b2 100644 --- a/src/lib/core/theming/_all-theme.scss +++ b/src/lib/core/theming/_all-theme.scss @@ -8,6 +8,7 @@ @import '../../chips/chips-theme'; @import '../../datepicker/datepicker-theme'; @import '../../dialog/dialog-theme'; +@import '../../expansion/expansion-theme'; @import '../../grid-list/grid-list-theme'; @import '../../icon/icon-theme'; @import '../../input/input-theme'; @@ -37,6 +38,7 @@ @include mat-chips-theme($theme); @include mat-datepicker-theme($theme); @include mat-dialog-theme($theme); + @include mat-expansion-panel-theme($theme); @include mat-grid-list-theme($theme); @include mat-icon-theme($theme); @include mat-input-theme($theme); diff --git a/src/lib/expansion/_expansion-theme.scss b/src/lib/expansion/_expansion-theme.scss new file mode 100644 index 000000000000..2ed9552afdc4 --- /dev/null +++ b/src/lib/expansion/_expansion-theme.scss @@ -0,0 +1,32 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; + +@mixin mat-expansion-panel-theme($theme) { + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .mat-expansion-panel { + background: mat-color($background, card); + color: mat-color($foreground, base); + } + + .mat-action-row { + border-top-color: mat-color($foreground, divider); + } + + .mat-expansion-panel-header:focus, + .mat-expansion-panel-header:hover { + background: mat-color($background, hover); + } + .mat-expansion-panel-header-title { + color: mat-color($foreground, text); + } + + .mat-expansion-panel-header-description { + color: mat-color($foreground, secondary-text); + } + + .mat-expansion-indicator::after { + color: mat-color($foreground, secondary-text); + } +} \ No newline at end of file diff --git a/src/lib/expansion/accordion-item.ts b/src/lib/expansion/accordion-item.ts new file mode 100644 index 000000000000..95c57ee74126 --- /dev/null +++ b/src/lib/expansion/accordion-item.ts @@ -0,0 +1,72 @@ +import {Output, EventEmitter, Input, Injectable, OnDestroy, Optional} from '@angular/core'; +import {UniqueSelectionDispatcher} from '../core'; +import {CdkAccordion} from './accordion'; + +/** Used to generate unique ID for each expansion panel. */ +let nextId = 0; + +/** + * An abstract class to be extended and decorated as a component. Sets up all + * events and attributes needed to be managed by a CdkAccordion parent. + */ +@Injectable() +export class AccordionItem implements OnDestroy { + /** Event emitted every time the MdAccordianChild is closed. */ + @Output() closed = new EventEmitter(); + /** Event emitted every time the MdAccordianChild is opened. */ + @Output() opened = new EventEmitter(); + /** Event emitted when the MdAccordianChild is destroyed. */ + @Output() destroyed = new EventEmitter(); + /** The unique MdAccordianChild id. */ + readonly id = `cdk-accordion-child-${nextId++}`; + /** Whether the MdAccordianChild is expanded. */ + @Input() get expanded(): boolean { return this._expanded; } + set expanded(expanded: boolean) { + // Only emit events and update the internal value if the value changes. + if (this._expanded !== expanded) { + this._expanded = expanded; + if (expanded) { + this.opened.emit(); + /** + * In the unique selection dispatcher, the id parameter is the id of the CdkAccordonItem, + * the name value is the id of the accordion. + */ + let accordionId = this.accordion ? this.accordion.id : this.id; + this._expansionDispatcher.notify(this.id, accordionId); + } else { + this.closed.emit(); + } + } + } + private _expanded: boolean; + + constructor(@Optional() public accordion: CdkAccordion, + protected _expansionDispatcher: UniqueSelectionDispatcher) { + _expansionDispatcher.listen((id: string, accordionId: string) => { + if (this.accordion && !this.accordion.multi && + this.accordion.id === accordionId && this.id !== id) { + this.expanded = false; + } + }); + } + + /** Emits an event for the accordion item being destroyed. */ + ngOnDestroy() { + this.destroyed.emit(); + } + + /** Toggles the expanded state of the accordion item. */ + toggle(): void { + this.expanded = !this.expanded; + } + + /** Sets the expanded state of the accordion item to false. */ + close(): void { + this.expanded = false; + } + + /** Sets the expanded state of the accordion item to true. */ + open(): void { + this.expanded = true; + } +} diff --git a/src/lib/expansion/accordion.spec.ts b/src/lib/expansion/accordion.spec.ts new file mode 100644 index 000000000000..09e198eac768 --- /dev/null +++ b/src/lib/expansion/accordion.spec.ts @@ -0,0 +1,66 @@ +import {async, TestBed} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MdExpansionModule} from './index'; + + +describe('CdkAccordion', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + MdExpansionModule + ], + declarations: [ + SetOfItems + ], + }); + TestBed.compileComponents(); + })); + + it('should ensure only one item is expanded at a time', () => { + let fixture = TestBed.createComponent(SetOfItems); + let items = fixture.debugElement.queryAll(By.css('.mat-expansion-panel')); + + fixture.componentInstance.firstPanelExpanded = true; + fixture.detectChanges(); + expect(items[0].classes['mat-expanded']).toBeTruthy(); + expect(items[1].classes['mat-expanded']).toBeFalsy(); + + fixture.componentInstance.secondPanelExpanded = true; + fixture.detectChanges(); + expect(items[0].classes['mat-expanded']).toBeFalsy(); + expect(items[1].classes['mat-expanded']).toBeTruthy(); + }); + + it('should allow multiple items to be expanded simultaneously', () => { + let fixture = TestBed.createComponent(SetOfItems); + let panels = fixture.debugElement.queryAll(By.css('.mat-expansion-panel')); + + fixture.componentInstance.multi = true; + fixture.componentInstance.firstPanelExpanded = true; + fixture.componentInstance.secondPanelExpanded = true; + fixture.detectChanges(); + expect(panels[0].classes['mat-expanded']).toBeTruthy(); + expect(panels[1].classes['mat-expanded']).toBeTruthy(); + }); +}); + + +@Component({template: ` + + + Summary +

Content

+
+ + Summary +

Content

+
+
`}) +class SetOfItems { + multi: boolean = false; + firstPanelExpanded: boolean = false; + secondPanelExpanded: boolean = false; +} diff --git a/src/lib/expansion/accordion.ts b/src/lib/expansion/accordion.ts new file mode 100644 index 000000000000..804887d3772f --- /dev/null +++ b/src/lib/expansion/accordion.ts @@ -0,0 +1,50 @@ +import {Directive, Input} from '@angular/core'; +import {coerceBooleanProperty} from '../core/coercion/boolean-property'; + +/** MdAccordion's display modes. */ +export type MdAccordionDisplayMode = 'default' | 'flat'; + +/** Unique ID counter */ +let nextId = 0; + +/** + * Directive whose purpose is to manage the expanded state of CdkAccordionItem children. + */ +@Directive({ + selector: '[cdk-accordion]', +}) +export class CdkAccordion { + /** A readonly id value to use for unique selection coordination. */ + readonly id = `cdk-accordion-${nextId++}`; + + /** Whether the accordion should allow multiple expanded accordion items simulateously. */ + @Input() get multi(): boolean { return this._multi; } + set multi(multi: boolean) { this._multi = coerceBooleanProperty(multi); } + private _multi: boolean = false; + + /** Whether the expansion indicator should be hidden. */ + @Input() get hideToggle(): boolean { return this._hideToggle; } + set hideToggle(show: boolean) { this._hideToggle = coerceBooleanProperty(show); } + private _hideToggle: boolean = false; + + /** + * The display mode used for all expansion panels in the accordion. Currently two display + * modes exist: + * default - a gutter-like spacing is placed around any expanded panel, placing the expanded + * panel at a different elevation from the reset of the accordion. + * flat - no spacing is placed around expanded panels, showing all panels at the same + * elevation. + */ + @Input() displayMode: MdAccordionDisplayMode = 'default'; +} + +/** + * Directive for a Material Design Accordion. + */ +@Directive({ + selector: 'mat-accordion, md-accordion', + host: { + class: 'mat-accordion' + } +}) +export class MdAccordion extends CdkAccordion {} diff --git a/src/lib/expansion/expansion-panel-header.html b/src/lib/expansion/expansion-panel-header.html new file mode 100644 index 000000000000..564a957bf803 --- /dev/null +++ b/src/lib/expansion/expansion-panel-header.html @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/lib/expansion/expansion-panel-header.scss b/src/lib/expansion/expansion-panel-header.scss new file mode 100644 index 000000000000..d88885d34396 --- /dev/null +++ b/src/lib/expansion/expansion-panel-header.scss @@ -0,0 +1,61 @@ +$mat-expansion-panel-header-height: 48px; +$mat-expansion-panel-header-height-expanded: 64px; + +.mat-expansion-panel-header { + cursor: pointer; + display: flex; + flex-direction: row; + height: $mat-expansion-panel-header-height; + line-height: $mat-expansion-panel-header-height; + padding: 0 24px; + + &.mat-expanded { + height: $mat-expansion-panel-header-height-expanded; + line-height: $mat-expansion-panel-header-height-expanded; + } + + &:focus, + &:hover { + outline: none; + } + + &.mat-expanded:focus, + &.mat-expanded:hover, { + background: inherit; + } +} + +.mat-content { + display: flex; + flex: 1; + flex-direction: row; + overflow: hidden; +} + +.mat-expansion-panel-header-title { + display: flex; + flex-grow: 1; + font-size: 15px; + margin-right: 16px; +} + +.mat-expansion-panel-header-description { + display: flex; + flex-grow: 2; + font-size: 15px; + margin-right: 16px; +} + +/** + * Creates the expansion indicator arrow. Done using ::after rather than having + * additional nodes in the template. + */ +.mat-expansion-indicator::after { + border-style: solid; + border-width: 0 2px 2px 0; + content: ''; + display: inline-block; + padding: 3px; + transform: rotate(45deg); + vertical-align: middle; +} diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts new file mode 100644 index 000000000000..f3f631cb1038 --- /dev/null +++ b/src/lib/expansion/expansion-panel-header.ts @@ -0,0 +1,122 @@ +import { + Component, + Directive, + Host, + ViewEncapsulation, +} from '@angular/core'; +import { + trigger, + state, + style, + transition, + animate, +} from '@angular/animations'; +import {SPACE, ENTER} from '../core/keyboard/keycodes'; +import {MdExpansionPanel, EXPANSION_PANEL_ANIMATION_TIMING} from './expansion-panel'; + + +/** + * component. + * + * This component corresponds to the header element of an . + * + * Please refer to README.md for examples on how to use it. + */ +@Component({ + moduleId: module.id, + selector: 'md-expansion-panel-header, mat-expansion-panel-header', + styleUrls: ['./expansion-panel-header.css'], + templateUrl: './expansion-panel-header.html', + encapsulation: ViewEncapsulation.None, + host: { + 'class': 'mat-expansion-panel-header', + 'role': 'button', + 'tabindex': '0', + '[attr.aria-controls]': '_getPanelId()', + '[attr.aria-expanded]': '_isExpanded()', + '[class.mat-expanded]': '_isExpanded()', + '(click)': '_toggle()', + '(keyup)': '_keyup($event)', + '[@expansionHeight]': '_getExpandedState()', + }, + animations: [ + trigger('indicatorRotate', [ + state('collapsed', style({transform: 'rotate(0deg)'})), + state('expanded', style({transform: 'rotate(180deg)'})), + transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)), + ]), + trigger('expansionHeight', [ + state('collapsed', style({height: '48px', 'line-height': '48px'})), + state('expanded', style({height: '64px', 'line-height': '68px'})), + transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)), + ]), + ], +}) +export class MdExpansionPanelHeader { + constructor(@Host() public panel: MdExpansionPanel) {} + + /** Toggles the expanded state of the panel. */ + _toggle(event?: KeyboardEvent): void { + this.panel.toggle(); + } + + /** Gets whether the panel is expanded. */ + _isExpanded(): boolean { + return this.panel.expanded; + } + + /** Gets the expanded state string of the panel. */ + _getExpandedState(): string { + return this.panel._getExpandedState(); + } + + /** Gets the panel id. */ + _getPanelId(): string { + return this.panel.id; + } + + /** Gets whether the expand indicator is hidden. */ + _getHideToggle(): boolean { + return this.panel.hideToggle; + } + + /** Handle keyup event calling to toggle() if appropriate. */ + _keyup(event: KeyboardEvent) { + switch (event.keyCode) { + // Toggle for space and enter keys. + case SPACE: + case ENTER: + event.preventDefault(); + this._toggle(); + break; + default: + return; + } + } +} + +/** + * directive. + * + * This direction is to be used inside of the MdExpansionPanelHeader component. + */ +@Directive({ + selector: 'md-panel-description, mat-panel-description', + host : { + class: 'mat-expansion-panel-header-description' + } +}) +export class MdExpansionPanelDescription {} + +/** + * directive. + * + * This direction is to be used inside of the MdExpansionPanelHeader component. + */ +@Directive({ + selector: 'md-panel-title, mat-panel-title', + host : { + class: 'mat-expansion-panel-header-title' + } +}) +export class MdExpansionPanelTitle {} diff --git a/src/lib/expansion/expansion-panel.html b/src/lib/expansion/expansion-panel.html new file mode 100644 index 000000000000..25422ebc6f70 --- /dev/null +++ b/src/lib/expansion/expansion-panel.html @@ -0,0 +1,8 @@ + +
+
+ +
+ +
\ No newline at end of file diff --git a/src/lib/expansion/expansion-panel.scss b/src/lib/expansion/expansion-panel.scss new file mode 100644 index 000000000000..0a258ad43037 --- /dev/null +++ b/src/lib/expansion/expansion-panel.scss @@ -0,0 +1,33 @@ +@import '../core/style/variables'; +@import '../core/style/elevation'; + +.mat-expansion-panel { + @include mat-elevation-transition; + box-sizing: content-box; + display: block; + + &:not([class*='mat-elevation-z']) { + @include mat-elevation(2); + } +} + +.mat-expansion-panel-content { + overflow: hidden; +} + +.mat-expansion-panel-body { + padding: 0 24px 16px; +} + +.mat-action-row { + border-top-style: solid; + border-top-width: 1px; + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 16px 8px 16px 24px; + + button.mat-button { + margin-left: 8px; + } +} \ No newline at end of file diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts new file mode 100644 index 000000000000..7c7c960c8a48 --- /dev/null +++ b/src/lib/expansion/expansion-panel.ts @@ -0,0 +1,106 @@ +import { + Component, + Directive, + Host, + Input, + ViewEncapsulation, + Optional, + forwardRef, +} from '@angular/core'; +import { + trigger, + state, + style, + transition, + animate, +} from '@angular/animations'; +import {MdAccordion, MdAccordionDisplayMode} from './accordion'; +import {AccordionItem} from './accordion-item'; +import {UniqueSelectionDispatcher} from '../core'; + + +/** MdExpansionPanel's states. */ +export type MdExpansionPanelState = 'expanded' | 'collapsed'; + +/** Time and timing curve for expansion panel animations. */ +export const EXPANSION_PANEL_ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,1)'; + +/** + * component. + * + * This component can be used as a single element to show expandable content, or as one of + * multiple children of an element with the CdkAccordion directive attached. + * + * Please refer to README.md for examples on how to use it. + */ +@Component({ + moduleId: module.id, + styleUrls: ['./expansion-panel.css'], + selector: 'md-expansion-panel, mat-expansion-panel', + templateUrl: './expansion-panel.html', + encapsulation: ViewEncapsulation.None, + host: { + 'class': 'mat-expansion-panel', + '[class.mat-expanded]': 'expanded', + '[@displayMode]': '_getDisplayMode()', + }, + providers: [ + {provide: AccordionItem, useExisting: forwardRef(() => MdExpansionPanel)} + ], + animations: [ + trigger('bodyExpansion', [ + state('collapsed', style({height: '0px'})), + state('expanded', style({height: '*'})), + transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)), + ]), + trigger('displayMode', [ + state('collapsed', style({margin: '0'})), + state('default', style({margin: '16px 0'})), + state('flat', style({margin: '0'})), + transition('flat <=> collapsed, default <=> collapsed, flat <=> default', + animate(EXPANSION_PANEL_ANIMATION_TIMING)), + ]), + ], +}) +export class MdExpansionPanel extends AccordionItem { + /** Whether the toggle indicator should be hidden. */ + @Input() hideToggle: boolean = false; + + constructor(@Optional() @Host() accordion: MdAccordion, + _uniqueSelectionDispatcher: UniqueSelectionDispatcher) { + super(accordion, _uniqueSelectionDispatcher); + this.accordion = accordion; + } + + /** Whether the expansion indicator should be hidden. */ + _getHideToggle(): boolean { + if (this.accordion) { + return this.accordion.hideToggle; + } + return this.hideToggle; + } + + /** Gets the panel's display mode. */ + _getDisplayMode(): MdAccordionDisplayMode | MdExpansionPanelState { + if (!this.expanded) { + return this._getExpandedState(); + } + if (this.accordion) { + return this.accordion.displayMode; + } + return this._getExpandedState(); + } + + /** Gets the expanded state string. */ + _getExpandedState(): MdExpansionPanelState { + return this.expanded ? 'expanded' : 'collapsed'; + } +} + +@Directive({ + selector: 'mat-action-row, md-action-row', + host: { + class: 'mat-action-row' + } +}) +export class MdExpansionPanelActionRow {} diff --git a/src/lib/expansion/expansion.spec.ts b/src/lib/expansion/expansion.spec.ts new file mode 100644 index 000000000000..e6f53a7d6437 --- /dev/null +++ b/src/lib/expansion/expansion.spec.ts @@ -0,0 +1,73 @@ +import {async, TestBed} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MdExpansionModule} from './index'; + + +describe('MdExpansionPanel', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + MdExpansionModule + ], + declarations: [ + PanelWithContent + ], + }); + TestBed.compileComponents(); + })); + + it('should expanded and collapse the panel', () => { + let fixture = TestBed.createComponent(PanelWithContent); + let contentEl = fixture.debugElement.query(By.css('.mat-expansion-panel-content')); + let headerEl = fixture.debugElement.query(By.css('.mat-expansion-panel-header')); + fixture.detectChanges(); + expect(headerEl.classes['mat-expanded']).toBeFalsy(); + expect(contentEl.classes['mat-expanded']).toBeFalsy(); + + fixture.componentInstance.expanded = true; + fixture.detectChanges(); + expect(headerEl.classes['mat-expanded']).toBeTruthy(); + expect(contentEl.classes['mat-expanded']).toBeTruthy(); + }); + + it('emit correct events for change in panel expanded state', () => { + let fixture = TestBed.createComponent(PanelWithContent); + fixture.componentInstance.expanded = true; + fixture.detectChanges(); + expect(fixture.componentInstance.openCallback).toHaveBeenCalled(); + + fixture.componentInstance.expanded = false; + fixture.detectChanges(); + expect(fixture.componentInstance.closeCallback).toHaveBeenCalled(); + }); + + it('creates a unique panel id for each panel', () => { + let fixtureOne = TestBed.createComponent(PanelWithContent); + let headerElOne = fixtureOne.nativeElement.querySelector('.mat-expansion-panel-header'); + let fixtureTwo = TestBed.createComponent(PanelWithContent); + let headerElTwo = fixtureTwo.nativeElement.querySelector('.mat-expansion-panel-header'); + fixtureOne.detectChanges(); + fixtureTwo.detectChanges(); + + let panelIdOne = headerElOne.getAttribute('aria-controls'); + let panelIdTwo = headerElTwo.getAttribute('aria-controls'); + expect(panelIdOne).not.toBe(panelIdTwo); + }); +}); + + +@Component({template: ` + + Panel Title +

Some content

+
`}) +class PanelWithContent { + expanded: boolean = false; + openCallback = jasmine.createSpy('openCallback'); + closeCallback = jasmine.createSpy('closeCallback'); +} diff --git a/src/lib/expansion/index.ts b/src/lib/expansion/index.ts new file mode 100644 index 000000000000..9dbeb268a2e1 --- /dev/null +++ b/src/lib/expansion/index.ts @@ -0,0 +1,57 @@ +import {NgModule, ModuleWithProviders} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CompatibilityModule, UNIQUE_SELECTION_DISPATCHER_PROVIDER} from '../core'; +import { + MdExpansionPanelHeader, + MdExpansionPanelDescription, + MdExpansionPanelTitle +} from './expansion-panel-header'; +import { + MdExpansionPanel, + MdExpansionPanelActionRow, +} from './expansion-panel'; +import { + CdkAccordion, + MdAccordion, +} from './accordion'; + +@NgModule({ + imports: [CompatibilityModule, CommonModule], + exports: [ + CdkAccordion, + MdAccordion, + MdExpansionPanel, + MdExpansionPanelActionRow, + MdExpansionPanelHeader, + MdExpansionPanelTitle, + MdExpansionPanelDescription + ], + declarations: [ + CdkAccordion, + MdAccordion, + MdExpansionPanel, + MdExpansionPanelActionRow, + MdExpansionPanelHeader, + MdExpansionPanelTitle, + MdExpansionPanelDescription + ], + providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER] +}) +export class MdExpansionModule {} + +export { + CdkAccordion, + MdAccordion, + MdAccordionDisplayMode +} from './accordion'; +export {AccordionItem} from './accordion-item'; +export { + MdExpansionPanel, + MdExpansionPanelState, + MdExpansionPanelActionRow +} from './expansion-panel'; +export { + MdExpansionPanelHeader, + MdExpansionPanelDescription, + MdExpansionPanelTitle +} from './expansion-panel-header'; diff --git a/src/lib/module.ts b/src/lib/module.ts index 255ba9b5d200..2041b0d2c7e5 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -37,6 +37,7 @@ import {MdAutocompleteModule} from './autocomplete/index'; import {StyleModule} from './core/style/index'; import {MdDatepickerModule} from './datepicker/index'; import {CdkDataTableModule} from './core/data-table/index'; +import {MdExpansionModule} from './expansion/index'; const MATERIAL_MODULES = [ MdAutocompleteModule, @@ -47,6 +48,7 @@ const MATERIAL_MODULES = [ MdCheckboxModule, MdDatepickerModule, MdDialogModule, + MdExpansionModule, MdGridListModule, MdIconModule, MdInputModule, diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts index 7eeeb82012d1..1a7a77478014 100644 --- a/src/lib/public_api.ts +++ b/src/lib/public_api.ts @@ -16,6 +16,7 @@ export * from './checkbox/index'; export * from './core/data-table/index'; export * from './datepicker/index'; export * from './dialog/index'; +export * from './expansion/index'; export * from './grid-list/index'; export * from './icon/index'; export * from './input/index'; diff --git a/tools/gulp/packaging/rollup-helpers.ts b/tools/gulp/packaging/rollup-helpers.ts index cbed6e3fc58f..15e37c06d9da 100644 --- a/tools/gulp/packaging/rollup-helpers.ts +++ b/tools/gulp/packaging/rollup-helpers.ts @@ -24,6 +24,10 @@ const ROLLUP_GLOBALS = { '@angular/cdk': 'ng.cdk', // Rxjs dependencies + 'rxjs/BehaviorSubject': 'Rx', + 'rxjs/Observable': 'Rx', + 'rxjs/Subject': 'Rx', + 'rxjs/Subscription': 'Rx', 'rxjs/add/observable/combineLatest': 'Rx.Observable', 'rxjs/add/observable/forkJoin': 'Rx.Observable', 'rxjs/add/observable/fromEvent': 'Rx.Observable', @@ -44,11 +48,6 @@ const ROLLUP_GLOBALS = { 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype', 'rxjs/add/operator/takeUntil': 'Rx.Observable.prototype', 'rxjs/add/operator/toPromise': 'Rx.Observable.prototype', - 'rxjs/BehaviorSubject': 'Rx', - 'rxjs/Observable': 'Rx', - 'rxjs/Subject': 'Rx', - 'rxjs/Subscription': 'Rx', - }; export type BundleConfig = {