From 5f9637535a3de555ea2650b7fc7563672362eb0b Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Tue, 11 Apr 2017 18:47:36 -0700 Subject: [PATCH] Allow snackbar position on the screen. --- src/demo-app/snack-bar/snack-bar-demo.html | 15 +- src/demo-app/snack-bar/snack-bar-demo.ts | 15 +- src/lib/snack-bar/snack-bar-config.ts | 12 + src/lib/snack-bar/snack-bar-container.scss | 20 +- src/lib/snack-bar/snack-bar-container.ts | 45 +++- src/lib/snack-bar/snack-bar.spec.ts | 284 +++++++++++++++++++-- src/lib/snack-bar/snack-bar.ts | 29 ++- 7 files changed, 379 insertions(+), 41 deletions(-) diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html index 7e1a56e0e35f..4d0634e59b74 100644 --- a/src/demo-app/snack-bar/snack-bar-demo.html +++ b/src/demo-app/snack-bar/snack-bar-demo.html @@ -3,6 +3,20 @@

SnackBar demo

Message:
+
+
Position in page:
+ + Start + End + Left + Right + Center + + + Top + Bottom + +

Show button on snack bar

@@ -27,7 +41,6 @@

SnackBar demo

-

Add extra class to container

diff --git a/src/demo-app/snack-bar/snack-bar-demo.ts b/src/demo-app/snack-bar/snack-bar-demo.ts index d61bed273a58..6f905ce052ba 100644 --- a/src/demo-app/snack-bar/snack-bar-demo.ts +++ b/src/demo-app/snack-bar/snack-bar-demo.ts @@ -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, @@ -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); } } diff --git a/src/lib/snack-bar/snack-bar-config.ts b/src/lib/snack-bar/snack-bar-config.ts index 85f02041e42e..4f0f2c1fa9cc 100644 --- a/src/lib/snack-bar/snack-bar-config.ts +++ b/src/lib/snack-bar/snack-bar-config.ts @@ -12,6 +12,12 @@ import {Direction} from '@angular/cdk/bidi'; export const MD_SNACK_BAR_DATA = new InjectionToken('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. */ @@ -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'; } diff --git a/src/lib/snack-bar/snack-bar-container.scss b/src/lib/snack-bar/snack-bar-container.scss index 0ffe0b93b791..164040d9b079 100644 --- a/src/lib/snack-bar/snack-bar-container.scss +++ b/src/lib/snack-bar/snack-bar-container.scss @@ -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; } diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts index e0acfc2b1766..1c8c1f884006 100644 --- a/src/lib/snack-bar/snack-bar-container.ts +++ b/src/lib/snack-bar/snack-bar-container.ts @@ -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). @@ -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)), ]) ], }) @@ -85,7 +92,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy { _onEnter: Subject = new Subject(); /** The state of the snack bar animations. */ - animationState: SnackBarState = 'initial'; + private _animationState: SnackBarState; /** The snack bar configuration. */ snackBarConfig: MdSnackBarConfig; @@ -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}`; + } + /** Attach a component portal as content to this snack bar container. */ attachComponentPortal(portal: ComponentPortal): ComponentRef { if (this._portalHost.hasAttached()) { @@ -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); } @@ -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; @@ -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 { - this.animationState = 'complete'; + this._animationState = 'hidden'; return this._onExit; } diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts index 2d57d390f381..fdd1da844e82 100644 --- a/src/lib/snack-bar/snack-bar.spec.ts +++ b/src/lib/snack-bar/snack-bar.spec.ts @@ -22,8 +22,6 @@ import { } from './index'; -// TODO(josephperrott): Update tests to mock waiting for time to complete for animations. - describe('MdSnackBar', () => { let snackBar: MdSnackBar; let liveAnnouncer: LiveAnnouncer; @@ -66,7 +64,7 @@ describe('MdSnackBar', () => { }); it('should have the role of alert', () => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; snackBar.open(simpleMessage, simpleActionLabel, config); let containerElement = overlayContainerElement.querySelector('snack-bar-container')!; @@ -92,7 +90,7 @@ describe('MdSnackBar', () => { })); it('should open a simple message with a button', () => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, simpleActionLabel, config); viewContainerFixture.detectChanges(); @@ -100,7 +98,8 @@ describe('MdSnackBar', () => { expect(snackBarRef.instance instanceof SimpleSnackBar) .toBe(true, 'Expected the snack bar content component to be SimpleSnackBar'); expect(snackBarRef.instance.snackBarRef) - .toBe(snackBarRef, 'Expected the snack bar reference to be placed in the component instance'); + .toBe(snackBarRef, + 'Expected the snack bar reference to be placed in the component instance'); let messageElement = overlayContainerElement.querySelector('snack-bar-container')!; expect(messageElement.textContent) @@ -115,7 +114,7 @@ describe('MdSnackBar', () => { }); it('should open a simple message with no button', () => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, undefined, config); viewContainerFixture.detectChanges(); @@ -133,7 +132,7 @@ describe('MdSnackBar', () => { }); it('should dismiss the snack bar and remove itself from the view', async(() => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let dismissObservableCompleted = false; let snackBarRef = snackBar.open(simpleMessage, undefined, config); @@ -183,33 +182,38 @@ describe('MdSnackBar', () => { })); it('should set the animation state to visible on entry', () => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, undefined, config); viewContainerFixture.detectChanges(); - expect(snackBarRef.containerInstance.animationState) - .toBe('visible', `Expected the animation state would be 'visible'.`); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`); + snackBarRef.dismiss(); + + viewContainerFixture.detectChanges(); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('hidden-bottom', `Expected the animation state would be 'hidden-bottom'.`); }); it('should set the animation state to complete on exit', () => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, undefined, config); snackBarRef.dismiss(); viewContainerFixture.detectChanges(); - expect(snackBarRef.containerInstance.animationState) - .toBe('complete', `Expected the animation state would be 'complete'.`); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('hidden-bottom', `Expected the animation state would be 'hidden-bottom'.`); }); it(`should set the old snack bar animation state to complete and the new snack bar animation state to visible on entry of new snack bar`, async(() => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, undefined, config); let dismissObservableCompleted = false; viewContainerFixture.detectChanges(); - expect(snackBarRef.containerInstance.animationState) - .toBe('visible', `Expected the animation state would be 'visible'.`); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`); let config2 = {viewContainerRef: testViewContainerRef}; let snackBarRef2 = snackBar.open(simpleMessage, undefined, config2); @@ -221,16 +225,17 @@ describe('MdSnackBar', () => { viewContainerFixture.whenStable().then(() => { expect(dismissObservableCompleted).toBe(true); - expect(snackBarRef.containerInstance.animationState) - .toBe('complete', `Expected the animation state would be 'complete'.`); - expect(snackBarRef2.containerInstance.animationState) - .toBe('visible', `Expected the animation state would be 'visible'.`); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('hidden-bottom', `Expected the animation state would be 'hidden-bottom'.`); + expect(snackBarRef2.containerInstance.getAnimationState()) + .toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`); }); })); it('should open a new snackbar after dismissing a previous snackbar', async(() => { - let config = {viewContainerRef: testViewContainerRef}; + let config: MdSnackBarConfig = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, 'Dismiss', config); + viewContainerFixture.detectChanges(); snackBarRef.dismiss(); @@ -243,7 +248,8 @@ describe('MdSnackBar', () => { // Wait for the snackbar open animation to finish. viewContainerFixture.whenStable().then(() => { - expect(snackBarRef.containerInstance.animationState).toBe('visible'); + expect(snackBarRef.containerInstance.getAnimationState()) + .toBe('visible-bottom', `Expected the animation state would be 'visible-bottom'.`); }); }); })); @@ -506,6 +512,240 @@ describe('MdSnackBar with parent MdSnackBar', () => { })); }); + +describe('MdSnackBar Positioning', () => { + let snackBar: MdSnackBar; + let liveAnnouncer: LiveAnnouncer; + let overlayContainerEl: HTMLElement; + + let testViewContainerRef: ViewContainerRef; + let viewContainerFixture: ComponentFixture; + + let simpleMessage = 'Burritos are here!'; + let simpleActionLabel = 'pickup'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdSnackBarModule, SnackBarTestModule, NoopAnimationsModule], + providers: [ + {provide: OverlayContainer, useFactory: () => { + overlayContainerEl = document.createElement('div'); + return {getContainerElement: () => overlayContainerEl}; + }} + ], + }); + TestBed.compileComponents(); + })); + + beforeEach(inject([MdSnackBar, LiveAnnouncer], (sb: MdSnackBar, la: LiveAnnouncer) => { + snackBar = sb; + liveAnnouncer = la; + })); + + afterEach(() => { + overlayContainerEl.innerHTML = ''; + liveAnnouncer.ngOnDestroy(); + }); + + beforeEach(() => { + viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer); + + viewContainerFixture.detectChanges(); + testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer; + }); + + it('should default to bottom center', () => { + let config: MdSnackBarConfig = {}; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeTruthy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeFalsy(); + expect(overlayPaneEl.style.marginBottom).toBe('0px', 'Expected margin-bottom to be "0px"'); + expect(overlayPaneEl.style.marginTop).toBe('', 'Expected margin-top to be ""'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should be in the bottom left corner', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'bottom', + horizontalPosition: 'left' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeFalsy(); + expect(overlayPaneEl.style.marginBottom).toBe('0px', 'Expected margin-bottom to be "0px"'); + expect(overlayPaneEl.style.marginTop).toBe('', 'Expected margin-top to be ""'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('0px', 'Expected margin-left to be "0px"'); + }); + + it('should be in the bottom right corner', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'bottom', + horizontalPosition: 'right' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeFalsy(); + expect(overlayPaneEl.style.marginBottom).toBe('0px', 'Expected margin-bottom to be "0px"'); + expect(overlayPaneEl.style.marginTop).toBe('', 'Expected margin-top to be ""'); + expect(overlayPaneEl.style.marginRight).toBe('0px', 'Expected margin-right to be "0px"'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should be in the bottom center', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'bottom', + horizontalPosition: 'center' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeTruthy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeFalsy(); + expect(overlayPaneEl.style.marginBottom).toBe('0px', 'Expected margin-bottom to be "0px"'); + expect(overlayPaneEl.style.marginTop).toBe('', 'Expected margin-top to be ""'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should be in the top left corner', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'left' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('0px', 'Expected margin-left to be "0px"'); + }); + + it('should be in the top right corner', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'right' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('0px', 'Expected margin-right to be "0px"'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should be in the top center', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'center' + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeTruthy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should handle start based on direction (rtl)', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'start', + direction: 'rtl', + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('0px', 'Expected margin-right to be "0px"'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + + it('should handle start based on direction (ltr)', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'start', + direction: 'ltr', + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('0px', 'Expected margin-left to be "0px"'); + }); + + + it('should handle end based on direction (rtl)', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'end', + direction: 'rtl', + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('', 'Expected margin-right to be ""'); + expect(overlayPaneEl.style.marginLeft).toBe('0px', 'Expected margin-left to be "0px"'); + }); + + it('should handle end based on direction (ltr)', () => { + let config: MdSnackBarConfig = { + verticalPosition: 'top', + horizontalPosition: 'end', + direction: 'ltr', + }; + snackBar.open(simpleMessage, simpleActionLabel, config); + let containerEl = overlayContainerEl.querySelector('snack-bar-container') as HTMLElement; + let overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement; + + expect(containerEl.classList.contains('mat-snack-bar-center')).toBeFalsy(); + expect(containerEl.classList.contains('mat-snack-bar-top')).toBeTruthy(); + expect(overlayPaneEl.style.marginBottom).toBe('', 'Expected margin-bottom to be ""'); + expect(overlayPaneEl.style.marginTop).toBe('0px', 'Expected margin-top to be "0px"'); + expect(overlayPaneEl.style.marginRight).toBe('0px', 'Expected margin-right to be "0px"'); + expect(overlayPaneEl.style.marginLeft).toBe('', 'Expected margin-left to be ""'); + }); + +}); + + @Directive({selector: 'dir-with-view-container'}) class DirectiveWithViewContainer { constructor(public viewContainerRef: ViewContainerRef) { } diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts index 8c2b89799024..8696895c04ad 100644 --- a/src/lib/snack-bar/snack-bar.ts +++ b/src/lib/snack-bar/snack-bar.ts @@ -153,11 +153,32 @@ export class MdSnackBar { * @param config The user-specified snack bar config. */ private _createOverlay(config: MdSnackBarConfig): OverlayRef { - const state = new OverlayState({ - direction: config.direction, - positionStrategy: this._overlay.position().global().centerHorizontally().bottom('0') - }); + const state = new OverlayState(); + state.direction = config.direction; + + let positionStrategy = this._overlay.position().global(); + // Set horizontal position. + const isRtl = config.direction === 'rtl'; + const isLeft = ( + config.horizontalPosition === 'left' || + (config.horizontalPosition === 'start' && !isRtl) || + (config.horizontalPosition === 'end' && isRtl)); + const isRight = !isLeft && config.horizontalPosition !== 'center'; + if (isLeft) { + positionStrategy.left('0'); + } else if (isRight) { + positionStrategy.right('0'); + } else { + positionStrategy.centerHorizontally(); + } + // Set horizontal position. + if (config.verticalPosition === 'top') { + positionStrategy.top('0'); + } else { + positionStrategy.bottom('0'); + } + state.positionStrategy = positionStrategy; return this._overlay.create(state); }