Skip to content

Commit b1a462c

Browse files
committed
Allow snackbar position to be set to left or center.
1 parent 85bc3a6 commit b1a462c

File tree

7 files changed

+132
-33
lines changed

7 files changed

+132
-33
lines changed

src/demo-app/snack-bar/snack-bar-demo.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ <h1>SnackBar demo</h1>
33
<div>
44
Message: <md-input-container><input mdInput type="text" [(ngModel)]="message"></md-input-container>
55
</div>
6+
<div>
7+
<div>Position in page: </div>
8+
<md-select [(ngModel)]="position.horizontal">
9+
<md-option value="left">Left</md-option>
10+
<md-option value="center">Center</md-option>
11+
<md-option value="right">Right</md-option>
12+
</md-select>
13+
<md-select [(ngModel)]="position.vertical">
14+
<md-option value="top">Top</md-option>
15+
<md-option value="bottom">Bottom</md-option>
16+
</md-select>
17+
</div>
618
<div>
719
<md-checkbox [(ngModel)]="action">
820
<p *ngIf="!action">Show button on snack bar</p>
@@ -27,7 +39,6 @@ <h1>SnackBar demo</h1>
2739
</md-input-container>
2840
</md-checkbox>
2941
</div>
30-
3142
<p>
3243
<md-checkbox [(ngModel)]="addExtraClass">Add extra class to container</md-checkbox>
3344
</p>

src/demo-app/snack-bar/snack-bar-demo.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, ViewEncapsulation} from '@angular/core';
2-
import {MdSnackBar, MdSnackBarConfig} from '@angular/material';
2+
import {MdSnackBar, MdSnackBarConfig, MdSnackBarPosition} from '@angular/material';
33

44
@Component({
55
moduleId: module.id,
@@ -15,12 +15,20 @@ export class SnackBarDemo {
1515
setAutoHide: boolean = true;
1616
autoHide: number = 10000;
1717
addExtraClass: boolean = false;
18+
position: MdSnackBarPosition = {
19+
horizontal: 'center',
20+
vertical: 'bottom'
21+
};
1822

1923
constructor(public snackBar: MdSnackBar) { }
2024

2125
open() {
2226
let config = new MdSnackBarConfig();
23-
config.duration = this.autoHide;
27+
config.position = {
28+
vertical: this.position.vertical,
29+
horizontal: this.position.horizontal
30+
};
31+
config.duration = this.setAutoHide ? this.autoHide : 0;
2432
config.extraClasses = this.addExtraClass ? ['party'] : null;
2533
this.snackBar.open(this.message, this.action && this.actionButtonLabel, config);
2634
}

src/lib/snack-bar/snack-bar-config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {ViewContainerRef} from '@angular/core';
22
import {AriaLivePoliteness} from '../core';
33

4+
export interface MdSnackBarPosition {
5+
horizontal: string;
6+
vertical: string;
7+
}
8+
49
/**
510
* Configuration used when opening a snack-bar.
611
*/
@@ -19,4 +24,10 @@ export class MdSnackBarConfig {
1924

2025
/** Extra CSS classes to be added to the snack bar container. */
2126
extraClasses?: string[];
27+
28+
/** The position to place the snack bar in the view, either 'left' or 'center'. */
29+
position?: MdSnackBarPosition = {
30+
horizontal: 'center',
31+
vertical: 'bottom'
32+
};
2233
}

src/lib/snack-bar/snack-bar-container.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
$mat-snack-bar-padding: 14px 24px !default;
55
$mat-snack-bar-min-width: 288px !default;
66
$mat-snack-bar-max-width: 568px !default;
7+
$mat-snack-bar-spacing-margin: 24px !default;
78

89

910
:host {
@@ -12,12 +13,21 @@ $mat-snack-bar-max-width: 568px !default;
1213
border-radius: 2px;
1314
box-sizing: content-box;
1415
display: block;
16+
margin: $mat-snack-bar-spacing-margin;
1517
max-width: $mat-snack-bar-max-width;
1618
min-width: $mat-snack-bar-min-width;
1719
padding: $mat-snack-bar-padding;
1820
// Initial transformation is applied to start snack bar out of view.
1921
transform: translateY(100%);
2022

23+
&.md-snack-bar-center {
24+
margin: 0;
25+
}
26+
27+
&.md-snack-bar-top {
28+
transform: translateY(-100%);
29+
}
30+
2131
@include cdk-high-contrast {
2232
border: solid 1px;
2333
}

src/lib/snack-bar/snack-bar-container.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {Subject} from 'rxjs/Subject';
2828

2929

3030

31-
export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void';
31+
export type SnackBarState = 'visible' | 'hidden' | 'void';
3232

3333
// TODO(jelbourn): we can't use constants from animation.ts here because you can't use
3434
// a text interpolation in anything that is analyzed statically with ngc (for AoT compile).
@@ -46,16 +46,22 @@ export const HIDE_ANIMATION = '195ms cubic-bezier(0.0,0.0,0.2,1)';
4646
styleUrls: ['snack-bar-container.css'],
4747
host: {
4848
'role': 'alert',
49-
'[@state]': 'animationState',
49+
'[@state]': 'getAnimationState()',
5050
'(@state.done)': 'onAnimationEnd($event)'
5151
},
5252
animations: [
5353
trigger('state', [
54-
state('initial', style({transform: 'translateY(100%)'})),
55-
state('visible', style({transform: 'translateY(0%)'})),
56-
state('complete', style({transform: 'translateY(100%)'})),
57-
transition('visible => complete', animate(HIDE_ANIMATION)),
58-
transition('initial => visible, void => visible', animate(SHOW_ANIMATION)),
54+
// Animation from top.
55+
state('visible-top', style({transform: 'translateY(0%)'})),
56+
state('hidden-top', style({transform: 'translateY(-100%)'})),
57+
transition('visible-top => hidden-top', animate(HIDE_ANIMATION)),
58+
transition('void => visible-top', animate(SHOW_ANIMATION)),
59+
// Animation from bottom.
60+
state('visible-bottom', style({transform: 'translateY(0%)'})),
61+
state('hidden-bottom', style({transform: 'translateY(100%)'})),
62+
transition('visible-bottom => hidden-bottom', animate(HIDE_ANIMATION)),
63+
transition('void => visible-bottom',
64+
animate(SHOW_ANIMATION)),
5965
])
6066
],
6167
})
@@ -70,7 +76,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
7076
private onEnter: Subject<any> = new Subject();
7177

7278
/** The state of the snack bar animations. */
73-
animationState: SnackBarState = 'initial';
79+
private animationState: SnackBarState;
7480

7581
/** The snack bar configuration. */
7682
snackBarConfig: MdSnackBarConfig;
@@ -82,6 +88,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
8288
super();
8389
}
8490

91+
/**
92+
* Gets the current animation state both combining one of the possibilities from
93+
* SnackBarState and the vertical location.
94+
*/
95+
getAnimationState(): string {
96+
return `${this.animationState}-${this.snackBarConfig.position.vertical}`;
97+
}
98+
8599
/** Attach a component portal as content to this snack bar container. */
86100
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
87101
if (this._portalHost.hasAttached()) {
@@ -96,6 +110,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
96110
}
97111
}
98112

113+
if (this.snackBarConfig.position.horizontal === 'center') {
114+
this._renderer.setElementClass(this._elementRef.nativeElement, 'md-snack-bar-center', true);
115+
}
116+
117+
if (this.snackBarConfig.position.vertical === 'top') {
118+
this._renderer.setElementClass(this._elementRef.nativeElement, 'md-snack-bar-top', true);
119+
}
120+
99121
return this._portalHost.attachComponentPortal(portal);
100122
}
101123

@@ -106,11 +128,11 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
106128

107129
/** Handle end of animations, updating the state of the snackbar. */
108130
onAnimationEnd(event: AnimationEvent) {
109-
if (event.toState === 'void' || event.toState === 'complete') {
131+
if (event.toState === 'void' || event.toState.startsWith('hidden')) {
110132
this._completeExit();
111133
}
112134

113-
if (event.toState === 'visible') {
135+
if (event.toState.startsWith('visible')) {
114136
this._ngZone.run(() => {
115137
this.onEnter.next();
116138
this.onEnter.complete();
@@ -131,7 +153,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
131153

132154
/** Begin animation of the snack bar exiting from view. */
133155
exit(): Observable<void> {
134-
this.animationState = 'complete';
156+
this.animationState = 'hidden';
135157
return this._onExit();
136158
}
137159

src/lib/snack-bar/snack-bar.spec.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import {MdSnackBarModule, MdSnackBar, MdSnackBarConfig, SimpleSnackBar} from './
1414
import {OverlayContainer, LiveAnnouncer} from '../core';
1515

1616

17-
// TODO(josephperrott): Update tests to mock waiting for time to complete for animations.
18-
1917
describe('MdSnackBar', () => {
2018
let snackBar: MdSnackBar;
2119
let liveAnnouncer: LiveAnnouncer;
@@ -113,7 +111,7 @@ describe('MdSnackBar', () => {
113111

114112
viewContainerFixture.detectChanges();
115113

116-
expect(snackBarRef.instance)
114+
expect(snackBarRef.instance).
117115
.toEqual(jasmine.any(SimpleSnackBar),
118116
'Expected the snack bar content component to be SimpleSnackBar');
119117
expect(snackBarRef.instance.snackBarRef)
@@ -193,8 +191,28 @@ describe('MdSnackBar', () => {
193191
let snackBarRef = snackBar.open(simpleMessage, null, config);
194192

195193
viewContainerFixture.detectChanges();
196-
expect(snackBarRef.containerInstance.animationState)
197-
.toBe('visible', `Expected the animation state would be 'visible'.`);
194+
expect(snackBarRef.containerInstance.getAnimationState())
195+
.toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`);
196+
});
197+
198+
it('should set the animation state based on the vertical position in the config', () => {
199+
let config = {
200+
viewContainerRef: testViewContainerRef,
201+
position: {
202+
vertical: 'top',
203+
horizontal: 'center'
204+
}
205+
};
206+
let snackBarRef = snackBar.open(simpleMessage, null, config);
207+
208+
viewContainerFixture.detectChanges();
209+
expect(snackBarRef.containerInstance.getAnimationState())
210+
.toBe('visible-top', `Expected the animation state would be 'visible-top'.`);
211+
snackBarRef.dismiss();
212+
213+
viewContainerFixture.detectChanges();
214+
expect(snackBarRef.containerInstance.getAnimationState())
215+
.toBe('hidden-top', `Expected the animation state would be 'hidden-top'.`);
198216
});
199217

200218
it('should set the animation state to complete on exit', () => {
@@ -203,8 +221,8 @@ describe('MdSnackBar', () => {
203221
snackBarRef.dismiss();
204222

205223
viewContainerFixture.detectChanges();
206-
expect(snackBarRef.containerInstance.animationState)
207-
.toBe('complete', `Expected the animation state would be 'complete'.`);
224+
expect(snackBarRef.containerInstance.getAnimationState())
225+
.toBe('hidden-bottom', `Expected the animation state would be 'hidden-bottom'.`);
208226
});
209227

210228
it(`should set the old snack bar animation state to complete and the new snack bar animation
@@ -214,8 +232,8 @@ describe('MdSnackBar', () => {
214232
let dismissObservableCompleted = false;
215233

216234
viewContainerFixture.detectChanges();
217-
expect(snackBarRef.containerInstance.animationState)
218-
.toBe('visible', `Expected the animation state would be 'visible'.`);
235+
expect(snackBarRef.containerInstance.getAnimationState())
236+
.toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`);
219237

220238
let config2 = {viewContainerRef: testViewContainerRef};
221239
let snackBarRef2 = snackBar.open(simpleMessage, null, config2);
@@ -227,10 +245,10 @@ describe('MdSnackBar', () => {
227245

228246
viewContainerFixture.whenStable().then(() => {
229247
expect(dismissObservableCompleted).toBe(true);
230-
expect(snackBarRef.containerInstance.animationState)
231-
.toBe('complete', `Expected the animation state would be 'complete'.`);
232-
expect(snackBarRef2.containerInstance.animationState)
233-
.toBe('visible', `Expected the animation state would be 'visible'.`);
248+
expect(snackBarRef.containerInstance.getAnimationState())
249+
.toBe('hidden-bottom', `Expected the animation state would be 'hidden-bottom'.`);
250+
expect(snackBarRef2.containerInstance.getAnimationState())
251+
.toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`);
234252
});
235253
}));
236254

@@ -249,7 +267,8 @@ describe('MdSnackBar', () => {
249267

250268
// Wait for the snackbar open animation to finish.
251269
viewContainerFixture.whenStable().then(() => {
252-
expect(snackBarRef.containerInstance.animationState).toBe('visible');
270+
expect(snackBarRef.containerInstance.getAnimationState())
271+
.toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`);
253272
});
254273
});
255274
}));

src/lib/snack-bar/snack-bar.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class MdSnackBar {
5454
*/
5555
openFromComponent<T>(component: ComponentType<T>, config?: MdSnackBarConfig): MdSnackBarRef<T> {
5656
config = _applyConfigDefaults(config);
57-
let overlayRef = this._createOverlay();
57+
let overlayRef = this._createOverlay(config);
5858
let snackBarContainer = this._attachSnackBarContainer(overlayRef, config);
5959
let snackBarRef = this._attachSnackbarContent(component, snackBarContainer, overlayRef);
6060

@@ -140,11 +140,29 @@ export class MdSnackBar {
140140
/**
141141
* Creates a new overlay and places it in the correct location.
142142
*/
143-
private _createOverlay(): OverlayRef {
143+
private _createOverlay(config: MdSnackBarConfig): OverlayRef {
144144
let state = new OverlayState();
145-
state.positionStrategy = this._overlay.position().global()
146-
.centerHorizontally()
147-
.bottom('0');
145+
let positionStrategy = this._overlay.position().global();
146+
switch (config.position.horizontal) {
147+
case 'left':
148+
positionStrategy.left('0');
149+
break;
150+
case 'right':
151+
positionStrategy.right('0');
152+
break;
153+
case 'center':
154+
positionStrategy.centerHorizontally();
155+
break;
156+
}
157+
switch (config.position.vertical) {
158+
case 'top':
159+
positionStrategy.top('0');
160+
break;
161+
case 'bottom':
162+
positionStrategy.bottom('0');
163+
break;
164+
}
165+
state.positionStrategy = positionStrategy;
148166
return this._overlay.create(state);
149167
}
150168
}

0 commit comments

Comments
 (0)