diff --git a/src/demo-app/expansion/expansion-demo.html b/src/demo-app/expansion/expansion-demo.html index 29c4cecc9953..22948b27da8e 100644 --- a/src/demo-app/expansion/expansion-demo.html +++ b/src/demo-app/expansion/expansion-demo.html @@ -43,6 +43,11 @@

matAccordion

Default Flat +

Toggle Position

+ + Before + After +

Accordion Actions ('Multi Expansion' mode only)

@@ -55,8 +60,8 @@

matAccordion


- + Section 1

This is the content text that makes sense here.

@@ -75,7 +80,7 @@

matAccordion

-

cdkAccordion

+

CdkAccordion

Accordion Options

@@ -108,4 +113,3 @@

cdkAccordion

I only show if item 3 is expanded

- diff --git a/src/demo-app/expansion/expansion-demo.ts b/src/demo-app/expansion/expansion-demo.ts index 565ab4b1d534..6f4575c307a2 100644 --- a/src/demo-app/expansion/expansion-demo.ts +++ b/src/demo-app/expansion/expansion-demo.ts @@ -20,6 +20,7 @@ export class ExpansionDemo { @ViewChild(MatAccordion) accordion: MatAccordion; displayMode = 'default'; + togglePosition = 'after'; multi = false; hideToggle = false; disabled = false; diff --git a/src/lib/expansion/_expansion-theme.scss b/src/lib/expansion/_expansion-theme.scss index 0a0a713444bf..75600cfafd43 100644 --- a/src/lib/expansion/_expansion-theme.scss +++ b/src/lib/expansion/_expansion-theme.scss @@ -30,7 +30,7 @@ } .mat-expansion-panel-header-description, - .mat-expansion-indicator::after { + .mat-expansion-indicator { color: mat-color($foreground, secondary-text); } diff --git a/src/lib/expansion/accordion.spec.ts b/src/lib/expansion/accordion.spec.ts index 6c12e2db42a4..83b6f82542ce 100644 --- a/src/lib/expansion/accordion.spec.ts +++ b/src/lib/expansion/accordion.spec.ts @@ -5,7 +5,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {MatExpansionModule, MatAccordion} from './index'; -describe('CdkAccordion', () => { +describe('MatAccordion', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -83,12 +83,39 @@ describe('CdkAccordion', () => { fixture.detectChanges(); expect(panels[0].classes['mat-expanded']).toBeFalsy(); expect(panels[1].classes['mat-expanded']).toBeFalsy(); + + }); + + it('should correctly apply the displayMode provided', () => { + const fixture = TestBed.createComponent(SetOfItems); + const firstPanel = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'))[0]; + + fixture.componentInstance.firstPanelExpanded = true; + fixture.detectChanges(); + expect(firstPanel.classes['mat-expansion-panel-spacing']).toBeTruthy(); + + fixture.componentInstance.displayMode = 'flat'; + fixture.detectChanges(); + expect(firstPanel.classes['mat-expansion-panel-spacing']).toBeFalsy(); + }); + + it('should correctly apply the togglePosition provided', () => { + const fixture = TestBed.createComponent(SetOfItems); + fixture.detectChanges(); + const firstPanelHeader = + fixture.debugElement.queryAll(By.css('.mat-expansion-indicator-container'))[0]; + + expect(firstPanelHeader.classes['mat-expansion-indicator-container-before']).toBeFalsy(); + + fixture.componentInstance.togglePosition = 'before'; + fixture.detectChanges(); + expect(firstPanelHeader.classes['mat-expansion-indicator-container-before']).toBeTruthy(); }); }); @Component({template: ` - + Summary

Content

@@ -102,6 +129,8 @@ class SetOfItems { @ViewChild(MatAccordion) accordion: MatAccordion; multi: boolean = false; + displayMode = 'default'; + togglePosition = 'after'; firstPanelExpanded: boolean = false; secondPanelExpanded: boolean = false; secondPanelDisabled: boolean = false; diff --git a/src/lib/expansion/accordion.ts b/src/lib/expansion/accordion.ts index 962179cd751d..660e858b53b5 100644 --- a/src/lib/expansion/accordion.ts +++ b/src/lib/expansion/accordion.ts @@ -6,13 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Input} from '@angular/core'; +import {Directive, Input, SimpleChanges} from '@angular/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {CdkAccordion} from '@angular/cdk/accordion'; +import {Subject} from 'rxjs'; /** MatAccordion's display modes. */ export type MatAccordionDisplayMode = 'default' | 'flat'; + + /** MatAccordion's toggle positions. */ + export type MatAccordionTogglePosition = 'before' | 'after'; + /** * Directive for a Material Design Accordion. */ @@ -24,6 +29,9 @@ export type MatAccordionDisplayMode = 'default' | 'flat'; } }) export class MatAccordion extends CdkAccordion { + /** Stream that emits for changes in `@Input` properties. */ + _inputChanges = new Subject(); + /** Whether the expansion indicator should be hidden. */ @Input() get hideToggle(): boolean { return this._hideToggle; } @@ -39,4 +47,15 @@ export class MatAccordion extends CdkAccordion { * elevation. */ @Input() displayMode: MatAccordionDisplayMode = 'default'; + + /** The positioning of the expansion indicator. */ + @Input() togglePosition: MatAccordionTogglePosition = 'after'; + + ngOnChanges(changes: SimpleChanges) { + this._inputChanges.next(changes); + } + + ngOnDestroy() { + this._inputChanges.complete(); + } } diff --git a/src/lib/expansion/expansion-animations.ts b/src/lib/expansion/expansion-animations.ts index 47e243f9d1ee..50768ebbcf17 100644 --- a/src/lib/expansion/expansion-animations.ts +++ b/src/lib/expansion/expansion-animations.ts @@ -28,8 +28,8 @@ export const matExpansionAnimations: { } = { /** Animation that rotates the indicator arrow. */ indicatorRotate: trigger('indicatorRotate', [ - state('collapsed', style({transform: 'rotate(0deg)'})), - state('expanded', style({transform: 'rotate(180deg)'})), + state('collapsed', style({transform: 'rotate(45deg)'})), + state('expanded', style({transform: 'rotate(225deg)'})), transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)), ]), diff --git a/src/lib/expansion/expansion-panel-header.html b/src/lib/expansion/expansion-panel-header.html index 41c329965859..ea01eccccc9f 100644 --- a/src/lib/expansion/expansion-panel-header.html +++ b/src/lib/expansion/expansion-panel-header.html @@ -3,5 +3,8 @@ - +
+ +
diff --git a/src/lib/expansion/expansion-panel-header.scss b/src/lib/expansion/expansion-panel-header.scss index 73a6638a44be..4ba5737723fe 100644 --- a/src/lib/expansion/expansion-panel-header.scss +++ b/src/lib/expansion/expansion-panel-header.scss @@ -1,9 +1,8 @@ - .mat-expansion-panel-header { display: flex; flex-direction: row; align-items: center; - padding: 0 24px; + padding: 0 16px; &:focus, &:hover { @@ -25,12 +24,13 @@ flex: 1; flex-direction: row; overflow: hidden; + padding-left: 8px; } .mat-expansion-panel-header-title, .mat-expansion-panel-header-description { display: flex; - flex-grow: 1; + flex: 1; margin-right: 16px; [dir='rtl'] & { @@ -40,19 +40,37 @@ } .mat-expansion-panel-header-description { - flex-grow: 2; + flex: 2; +} + +.mat-expansion-indicator-container { + // A margin is required to offset the entire expansion indicator against the space the arrow + // takes up. It is calculated as sqrt(2 * border-width ^ 2) / 2. + margin-bottom: 1.41px; + padding: 0 8px 0 0; + width: 8px; + order: 1; + + &.mat-expansion-indicator-container-before { + order: -1; + padding: 0 8px; + } } -/** - * Creates the expansion indicator arrow. Done using ::after rather than having - * additional nodes in the template. - */ -.mat-expansion-indicator::after { +.mat-expansion-indicator { border-style: solid; border-width: 0 2px 2px 0; - content: ''; - display: inline-block; - padding: 3px; + display: block; + height: 6px; transform: rotate(45deg); - vertical-align: middle; + // The transform origin is set by determining the center pointer of the arrow created. It is + // calculated as by calculating the length of the line between the top left corner of the div, + // and the centroid of the triangle created in the bottom right half of the div. This centroid + // is calculated for both X and Y (as the indicator is a square) as + // (indicator-width + indicator-height + 0) / 3 + // The length between the resulting coordinates and the top left (0, 0) of the div are + // calculated sqrt((centroids-x-coord ^ 2) + (centroids-y-coord ^ 2)) + // This value is used to transform the origin on both the X and Y axes. + transform-origin: 5.65px 5.65px; + width: 6px; } diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index 956a1e08c31f..7787c472e2e2 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -16,6 +16,7 @@ import { ElementRef, Host, Input, + Optional, OnDestroy, ViewEncapsulation, } from '@angular/core'; @@ -23,6 +24,7 @@ import {merge, Subscription} from 'rxjs'; import {filter} from 'rxjs/operators'; import {matExpansionAnimations} from './expansion-animations'; import {MatExpansionPanel} from './expansion-panel'; +import {MatAccordion} from './accordion'; /** @@ -65,17 +67,24 @@ export class MatExpansionPanelHeader implements OnDestroy { private _parentChangeSubscription = Subscription.EMPTY; constructor( + @Optional() accordion: MatAccordion, @Host() public panel: MatExpansionPanel, private _element: ElementRef, private _focusMonitor: FocusMonitor, private _changeDetectorRef: ChangeDetectorRef) { + let changeStreams = [panel._inputChanges]; + if (accordion) { + changeStreams.push(accordion._inputChanges); + } + // Since the toggle state depends on an @Input on the panel, we - // need to subscribe and trigger change detection manually. + // need to subscribe and trigger change detection manually. this._parentChangeSubscription = merge( panel.opened, panel.closed, - panel._inputChanges.pipe(filter(changes => !!(changes.hideToggle || changes.disabled))) + merge(...changeStreams).pipe( + filter(changes => !!(changes.hideToggle || changes.disabled || changes.togglePosition))) ) .subscribe(() => this._changeDetectorRef.markForCheck()); @@ -109,10 +118,15 @@ export class MatExpansionPanelHeader implements OnDestroy { } /** Gets whether the expand indicator should be shown. */ - _showToggle(): boolean { + _isToggleVisible(): boolean { return !this.panel.hideToggle && !this.panel.disabled; } + /** Whether the expand indicator should be shown before the header content */ + _placeToggleBefore(): boolean { + return this.panel.togglePosition === 'before'; + } + /** Handle keydown event calling to toggle() if appropriate. */ _keydown(event: KeyboardEvent) { switch (event.keyCode) { diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts index 63846745a44a..0197f53ca50d 100644 --- a/src/lib/expansion/expansion-panel.ts +++ b/src/lib/expansion/expansion-panel.ts @@ -28,9 +28,9 @@ import { } from '@angular/core'; import {Subject} from 'rxjs'; import {filter, startWith, take} from 'rxjs/operators'; -import {MatAccordion} from './accordion'; -import {matExpansionAnimations} from './expansion-animations'; +import {MatAccordion, MatAccordionTogglePosition} from './accordion'; import {MatExpansionPanelContent} from './expansion-panel-content'; +import {matExpansionAnimations} from './expansion-animations'; /** MatExpansionPanel's states. */ @@ -72,6 +72,16 @@ export class MatExpansionPanel extends CdkAccordionItem } private _hideToggle = false; + /** The positioning of the expansion indicator. */ + @Input() + get togglePosition(): MatAccordionTogglePosition { + return this.accordion ? this.accordion.togglePosition : this._togglePosition; + } + set togglePosition(position: MatAccordionTogglePosition) { + this._togglePosition = position; + } + private _togglePosition: MatAccordionTogglePosition = 'after'; + /** Stream that emits for changes in `@Input` properties. */ readonly _inputChanges = new Subject(); diff --git a/src/lib/expansion/expansion.md b/src/lib/expansion/expansion.md index d93ad448fac9..96983a081b23 100644 --- a/src/lib/expansion/expansion.md +++ b/src/lib/expansion/expansion.md @@ -15,7 +15,8 @@ header to align with Material Design specifications. By default, the expansion-panel header includes a toggle icon at the end of the header to indicate the expansion state. This icon can be hidden via the -`hideToggle` property. +`hideToggle` property. The icon's position can also be configured with the `togglePosition` +property. ```html diff --git a/src/lib/expansion/expansion.spec.ts b/src/lib/expansion/expansion.spec.ts index 914dd95a2a8f..39fb8d1b154e 100644 --- a/src/lib/expansion/expansion.spec.ts +++ b/src/lib/expansion/expansion.spec.ts @@ -211,13 +211,30 @@ describe('MatExpansionPanel', () => { const arrow = fixture.debugElement.query(By.css('.mat-expansion-indicator')).nativeElement; - expect(arrow.style.transform).toBe('rotate(0deg)', 'Expected no rotation.'); + expect(arrow.style.transform).toBe('rotate(45deg)', 'Expected 45 degree rotation.'); fixture.componentInstance.expanded = true; fixture.detectChanges(); tick(250); - expect(arrow.style.transform).toBe('rotate(180deg)', 'Expected 180 degree rotation.'); + expect(arrow.style.transform).toBe('rotate(225deg)', 'Expected 225 degree rotation.'); + })); + + it('should update the indicator position when the position is set programmatically', + fakeAsync(() => { + const fixture = TestBed.createComponent(PanelWithContent); + + fixture.detectChanges(); + + const arrowContainer = fixture.debugElement.query( + By.css('.mat-expansion-indicator-container')).nativeElement; + + expect(arrowContainer.classList).not.toContain('mat-expansion-indicator-container-before'); + + fixture.componentInstance.togglePosition = 'before'; + fixture.detectChanges(); + + expect(arrowContainer.classList).toContain('mat-expansion-indicator-container-before'); })); it('should make sure accordion item runs ngOnDestroy when expansion panel is destroyed', () => { @@ -329,10 +346,11 @@ describe('MatExpansionPanel', () => { @Component({ template: ` + [hideToggle]="hideToggle" + [togglePosition]="togglePosition" + [disabled]="disabled" + (opened)="openCallback()" + (closed)="closeCallback()"> Panel Title

Some content

@@ -341,6 +359,7 @@ describe('MatExpansionPanel', () => { class PanelWithContent { expanded = false; hideToggle = false; + togglePosition = 'after'; disabled = false; openCallback = jasmine.createSpy('openCallback'); closeCallback = jasmine.createSpy('closeCallback');