@@ -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