diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index d0a14dfd475f..4a292c8f301b 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -29,6 +29,7 @@ import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-d import {SelectDemo} from './select/select-demo'; import {SliderDemo} from './slider/slider-demo'; import {SidenavDemo} from './sidenav/sidenav-demo'; +import {SnackBarDemo} from './snack-bar/snack-bar-demo'; import {PortalDemo, ScienceJoke} from './portal/portal-demo'; import {MenuDemo} from './menu/menu-demo'; import {TabsDemo} from './tabs/tab-group-demo'; @@ -61,6 +62,7 @@ import {TabsDemo} from './tabs/tab-group-demo'; LiveAnnouncerDemo, MdCheckboxDemoNestedChecklist, MenuDemo, + SnackBarDemo, OverlayDemo, PortalDemo, ProgressBarDemo, diff --git a/src/demo-app/demo-app/demo-app.html b/src/demo-app/demo-app/demo-app.html index ff618689e032..15084fbcdc9f 100644 --- a/src/demo-app/demo-app/demo-app.html +++ b/src/demo-app/demo-app/demo-app.html @@ -23,6 +23,7 @@ Sidenav Slider Slide Toggle + Snack Bar Tabs Toolbar Tooltip diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts index 03dbfd0ec3ac..b7c3fa47f3a7 100644 --- a/src/demo-app/demo-app/routes.ts +++ b/src/demo-app/demo-app/routes.ts @@ -26,6 +26,7 @@ import {MenuDemo} from '../menu/menu-demo'; import {RippleDemo} from '../ripple/ripple-demo'; import {DialogDemo} from '../dialog/dialog-demo'; import {TooltipDemo} from '../tooltip/tooltip-demo'; +import {SnackBarDemo} from '../snack-bar/snack-bar-demo'; export const DEMO_APP_ROUTES: Routes = [ @@ -56,4 +57,5 @@ export const DEMO_APP_ROUTES: Routes = [ {path: 'ripple', component: RippleDemo}, {path: 'dialog', component: DialogDemo}, {path: 'tooltip', component: TooltipDemo}, + {path: 'snack-bar', component: SnackBarDemo}, ]; diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html new file mode 100644 index 000000000000..22bd06ead17f --- /dev/null +++ b/src/demo-app/snack-bar/snack-bar-demo.html @@ -0,0 +1,13 @@ +

SnackBar demo

+
+
Message:
+
+ Show button + +
+
+ + \ No newline at end of file diff --git a/src/demo-app/snack-bar/snack-bar-demo.scss b/src/demo-app/snack-bar/snack-bar-demo.scss new file mode 100644 index 000000000000..87b6bb25d3b8 --- /dev/null +++ b/src/demo-app/snack-bar/snack-bar-demo.scss @@ -0,0 +1,3 @@ +.button-label-input { + display: inline-block; +} \ No newline at end of file diff --git a/src/demo-app/snack-bar/snack-bar-demo.ts b/src/demo-app/snack-bar/snack-bar-demo.ts new file mode 100644 index 000000000000..790d045ec6c8 --- /dev/null +++ b/src/demo-app/snack-bar/snack-bar-demo.ts @@ -0,0 +1,31 @@ +import {Component, ViewContainerRef} from '@angular/core'; +import {MdSnackBar, MdSnackBarConfig} from '@angular2-material/snack-bar'; + +@Component({ + moduleId: module.id, + selector: 'snack-bar-demo', + templateUrl: 'snack-bar-demo.html', +}) +export class SnackBarDemo { + message: string = 'Snack Bar opened.'; + actionButtonLabel: string = 'Retry'; + action: boolean = false; + + constructor( + public snackBar: MdSnackBar, + public viewContainerRef: ViewContainerRef) { } + + open() { + let config = new MdSnackBarConfig(this.viewContainerRef); + this.snackBar.open(this.message, this.action && this.actionButtonLabel, config); + } +} + + +@Component({ + moduleId: module.id, + selector: 'demo-snack', + templateUrl: 'snack-bar-demo.html', + styleUrls: ['./snack-bar-demo.css'], +}) +export class DemoSnack {} diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts index 1ad0109145b7..baa8a7f056cd 100644 --- a/src/demo-app/system-config.ts +++ b/src/demo-app/system-config.ts @@ -20,6 +20,7 @@ const components = [ 'sidenav', 'slider', 'slide-toggle', + 'snack-bar', 'button-toggle', 'tabs', 'toolbar', diff --git a/src/lib/all/all.ts b/src/lib/all/all.ts index 784156e2d7c5..f551ab454a6d 100644 --- a/src/lib/all/all.ts +++ b/src/lib/all/all.ts @@ -15,6 +15,7 @@ import {MdProgressCircleModule} from '@angular2-material/progress-circle'; import {MdProgressBarModule} from '@angular2-material/progress-bar'; import {MdInputModule} from '@angular2-material/input'; import {MdTabsModule} from '@angular2-material/tabs'; +import {MdSnackBarModule} from '@angular2-material/snack-bar'; import {MdToolbarModule} from '@angular2-material/toolbar'; import {MdTooltipModule} from '@angular2-material/tooltip'; import { @@ -47,6 +48,7 @@ const MATERIAL_MODULES = [ MdSidenavModule, MdSliderModule, MdSlideToggleModule, + MdSnackBarModule, MdTabsModule, MdToolbarModule, MdTooltipModule, @@ -81,6 +83,7 @@ const MATERIAL_MODULES = [ MdRadioModule.forRoot(), MdSliderModule.forRoot(), MdSlideToggleModule.forRoot(), + MdSnackBarModule.forRoot(), MdTooltipModule.forRoot(), OverlayModule.forRoot(), ], diff --git a/src/lib/snack-bar/base-snack-bar.ts b/src/lib/snack-bar/base-snack-bar.ts new file mode 100644 index 000000000000..ffbcb65bae7e --- /dev/null +++ b/src/lib/snack-bar/base-snack-bar.ts @@ -0,0 +1,12 @@ +import {MdSnackBarRef} from './snack-bar-ref'; + + +export class BaseSnackBarContent { + /** The instance of the component making up the content of the snack bar. */ + snackBarRef: MdSnackBarRef; + + /** Dismisses the snack bar. */ + dismiss(): void { + this.snackBarRef.dismiss(); + } +} diff --git a/src/lib/snack-bar/index.ts b/src/lib/snack-bar/index.ts new file mode 100644 index 000000000000..259852618083 --- /dev/null +++ b/src/lib/snack-bar/index.ts @@ -0,0 +1 @@ +export * from './snack-bar'; diff --git a/src/lib/snack-bar/package.json b/src/lib/snack-bar/package.json new file mode 100644 index 000000000000..be0d7e0de4fa --- /dev/null +++ b/src/lib/snack-bar/package.json @@ -0,0 +1,29 @@ +{ + "name": "@angular2-material/snack-bar", + "version": "2.0.0-alpha.8-1", + "description": "Angular 2 Material snack bar", + "main": "./snack-bar.umd.js", + "module": "./index.js", + "typings": "./index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/angular/material2.git" + }, + "keywords": [ + "angular", + "material", + "material design", + "components", + "snackbar", + "toast", + "notification" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/material2/issues" + }, + "homepage": "https://github.com/angular/material2#readme", + "peerDependencies": { + "@angular2-material/core": "2.0.0-alpha.8-1" + } +} diff --git a/src/lib/snack-bar/simple-snack-bar.html b/src/lib/snack-bar/simple-snack-bar.html new file mode 100644 index 000000000000..772bde96d7c5 --- /dev/null +++ b/src/lib/snack-bar/simple-snack-bar.html @@ -0,0 +1,2 @@ +{{message}} + \ No newline at end of file diff --git a/src/lib/snack-bar/simple-snack-bar.scss b/src/lib/snack-bar/simple-snack-bar.scss new file mode 100644 index 000000000000..52b99f292b58 --- /dev/null +++ b/src/lib/snack-bar/simple-snack-bar.scss @@ -0,0 +1,28 @@ +:host { + display: flex; + justify-content: space-between; + + span { + box-sizing: border-box; + border: none; + color: white; + font-family: Roboto, 'Helvetica Neue', sans-serif; + font-size: 14px; + line-height: 20px; + outline: none; + text-decoration: none; + word-break: break-all; + } + + button { + box-sizing: border-box; + color: white; + float: right; + font-weight: 600; + line-height: 20px; + margin: -5px 0 0 48px; + min-width: initial; + padding: 5px; + text-transform: uppercase; + } +} \ No newline at end of file diff --git a/src/lib/snack-bar/simple-snack-bar.ts b/src/lib/snack-bar/simple-snack-bar.ts new file mode 100644 index 000000000000..542cbe08f586 --- /dev/null +++ b/src/lib/snack-bar/simple-snack-bar.ts @@ -0,0 +1,20 @@ +import {Component} from '@angular/core'; +import {BaseSnackBarContent} from './base-snack-bar'; + + +@Component({ + moduleId: module.id, + selector: 'simple-snack-bar', + templateUrl: 'simple-snack-bar.html', + styleUrls: ['simple-snack-bar.css'], +}) +export class SimpleSnackBar extends BaseSnackBarContent { + /** The message to be shown in the snack bar. */ + message: string; + + /** The label for the button in the snack bar. */ + action: string; + + /** If the action button should be shown. */ + get hasAction(): boolean { return !!this.action; } +} diff --git a/src/lib/snack-bar/snack-bar-config.ts b/src/lib/snack-bar/snack-bar-config.ts new file mode 100644 index 000000000000..9225964d33a6 --- /dev/null +++ b/src/lib/snack-bar/snack-bar-config.ts @@ -0,0 +1,16 @@ +import {ViewContainerRef} from '@angular/core'; + + +export type SnackBarRole = 'alert' | 'polite'; + +export class MdSnackBarConfig { + /** The aria-role of the snack bar. */ + role: SnackBarRole = 'alert'; + + /** The view container to place the overlay for the snack bar into. */ + viewContainerRef: ViewContainerRef; + + constructor(viewContainerRef: ViewContainerRef) { + this.viewContainerRef = viewContainerRef; + } +} diff --git a/src/lib/snack-bar/snack-bar-container.html b/src/lib/snack-bar/snack-bar-container.html new file mode 100644 index 000000000000..23e1d44627f8 --- /dev/null +++ b/src/lib/snack-bar/snack-bar-container.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/snack-bar/snack-bar-container.scss b/src/lib/snack-bar/snack-bar-container.scss new file mode 100644 index 000000000000..2a47c390795b --- /dev/null +++ b/src/lib/snack-bar/snack-bar-container.scss @@ -0,0 +1,19 @@ +@import '../core/style/elevation'; + +$md-snack-bar-padding: 14px 24px !default; +$md-snack-bar-height: 20px !default; +$md-snack-bar-min-width: 288px !default; +$md-snack-bar-max-width: 568px !default; + + +:host { + background: #323232; + border-radius: 2px; + display: block; + height: $md-snack-bar-height; + @include md-elevation(24); + max-width: $md-snack-bar-max-width; + min-width: $md-snack-bar-min-width; + overflow: hidden; + padding: $md-snack-bar-padding; +} \ No newline at end of file diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts new file mode 100644 index 000000000000..2de010fc3a5d --- /dev/null +++ b/src/lib/snack-bar/snack-bar-container.ts @@ -0,0 +1,46 @@ +import { + Component, + ComponentRef, + ViewChild +} from '@angular/core'; +import { + BasePortalHost, + ComponentPortal, + TemplatePortal, + PortalHostDirective} from '@angular2-material/core'; +import {MdSnackBarConfig} from './snack-bar-config'; +import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors'; + + +/** + * Internal component that wraps user-provided snack bar content. + */ +@Component({ + moduleId: module.id, + selector: 'snack-bar-content', + templateUrl: 'snack-bar-container.html', + styleUrls: ['snack-bar-container.css'], + host: { + '[attr.role]': 'snackBarConfig?.role' + } +}) +export class MdSnackBarContainer extends BasePortalHost { + /** The portal host inside of this container into which the snack bar content will be loaded. */ + @ViewChild(PortalHostDirective) private _portalHost: PortalHostDirective; + + /** The snack bar configuration. */ + snackBarConfig: MdSnackBarConfig; + + /** Attach a portal as content to this snack bar container. */ + attachComponentPortal(portal: ComponentPortal): ComponentRef { + if (this._portalHost.hasAttached()) { + throw new MdSnackBarContentAlreadyAttached(); + } + + return this._portalHost.attachComponentPortal(portal); + } + + attachTemplatePortal(portal: TemplatePortal): Map { + throw Error('Not yet implemented'); + } +} diff --git a/src/lib/snack-bar/snack-bar-errors.ts b/src/lib/snack-bar/snack-bar-errors.ts new file mode 100644 index 000000000000..cb8bbe2b5723 --- /dev/null +++ b/src/lib/snack-bar/snack-bar-errors.ts @@ -0,0 +1,8 @@ +import {MdError} from '@angular2-material/core'; + + +export class MdSnackBarContentAlreadyAttached extends MdError { + constructor() { + super('Attempting to attach snack bar content after content is already attached'); + } +} diff --git a/src/lib/snack-bar/snack-bar-ref.ts b/src/lib/snack-bar/snack-bar-ref.ts new file mode 100644 index 000000000000..4f1581fdd972 --- /dev/null +++ b/src/lib/snack-bar/snack-bar-ref.ts @@ -0,0 +1,41 @@ +import {OverlayRef} from '@angular2-material/core'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + +// TODO(josephperrott): Implement onAction observable. + + +/** + * Reference to a snack bar dispatched from the snack bar service. + */ +export class MdSnackBarRef { + /** The instance of the component making up the content of the snack bar. */ + readonly instance: T|any; + + /** Subject for notifying the user that the snack bar has closed. */ + private _afterClosed: Subject = new Subject(); + + /** If the snack bar is active. */ + private _isActive: boolean = true; + + constructor(instance: T|any, private _overlayRef: OverlayRef) { + // Sets the readonly instance of the snack bar content component. + this.instance = instance; + this.afterDismissed().subscribe(null, null, () => { this._isActive = false; }); + } + + + /** Dismisses the snack bar. */ + dismiss(): void { + if (this._isActive) { + this._overlayRef.dispose(); + this._afterClosed.complete(); + } + } + + + /** Gets an observable that is notified when the snack bar is finished closing. */ + afterDismissed(): Observable { + return this._afterClosed.asObservable(); + } +} diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts new file mode 100644 index 000000000000..544c48394e37 --- /dev/null +++ b/src/lib/snack-bar/snack-bar.ts @@ -0,0 +1,124 @@ +import { + NgModule, + ModuleWithProviders, + Injectable, + ComponentRef, +} from '@angular/core'; +import { + Overlay, + OverlayState, + OverlayRef, + ComponentType, + ComponentPortal, + OverlayModule, + PortalModule, + OVERLAY_PROVIDERS, +} from '@angular2-material/core'; +import {CommonModule} from '@angular/common'; +import {MdSnackBarConfig} from './snack-bar-config'; +import {MdSnackBarRef} from './snack-bar-ref'; +import {MdSnackBarContainer} from './snack-bar-container'; +import {SimpleSnackBar} from './simple-snack-bar'; + +export {MdSnackBarRef} from './snack-bar-ref'; +export {MdSnackBarConfig} from './snack-bar-config'; + +// TODO(josephperrott): Animate entrance and exit of snack bars. +// TODO(josephperrott): Automate dismiss after timeout. + + +/** + * Service to dispatch Material Design snack bar messages. + */ +@Injectable() +export class MdSnackBar { + /** A reference to the current snack bar in the view. */ + private _snackBarRef: MdSnackBarRef; + + constructor(private _overlay: Overlay) {} + + /** + * Creates and dispatches a snack bar with a custom component for the content, removing any + * currently opened snack bars. + */ + openFromComponent(component: ComponentType, + config: MdSnackBarConfig): MdSnackBarRef { + if (this._snackBarRef) { + this._snackBarRef.dismiss(); + } + let overlayRef = this._createOverlay(); + let snackBarContainer = this._attachSnackBarContainer(overlayRef, config); + + return this._fillSnackBarContainer(component, snackBarContainer, overlayRef); + } + + + /** + * Creates and dispatches a snack bar. + */ + open(message: string, buttonLabel: string, + config: MdSnackBarConfig): MdSnackBarRef { + let simpleSnackBar = this.openFromComponent(SimpleSnackBar, config); + simpleSnackBar.instance.message = message; + simpleSnackBar.instance.action = buttonLabel; + return simpleSnackBar; + } + + + /** + * Attaches the snack bar container component to the overlay. + */ + private _attachSnackBarContainer(overlayRef: OverlayRef, + config: MdSnackBarConfig): MdSnackBarContainer { + let containerPortal = new ComponentPortal(MdSnackBarContainer, config.viewContainerRef); + let containerRef: ComponentRef = overlayRef.attach(containerPortal); + containerRef.instance.snackBarConfig = config; + + return containerRef.instance; + } + + + /** + * Places a new component as the content of the snack bar container. + */ + private _fillSnackBarContainer(component: ComponentType, + container: MdSnackBarContainer, + overlayRef: OverlayRef): MdSnackBarRef { + let portal = new ComponentPortal(component); + let contentRef = container.attachComponentPortal(portal); + let snackBarRef = > new MdSnackBarRef(contentRef.instance, overlayRef); + snackBarRef.instance.snackBarRef = snackBarRef; + + this._snackBarRef = snackBarRef; + return snackBarRef; + } + + + /** + * Creates a new overlay and places it in the correct location. + */ + private _createOverlay(): OverlayRef { + let state = new OverlayState(); + state.positionStrategy = this._overlay.position().global() + .fixed() + .centerHorizontally() + .bottom('0px'); + return this._overlay.create(state); + } +} + + +@NgModule({ + imports: [OverlayModule, PortalModule, CommonModule], + exports: [MdSnackBarContainer], + declarations: [MdSnackBarContainer, SimpleSnackBar], + entryComponents: [MdSnackBarContainer, SimpleSnackBar], +}) +export class MdSnackBarModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdSnackBarModule, + providers: [MdSnackBar, OVERLAY_PROVIDERS] + }; + } +} diff --git a/src/lib/system-config-spec.ts b/src/lib/system-config-spec.ts index 0490942f8fd9..42d17ab29500 100644 --- a/src/lib/system-config-spec.ts +++ b/src/lib/system-config-spec.ts @@ -20,6 +20,7 @@ const components = [ 'sidenav', 'slider', 'slide-toggle', + 'snack-bar', 'button-toggle', 'tabs', 'toolbar',