Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snackbar): allow snack bar to be positioned on the page #4045

Merged
merged 1 commit into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/demo-app/snack-bar/snack-bar-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ <h1>SnackBar demo</h1>
<div>
Message: <md-form-field><input mdInput type="text" [(ngModel)]="message"></md-form-field>
</div>
<div>
<div>Position in page: </div>
<md-select [(ngModel)]="horizontalPosition">
<md-option value="start">Start</md-option>
<md-option value="end">End</md-option>
<md-option value="left">Left</md-option>
<md-option value="right">Right</md-option>
<md-option value="center">Center</md-option>
</md-select>
<md-select [(ngModel)]="verticalPosition">
<md-option value="top">Top</md-option>
<md-option value="bottom">Bottom</md-option>
</md-select>
</div>
<div>
<md-checkbox [(ngModel)]="action">
<p *ngIf="!action">Show button on snack bar</p>
Expand All @@ -27,7 +41,6 @@ <h1>SnackBar demo</h1>
</md-form-field>
</md-checkbox>
</div>

<p>
<md-checkbox [(ngModel)]="addExtraClass">Add extra class to container</md-checkbox>
</p>
Expand Down
15 changes: 13 additions & 2 deletions src/demo-app/snack-bar/snack-bar-demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {MdSnackBar, MdSnackBarConfig} from '@angular/material';
import {
MdSnackBar,
MdSnackBarConfig,
MdSnackBarHorizontalPosition,
MdSnackBarVerticalPosition,
Dir,
} from '@angular/material';

@Component({
moduleId: module.id,
Expand All @@ -15,13 +21,18 @@ export class SnackBarDemo {
setAutoHide: boolean = true;
autoHide: number = 10000;
addExtraClass: boolean = false;
horizontalPosition: MdSnackBarHorizontalPosition = 'center';
verticalPosition: MdSnackBarVerticalPosition = 'bottom';

constructor(public snackBar: MdSnackBar) { }
constructor(public snackBar: MdSnackBar, private dir: Dir) { }

open() {
let config = new MdSnackBarConfig();
config.verticalPosition = this.verticalPosition;
config.horizontalPosition = this.horizontalPosition;
config.duration = this.autoHide;
config.extraClasses = this.addExtraClass ? ['party'] : undefined;
config.direction = this.dir.value;
this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
}
}
12 changes: 12 additions & 0 deletions src/lib/snack-bar/snack-bar-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import {Direction} from '@angular/cdk/bidi';

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

/** Possible values for horizontalPosition on MdSnackBarConfig. */
export type MdSnackBarHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';

/** Possible values for verticalPosition on MdSnackBarConfig. */
export type MdSnackBarVerticalPosition = 'top' | 'bottom';

/**
* Configuration used when opening a snack-bar.
*/
Expand All @@ -36,4 +42,10 @@ export class MdSnackBarConfig {

/** Data being injected into the child component. */
data?: any = null;

/** The horizontal position to place the snack bar. */
horizontalPosition?: MdSnackBarHorizontalPosition = 'center';

/** The vertical position to place the snack bar. */
verticalPosition?: MdSnackBarVerticalPosition = 'bottom';
}
20 changes: 19 additions & 1 deletion src/lib/snack-bar/snack-bar-container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,36 @@
$mat-snack-bar-padding: 14px 24px !default;
$mat-snack-bar-min-width: 288px !default;
$mat-snack-bar-max-width: 568px !default;
$mat-snack-bar-spacing-margin: 24px !default;


.mat-snack-bar-container {
border-radius: 2px;
box-sizing: content-box;
display: block;
margin: $mat-snack-bar-spacing-margin;
max-width: $mat-snack-bar-max-width;
min-width: $mat-snack-bar-min-width;
padding: $mat-snack-bar-padding;
// Initial transformation is applied to start snack bar out of view.
// Initial transformation is applied to start snack bar out of view, below its target position.
transform: translateY(100%);

/**
* Removes margin of snack bars which are center positioned horizontally. This
* is done to align snack bars to the edge of the view vertically to match spec.
*/
&.mat-snack-bar-center {
margin: 0;
}

/**
* To allow for animations from a 'top' vertical position to animate in a downward
* direction, set the translation to start the snack bar above the target position.
*/
&.mat-snack-bar-top {
transform: translateY(-100%);
}

@include cdk-high-contrast {
border: solid 1px;
}
Expand Down
45 changes: 34 additions & 11 deletions src/lib/snack-bar/snack-bar-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {Subject} from 'rxjs/Subject';
import {MdSnackBarConfig} from './snack-bar-config';


export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void';
export type SnackBarState = 'visible' | 'hidden' | 'void';

// TODO(jelbourn): we can't use constants from animation.ts here because you can't use
// a text interpolation in anything that is analyzed statically with ngc (for AoT compile).
Expand All @@ -59,15 +59,22 @@ export const HIDE_ANIMATION = '195ms cubic-bezier(0.0,0.0,0.2,1)';
host: {
'role': 'alert',
'class': 'mat-snack-bar-container',
'[@state]': 'animationState',
'[@state]': 'getAnimationState()',
'(@state.done)': 'onAnimationEnd($event)'
},
animations: [
trigger('state', [
state('void, initial, complete', style({transform: 'translateY(100%)'})),
state('visible', style({transform: 'translateY(0%)'})),
transition('visible => complete', animate(HIDE_ANIMATION)),
transition('initial => visible, void => visible', animate(SHOW_ANIMATION)),
// Animation from top.
state('visible-top', style({transform: 'translateY(0%)'})),
state('hidden-top', style({transform: 'translateY(-100%)'})),
transition('visible-top => hidden-top', animate(HIDE_ANIMATION)),
transition('void => visible-top', animate(SHOW_ANIMATION)),
// Animation from bottom.
state('visible-bottom', style({transform: 'translateY(0%)'})),
state('hidden-bottom', style({transform: 'translateY(100%)'})),
transition('visible-bottom => hidden-bottom', animate(HIDE_ANIMATION)),
transition('void => visible-bottom',
animate(SHOW_ANIMATION)),
])
],
})
Expand All @@ -85,7 +92,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
_onEnter: Subject<any> = new Subject();

/** The state of the snack bar animations. */
animationState: SnackBarState = 'initial';
private _animationState: SnackBarState;

/** The snack bar configuration. */
snackBarConfig: MdSnackBarConfig;
Expand All @@ -98,6 +105,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
super();
}

/**
* Gets the current animation state both combining one of the possibilities from
* SnackBarState and the vertical location.
*/
getAnimationState(): string {
return `${this._animationState}-${this.snackBarConfig.verticalPosition}`;
Copy link
Contributor

@kara kara Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes this is called when this.snackBarConfig is undefined, so tests are failing with

Cannot read property 'verticalPosition' of undefined

Add a check?

}

/** Attach a component portal as content to this snack bar container. */
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
if (this._portalHost.hasAttached()) {
Expand All @@ -112,6 +127,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
}
}

if (this.snackBarConfig.horizontalPosition === 'center') {
this._renderer.addClass(this._elementRef.nativeElement, 'mat-snack-bar-center');
}

if (this.snackBarConfig.verticalPosition === 'top') {
this._renderer.addClass(this._elementRef.nativeElement, 'mat-snack-bar-top');
}

return this._portalHost.attachComponentPortal(portal);
}

Expand All @@ -122,11 +145,11 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {

/** Handle end of animations, updating the state of the snackbar. */
onAnimationEnd(event: AnimationEvent) {
if (event.toState === 'void' || event.toState === 'complete') {
if (event.toState === 'void' || event.toState.startsWith('hidden')) {
this._completeExit();
}

if (event.toState === 'visible') {
if (event.toState.startsWith('visible')) {
// Note: we shouldn't use `this` inside the zone callback,
// because it can cause a memory leak.
const onEnter = this._onEnter;
Expand All @@ -141,14 +164,14 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy {
/** Begin animation of snack bar entrance into view. */
enter(): void {
if (!this._destroyed) {
this.animationState = 'visible';
this._animationState = 'visible';
this._changeDetectorRef.detectChanges();
}
}

/** Begin animation of the snack bar exiting from view. */
exit(): Observable<void> {
this.animationState = 'complete';
this._animationState = 'hidden';
return this._onExit;
}

Expand Down
Loading