Skip to content

Commit 51d859f

Browse files
josephperrottmmalerba
authored andcommitted
feat(expansion): allow expansion indicator positioning. (#8199)
1 parent b9041e3 commit 51d859f

12 files changed

+155
-37
lines changed

src/demo-app/expansion/expansion-demo.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ <h1>matAccordion</h1>
4343
<mat-radio-button value="default">Default</mat-radio-button>
4444
<mat-radio-button value="flat">Flat</mat-radio-button>
4545
</mat-radio-group>
46+
<p>Toggle Position</p>
47+
<mat-radio-group [(ngModel)]="togglePosition">
48+
<mat-radio-button value="before">Before</mat-radio-button>
49+
<mat-radio-button value="after">After</mat-radio-button>
50+
</mat-radio-group>
4651
<p>Accordion Actions <sup>('Multi Expansion' mode only)</sup></p>
4752
<div>
4853
<button mat-button (click)="accordion.openAll()" [disabled]="!multi">Expand All</button>
@@ -55,8 +60,8 @@ <h1>matAccordion</h1>
5560
</div>
5661
</div>
5762
<br>
58-
<mat-accordion [displayMode]="displayMode" [multi]="multi"
59-
class="demo-expansion-width">
63+
<mat-accordion [displayMode]="displayMode" [multi]="multi" [togglePosition]="togglePosition"
64+
class="demo-expansion-width">
6065
<mat-expansion-panel #panel1 [hideToggle]="hideToggle">
6166
<mat-expansion-panel-header>Section 1</mat-expansion-panel-header>
6267
<p>This is the content text that makes sense here.</p>
@@ -75,7 +80,7 @@ <h1>matAccordion</h1>
7580
</mat-expansion-panel>
7681
</mat-accordion>
7782

78-
<h1>cdkAccordion</h1>
83+
<h1>CdkAccordion</h1>
7984
<div>
8085
<p>Accordion Options</p>
8186
<div>
@@ -108,4 +113,3 @@ <h1>cdkAccordion</h1>
108113
<p *ngIf="item3.expanded">I only show if item 3 is expanded</p>
109114
</cdk-accordion-item>
110115
</cdk-accordion>
111-

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class ExpansionDemo {
2020
@ViewChild(MatAccordion) accordion: MatAccordion;
2121

2222
displayMode = 'default';
23+
togglePosition = 'after';
2324
multi = false;
2425
hideToggle = false;
2526
disabled = false;

src/lib/expansion/_expansion-theme.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
}
3131

3232
.mat-expansion-panel-header-description,
33-
.mat-expansion-indicator::after {
33+
.mat-expansion-indicator {
3434
color: mat-color($foreground, secondary-text);
3535
}
3636

src/lib/expansion/accordion.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
55
import {MatExpansionModule, MatAccordion} from './index';
66

77

8-
describe('CdkAccordion', () => {
8+
describe('MatAccordion', () => {
99
beforeEach(async(() => {
1010
TestBed.configureTestingModule({
1111
imports: [
@@ -83,12 +83,39 @@ describe('CdkAccordion', () => {
8383
fixture.detectChanges();
8484
expect(panels[0].classes['mat-expanded']).toBeFalsy();
8585
expect(panels[1].classes['mat-expanded']).toBeFalsy();
86+
87+
});
88+
89+
it('should correctly apply the displayMode provided', () => {
90+
const fixture = TestBed.createComponent(SetOfItems);
91+
const firstPanel = fixture.debugElement.queryAll(By.css('.mat-expansion-panel'))[0];
92+
93+
fixture.componentInstance.firstPanelExpanded = true;
94+
fixture.detectChanges();
95+
expect(firstPanel.classes['mat-expansion-panel-spacing']).toBeTruthy();
96+
97+
fixture.componentInstance.displayMode = 'flat';
98+
fixture.detectChanges();
99+
expect(firstPanel.classes['mat-expansion-panel-spacing']).toBeFalsy();
100+
});
101+
102+
it('should correctly apply the togglePosition provided', () => {
103+
const fixture = TestBed.createComponent(SetOfItems);
104+
fixture.detectChanges();
105+
const firstPanelHeader =
106+
fixture.debugElement.queryAll(By.css('.mat-expansion-indicator-container'))[0];
107+
108+
expect(firstPanelHeader.classes['mat-expansion-indicator-container-before']).toBeFalsy();
109+
110+
fixture.componentInstance.togglePosition = 'before';
111+
fixture.detectChanges();
112+
expect(firstPanelHeader.classes['mat-expansion-indicator-container-before']).toBeTruthy();
86113
});
87114
});
88115

89116

90117
@Component({template: `
91-
<mat-accordion [multi]="multi">
118+
<mat-accordion [multi]="multi" [displayMode]="displayMode" [togglePosition]="togglePosition">
92119
<mat-expansion-panel [expanded]="firstPanelExpanded">
93120
<mat-expansion-panel-header>Summary</mat-expansion-panel-header>
94121
<p>Content</p>
@@ -102,6 +129,8 @@ class SetOfItems {
102129
@ViewChild(MatAccordion) accordion: MatAccordion;
103130

104131
multi: boolean = false;
132+
displayMode = 'default';
133+
togglePosition = 'after';
105134
firstPanelExpanded: boolean = false;
106135
secondPanelExpanded: boolean = false;
107136
secondPanelDisabled: boolean = false;

src/lib/expansion/accordion.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, Input} from '@angular/core';
9+
import {Directive, Input, SimpleChanges} from '@angular/core';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1111
import {CdkAccordion} from '@angular/cdk/accordion';
12+
import {Subject} from 'rxjs';
1213

1314
/** MatAccordion's display modes. */
1415
export type MatAccordionDisplayMode = 'default' | 'flat';
1516

17+
18+
/** MatAccordion's toggle positions. */
19+
export type MatAccordionTogglePosition = 'before' | 'after';
20+
1621
/**
1722
* Directive for a Material Design Accordion.
1823
*/
@@ -24,6 +29,9 @@ export type MatAccordionDisplayMode = 'default' | 'flat';
2429
}
2530
})
2631
export class MatAccordion extends CdkAccordion {
32+
/** Stream that emits for changes in `@Input` properties. */
33+
_inputChanges = new Subject<SimpleChanges>();
34+
2735
/** Whether the expansion indicator should be hidden. */
2836
@Input()
2937
get hideToggle(): boolean { return this._hideToggle; }
@@ -39,4 +47,15 @@ export class MatAccordion extends CdkAccordion {
3947
* elevation.
4048
*/
4149
@Input() displayMode: MatAccordionDisplayMode = 'default';
50+
51+
/** The positioning of the expansion indicator. */
52+
@Input() togglePosition: MatAccordionTogglePosition = 'after';
53+
54+
ngOnChanges(changes: SimpleChanges) {
55+
this._inputChanges.next(changes);
56+
}
57+
58+
ngOnDestroy() {
59+
this._inputChanges.complete();
60+
}
4261
}

src/lib/expansion/expansion-animations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export const matExpansionAnimations: {
2828
} = {
2929
/** Animation that rotates the indicator arrow. */
3030
indicatorRotate: trigger('indicatorRotate', [
31-
state('collapsed', style({transform: 'rotate(0deg)'})),
32-
state('expanded', style({transform: 'rotate(180deg)'})),
31+
state('collapsed', style({transform: 'rotate(45deg)'})),
32+
state('expanded', style({transform: 'rotate(225deg)'})),
3333
transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)),
3434
]),
3535

src/lib/expansion/expansion-panel-header.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
<ng-content select="mat-panel-description"></ng-content>
44
<ng-content></ng-content>
55
</span>
6-
<span [@indicatorRotate]="_getExpandedState()" *ngIf="_showToggle()"
7-
class="mat-expansion-indicator"></span>
6+
<div class="mat-expansion-indicator-container"
7+
[class.mat-expansion-indicator-container-before]="_placeToggleBefore()">
8+
<span *ngIf="_isToggleVisible()" class="mat-expansion-indicator"
9+
[@indicatorRotate]="_getExpandedState()"></span>
10+
</div>

src/lib/expansion/expansion-panel-header.scss

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
21
.mat-expansion-panel-header {
32
display: flex;
43
flex-direction: row;
54
align-items: center;
6-
padding: 0 24px;
5+
padding: 0 16px;
76

87
&:focus,
98
&:hover {
@@ -25,12 +24,13 @@
2524
flex: 1;
2625
flex-direction: row;
2726
overflow: hidden;
27+
padding-left: 8px;
2828
}
2929

3030
.mat-expansion-panel-header-title,
3131
.mat-expansion-panel-header-description {
3232
display: flex;
33-
flex-grow: 1;
33+
flex: 1;
3434
margin-right: 16px;
3535

3636
[dir='rtl'] & {
@@ -40,19 +40,37 @@
4040
}
4141

4242
.mat-expansion-panel-header-description {
43-
flex-grow: 2;
43+
flex: 2;
44+
}
45+
46+
.mat-expansion-indicator-container {
47+
// A margin is required to offset the entire expansion indicator against the space the arrow
48+
// takes up. It is calculated as sqrt(2 * border-width ^ 2) / 2.
49+
margin-bottom: 1.41px;
50+
padding: 0 8px 0 0;
51+
width: 8px;
52+
order: 1;
53+
54+
&.mat-expansion-indicator-container-before {
55+
order: -1;
56+
padding: 0 8px;
57+
}
4458
}
4559

46-
/**
47-
* Creates the expansion indicator arrow. Done using ::after rather than having
48-
* additional nodes in the template.
49-
*/
50-
.mat-expansion-indicator::after {
60+
.mat-expansion-indicator {
5161
border-style: solid;
5262
border-width: 0 2px 2px 0;
53-
content: '';
54-
display: inline-block;
55-
padding: 3px;
63+
display: block;
64+
height: 6px;
5665
transform: rotate(45deg);
57-
vertical-align: middle;
66+
// The transform origin is set by determining the center pointer of the arrow created. It is
67+
// calculated as by calculating the length of the line between the top left corner of the div,
68+
// and the centroid of the triangle created in the bottom right half of the div. This centroid
69+
// is calculated for both X and Y (as the indicator is a square) as
70+
// (indicator-width + indicator-height + 0) / 3
71+
// The length between the resulting coordinates and the top left (0, 0) of the div are
72+
// calculated sqrt((centroids-x-coord ^ 2) + (centroids-y-coord ^ 2))
73+
// This value is used to transform the origin on both the X and Y axes.
74+
transform-origin: 5.65px 5.65px;
75+
width: 6px;
5876
}

src/lib/expansion/expansion-panel-header.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {
1616
ElementRef,
1717
Host,
1818
Input,
19+
Optional,
1920
OnDestroy,
2021
ViewEncapsulation,
2122
} from '@angular/core';
2223
import {merge, Subscription} from 'rxjs';
2324
import {filter} from 'rxjs/operators';
2425
import {matExpansionAnimations} from './expansion-animations';
2526
import {MatExpansionPanel} from './expansion-panel';
27+
import {MatAccordion} from './accordion';
2628

2729

2830
/**
@@ -65,17 +67,24 @@ export class MatExpansionPanelHeader implements OnDestroy {
6567
private _parentChangeSubscription = Subscription.EMPTY;
6668

6769
constructor(
70+
@Optional() accordion: MatAccordion,
6871
@Host() public panel: MatExpansionPanel,
6972
private _element: ElementRef,
7073
private _focusMonitor: FocusMonitor,
7174
private _changeDetectorRef: ChangeDetectorRef) {
7275

76+
let changeStreams = [panel._inputChanges];
77+
if (accordion) {
78+
changeStreams.push(accordion._inputChanges);
79+
}
80+
7381
// Since the toggle state depends on an @Input on the panel, we
74-
// need to subscribe and trigger change detection manually.
82+
// need to subscribe and trigger change detection manually.
7583
this._parentChangeSubscription = merge(
7684
panel.opened,
7785
panel.closed,
78-
panel._inputChanges.pipe(filter(changes => !!(changes.hideToggle || changes.disabled)))
86+
merge(...changeStreams).pipe(
87+
filter(changes => !!(changes.hideToggle || changes.disabled || changes.togglePosition)))
7988
)
8089
.subscribe(() => this._changeDetectorRef.markForCheck());
8190

@@ -109,10 +118,15 @@ export class MatExpansionPanelHeader implements OnDestroy {
109118
}
110119

111120
/** Gets whether the expand indicator should be shown. */
112-
_showToggle(): boolean {
121+
_isToggleVisible(): boolean {
113122
return !this.panel.hideToggle && !this.panel.disabled;
114123
}
115124

125+
/** Whether the expand indicator should be shown before the header content */
126+
_placeToggleBefore(): boolean {
127+
return this.panel.togglePosition === 'before';
128+
}
129+
116130
/** Handle keydown event calling to toggle() if appropriate. */
117131
_keydown(event: KeyboardEvent) {
118132
switch (event.keyCode) {

src/lib/expansion/expansion-panel.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import {
2828
} from '@angular/core';
2929
import {Subject} from 'rxjs';
3030
import {filter, startWith, take} from 'rxjs/operators';
31-
import {MatAccordion} from './accordion';
32-
import {matExpansionAnimations} from './expansion-animations';
31+
import {MatAccordion, MatAccordionTogglePosition} from './accordion';
3332
import {MatExpansionPanelContent} from './expansion-panel-content';
33+
import {matExpansionAnimations} from './expansion-animations';
3434

3535

3636
/** MatExpansionPanel's states. */
@@ -72,6 +72,16 @@ export class MatExpansionPanel extends CdkAccordionItem
7272
}
7373
private _hideToggle = false;
7474

75+
/** The positioning of the expansion indicator. */
76+
@Input()
77+
get togglePosition(): MatAccordionTogglePosition {
78+
return this.accordion ? this.accordion.togglePosition : this._togglePosition;
79+
}
80+
set togglePosition(position: MatAccordionTogglePosition) {
81+
this._togglePosition = position;
82+
}
83+
private _togglePosition: MatAccordionTogglePosition = 'after';
84+
7585
/** Stream that emits for changes in `@Input` properties. */
7686
readonly _inputChanges = new Subject<SimpleChanges>();
7787

src/lib/expansion/expansion.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ header to align with Material Design specifications.
1515

1616
By default, the expansion-panel header includes a toggle icon at the end of the
1717
header to indicate the expansion state. This icon can be hidden via the
18-
`hideToggle` property.
18+
`hideToggle` property. The icon's position can also be configured with the `togglePosition`
19+
property.
1920

2021
```html
2122
<mat-expansion-panel>

0 commit comments

Comments
 (0)