Skip to content

Commit 05c0854

Browse files
committed
Allow snackbar position on the screen.
1 parent ec4ea06 commit 05c0854

File tree

7 files changed

+379
-42
lines changed

7 files changed

+379
-42
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ <h1>SnackBar demo</h1>
33
<div>
44
Message: <md-form-field><input mdInput type="text" [(ngModel)]="message"></md-form-field>
55
</div>
6+
<div>
7+
<div>Position in page: </div>
8+
<md-select [(ngModel)]="horizontalPosition">
9+
<md-option value="start">Start</md-option>
10+
<md-option value="end">End</md-option>
11+
<md-option value="left">Left</md-option>
12+
<md-option value="right">Right</md-option>
13+
<md-option value="center">Center</md-option>
14+
</md-select>
15+
<md-select [(ngModel)]="verticalPosition">
16+
<md-option value="top">Top</md-option>
17+
<md-option value="bottom">Bottom</md-option>
18+
</md-select>
19+
</div>
620
<div>
721
<md-checkbox [(ngModel)]="action">
822
<p *ngIf="!action">Show button on snack bar</p>
@@ -27,7 +41,6 @@ <h1>SnackBar demo</h1>
2741
</md-form-field>
2842
</md-checkbox>
2943
</div>
30-
3144
<p>
3245
<md-checkbox [(ngModel)]="addExtraClass">Add extra class to container</md-checkbox>
3346
</p>

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import {Component, ViewEncapsulation} from '@angular/core';
2-
import {MdSnackBar, MdSnackBarConfig} from '@angular/material';
2+
import {
3+
MdSnackBar,
4+
MdSnackBarConfig,
5+
MdSnackBarHorizontalPosition,
6+
MdSnackBarVerticalPosition,
7+
Dir,
8+
} from '@angular/material';
39

410
@Component({
511
moduleId: module.id,
@@ -15,13 +21,18 @@ export class SnackBarDemo {
1521
setAutoHide: boolean = true;
1622
autoHide: number = 10000;
1723
addExtraClass: boolean = false;
24+
horizontalPosition: MdSnackBarHorizontalPosition = 'center';
25+
verticalPosition: MdSnackBarVerticalPosition = 'bottom';
1826

19-
constructor(public snackBar: MdSnackBar) { }
27+
constructor(public snackBar: MdSnackBar, private dir: Dir) { }
2028

2129
open() {
2230
let config = new MdSnackBarConfig();
31+
config.verticalPosition = this.verticalPosition;
32+
config.horizontalPosition = this.horizontalPosition;
2333
config.duration = this.autoHide;
2434
config.extraClasses = this.addExtraClass ? ['party'] : undefined;
35+
config.direction = this.dir.value;
2536
this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
2637
}
2738
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {Direction} from '@angular/cdk/bidi';
1212

1313
export const MD_SNACK_BAR_DATA = new InjectionToken<any>('MdSnackBarData');
1414

15+
/** Possible values for horizontalPosition on MdSnackBarConfig. */
16+
export type MdSnackBarHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';
17+
18+
/** Possible values for verticalPosition on MdSnackBarConfig. */
19+
export type MdSnackBarVerticalPosition = 'top' | 'bottom';
20+
1521
/**
1622
* Configuration used when opening a snack-bar.
1723
*/
@@ -36,4 +42,10 @@ export class MdSnackBarConfig {
3642

3743
/** Data being injected into the child component. */
3844
data?: any = null;
45+
46+
/** The horizontal position to place the snack bar. */
47+
horizontalPosition?: MdSnackBarHorizontalPosition = 'center';
48+
49+
/** The vertical position to place the snack bar. */
50+
verticalPosition?: MdSnackBarVerticalPosition = 'bottom';
3951
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,36 @@
33
$mat-snack-bar-padding: 14px 24px !default;
44
$mat-snack-bar-min-width: 288px !default;
55
$mat-snack-bar-max-width: 568px !default;
6+
$mat-snack-bar-spacing-margin: 24px !default;
67

78

89
.mat-snack-bar-container {
910
border-radius: 2px;
1011
box-sizing: content-box;
1112
display: block;
13+
margin: $mat-snack-bar-spacing-margin;
1214
max-width: $mat-snack-bar-max-width;
1315
min-width: $mat-snack-bar-min-width;
1416
padding: $mat-snack-bar-padding;
15-
// Initial transformation is applied to start snack bar out of view.
17+
// Initial transformation is applied to start snack bar out of view, below its target position.
1618
transform: translateY(100%);
1719

20+
/**
21+
* Removes margin of snack bars which are center positioned horizontally. This
22+
* is done to align snack bars to the edge of the view vertically to match spec.
23+
*/
24+
&.mat-snack-bar-center {
25+
margin: 0;
26+
}
27+
28+
/**
29+
* To allow for animations from a 'top' vertical position to animate in a downward
30+
* direction, set the translation to start the snack bar above the target position.
31+
*/
32+
&.mat-snack-bar-top {
33+
transform: translateY(-100%);
34+
}
35+
1836
@include cdk-high-contrast {
1937
border: solid 1px;
2038
}

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

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {Subject} from 'rxjs/Subject';
3737
import {MdSnackBarConfig} from './snack-bar-config';
3838

3939

40-
export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void';
40+
export type SnackBarState = 'visible' | 'hidden' | 'void';
4141

4242
// TODO(jelbourn): we can't use constants from animation.ts here because you can't use
4343
// a text interpolation in anything that is analyzed statically with ngc (for AoT compile).
@@ -58,17 +58,22 @@ export const HIDE_ANIMATION = '195ms cubic-bezier(0.0,0.0,0.2,1)';
5858
host: {
5959
'role': 'alert',
6060
'class': 'mat-snack-bar-container',
61-
'[@state]': 'animationState',
61+
'[@state]': 'getAnimationState()',
6262
'(@state.done)': 'onAnimationEnd($event)'
6363
},
6464
animations: [
6565
trigger('state', [
66-
state('void', style({transform: 'translateY(100%)'})),
67-
state('initial', style({transform: 'translateY(100%)'})),
68-
state('visible', style({transform: 'translateY(0%)'})),
69-
state('complete', style({transform: 'translateY(100%)'})),
70-
transition('visible => complete', animate(HIDE_ANIMATION)),
71-
transition('initial => visible, void => visible', animate(SHOW_ANIMATION)),
66+
// Animation from top.
67+
state('visible-top', style({transform: 'translateY(0%)'})),
68+
state('hidden-top', style({transform: 'translateY(-100%)'})),
69+
transition('visible-top => hidden-top', animate(HIDE_ANIMATION)),
70+
transition('void => visible-top', animate(SHOW_ANIMATION)),
71+
// Animation from bottom.
72+
state('visible-bottom', style({transform: 'translateY(0%)'})),
73+
state('hidden-bottom', style({transform: 'translateY(100%)'})),
74+
transition('visible-bottom => hidden-bottom', animate(HIDE_ANIMATION)),
75+
transition('void => visible-bottom',
76+
animate(SHOW_ANIMATION)),
7277
])
7378
],
7479
})
@@ -83,7 +88,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
8388
private onEnter: Subject<any> = new Subject();
8489

8590
/** The state of the snack bar animations. */
86-
animationState: SnackBarState = 'initial';
91+
private _animationState: SnackBarState;
8792

8893
/** The snack bar configuration. */
8994
snackBarConfig: MdSnackBarConfig;
@@ -95,6 +100,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
95100
super();
96101
}
97102

103+
/**
104+
* Gets the current animation state both combining one of the possibilities from
105+
* SnackBarState and the vertical location.
106+
*/
107+
getAnimationState(): string {
108+
return `${this._animationState}-${this.snackBarConfig.verticalPosition}`;
109+
}
110+
98111
/** Attach a component portal as content to this snack bar container. */
99112
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
100113
if (this._portalHost.hasAttached()) {
@@ -109,6 +122,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
109122
}
110123
}
111124

125+
if (this.snackBarConfig.horizontalPosition === 'center') {
126+
this._renderer.addClass(this._elementRef.nativeElement, 'mat-snack-bar-center');
127+
}
128+
129+
if (this.snackBarConfig.verticalPosition === 'top') {
130+
this._renderer.addClass(this._elementRef.nativeElement, 'mat-snack-bar-top');
131+
}
132+
112133
return this._portalHost.attachComponentPortal(portal);
113134
}
114135

@@ -119,15 +140,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
119140

120141
/** Handle end of animations, updating the state of the snackbar. */
121142
onAnimationEnd(event: AnimationEvent) {
122-
if (event.toState === 'void' || event.toState === 'complete') {
143+
if (event.toState === 'void' || event.toState.startsWith('hidden')) {
123144
this._completeExit();
124145
}
125146

126-
if (event.toState === 'visible') {
147+
if (event.toState.startsWith('visible')) {
127148
// Note: we shouldn't use `this` inside the zone callback,
128149
// because it can cause a memory leak.
129150
const onEnter = this.onEnter;
130-
131151
this._ngZone.run(() => {
132152
onEnter.next();
133153
onEnter.complete();
@@ -137,18 +157,18 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
137157

138158
/** Begin animation of snack bar entrance into view. */
139159
enter(): void {
140-
this.animationState = 'visible';
160+
this._animationState = 'visible';
141161
}
142162

143163
/** Returns an observable resolving when the enter animation completes. */
144164
_onEnter(): Observable<void> {
145-
this.animationState = 'visible';
165+
this._animationState = 'visible';
146166
return this.onEnter.asObservable();
147167
}
148168

149169
/** Begin animation of the snack bar exiting from view. */
150170
exit(): Observable<void> {
151-
this.animationState = 'complete';
171+
this._animationState = 'hidden';
152172
return this._onExit();
153173
}
154174

0 commit comments

Comments
 (0)