diff --git a/ionic/components.core.scss b/ionic/components.core.scss index 7252f4a8ad6..e3a102a431e 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -26,6 +26,7 @@ "components/show-hide-when/show-hide-when", "components/slides/slides", "components/spinner/spinner", + "components/toast/toast", "components/virtual-scroll/virtual-scroll"; diff --git a/ionic/components.ios.scss b/ionic/components.ios.scss index aa02af24f17..b37c65aff0c 100644 --- a/ionic/components.ios.scss +++ b/ionic/components.ios.scss @@ -27,6 +27,7 @@ "components/select/select.ios", "components/tabs/tabs.ios", "components/toggle/toggle.ios", + "components/toast/toast.ios", "components/toolbar/toolbar.ios"; diff --git a/ionic/components.md.scss b/ionic/components.md.scss index 91911f4c626..4515f65aa16 100644 --- a/ionic/components.md.scss +++ b/ionic/components.md.scss @@ -27,6 +27,7 @@ "components/select/select.md", "components/tabs/tabs.md", "components/toggle/toggle.md", + "components/toast/toast.md", "components/toolbar/toolbar.md"; diff --git a/ionic/components.ts b/ionic/components.ts index 86455e54bb6..a961e2ab846 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -47,5 +47,6 @@ export * from './components/tabs/tabs' export * from './components/tabs/tab' export * from './components/tap-click/tap-click' export * from './components/toggle/toggle' +export * from './components/toast/toast' export * from './components/toolbar/toolbar' export * from './components/virtual-scroll/virtual-scroll' diff --git a/ionic/components/toast/test/basic/e2e.ts b/ionic/components/toast/test/basic/e2e.ts new file mode 100644 index 00000000000..cffef5a0be7 --- /dev/null +++ b/ionic/components/toast/test/basic/e2e.ts @@ -0,0 +1,8 @@ + +it('should open action sheet', function() { + element(by.css('.e2eOpenActionSheet')).click(); +}); + +it('should close with backdrop click', function() { + element(by.css('.backdrop')).click(); +}); diff --git a/ionic/components/toast/test/basic/index.ts b/ionic/components/toast/test/basic/index.ts new file mode 100644 index 00000000000..b6341523ffa --- /dev/null +++ b/ionic/components/toast/test/basic/index.ts @@ -0,0 +1,78 @@ +import {App, Page, Toast, NavController, Platform} from 'ionic-angular'; + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + constructor( + private nav: NavController, + private platform: Platform) + {} + + showToast() { + const toast = Toast.create({ + message: 'User was created successfully', + }); + + this.nav.present(toast); + } + + showLongToast() { + const toast = Toast.create({ + message: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea voluptatibus quibusdam eum nihil optio, ullam accusamus magni, nobis suscipit reprehenderit, sequi quam amet impedit. Accusamus dolorem voluptates laborum dolor obcaecati.', + }); + + this.nav.present(toast); + } + + showDismissDurationToast() { + const toast = Toast.create({ + message: 'I am dismissed after 1.5 seconds', + duration: 1500 + }); + + this.nav.present(toast); + } + + showToastWithCloseButton() { + const toast = Toast.create({ + message: 'Your internet connection appears to be offline. Data integrity is not gauranteed.', + showCloseButton: true, + closeButtonText: 'Ok' + }); + + this.nav.present(toast); + } + +} + +@Page({ + template: ` + + + + + Toast + + + Hi, I'm Bob, and I'm a modal. + + ` +}) +class ToastPage { + constructor(private viewCtrl: ViewController) { } + + dismiss() { + this.viewCtrl.dismiss(); + } +} + + +@App({ + template: '' +}) +class E2EApp { + constructor() { + this.root = E2EPage; + } +} diff --git a/ionic/components/toast/test/basic/main.html b/ionic/components/toast/test/basic/main.html new file mode 100644 index 00000000000..f6407aa6d20 --- /dev/null +++ b/ionic/components/toast/test/basic/main.html @@ -0,0 +1,11 @@ + + Toasts + + + + + +
+ + +
diff --git a/ionic/components/toast/toast.ios.scss b/ionic/components/toast/toast.ios.scss new file mode 100644 index 00000000000..1c6badde01e --- /dev/null +++ b/ionic/components/toast/toast.ios.scss @@ -0,0 +1,39 @@ +// iOS Toast +// -------------------------------------------------- +$toast-ios-text-align: left !default; +$toast-ios-background: rgba(0, 0, 0, 0.70) !default; +$toast-ios-border-radius: 0.65rem !default; + +$toast-ios-title-color: #fff !default; +$toast-ios-title-font-size: 1.4rem !default; +$toast-ios-title-padding: 1.5rem !default; + +ion-toast { + display: block; + height: $toast-width; + left: 0; + position: absolute; + top: 0; + width: $toast-width; + z-index: $z-index-overlay; +} + +.toast-wrapper { + background: $toast-ios-background; + border-radius: $toast-ios-border-radius; + bottom: 10px; + display: block; + left: 10px; + margin: auto; + max-width: $toast-max-width; + position: absolute; + right: 10px; + transform: translate3d(0, 100%, 0); + z-index: $z-index-overlay-wrapper; +} + +.toast-message { + color: $toast-ios-title-color; + font-size: $toast-ios-title-font-size; + padding: $toast-ios-title-padding; +} diff --git a/ionic/components/toast/toast.md.scss b/ionic/components/toast/toast.md.scss new file mode 100644 index 00000000000..a50b34b29ff --- /dev/null +++ b/ionic/components/toast/toast.md.scss @@ -0,0 +1,41 @@ +@import "../../globals.md"; + +// Material Design Toast +// -------------------------------------------------- +$toast-md-text-align: left !default; +$toast-md-background: #333333 !default; +$toast-md-group-margin-bottom: 8px !default; + +$toast-md-title-color: #fff !default; +$toast-md-title-font-size: 1.5rem !default; +$toast-md-title-padding: 19px 16px 17px !default; + +ion-toast { + display: block; + height: $toast-width; + left: 0; + position: absolute; + top: 0; + width: $toast-width; + z-index: $z-index-overlay; +} + +.toast-wrapper { + background: $toast-md-background; + bottom: 0; + display: block; + left: 0; + margin: auto; + max-width: $toast-max-width; + position: absolute; + right: 0; + transform: translate3d(0, 100%, 0); + width: $toast-width; + z-index: $z-index-overlay-wrapper; +} + +.toast-message { + color: $toast-md-title-color; + font-size: $toast-md-title-font-size; + padding: $toast-md-title-padding; +} diff --git a/ionic/components/toast/toast.scss b/ionic/components/toast/toast.scss new file mode 100644 index 00000000000..3a09c7954e5 --- /dev/null +++ b/ionic/components/toast/toast.scss @@ -0,0 +1,43 @@ +@import "../../globals.ios"; + +// Action Sheet +// -------------------------------------------------- + +$toast-width: 100% !default; +$toast-max-width: 700px !default; + +ion-toast { + position: absolute; + top: 0; + left: 0; + z-index: $z-index-overlay; + display: block; + width: $toast-width; + height: $toast-width; +} + +.toast-container { + display: flex; + align-items: center; + + button { + font-size: 1.5rem; + padding: 19px 16px 17px; + } +} + +.toast-message { + flex: 1; +} + +.toast-wrapper { + bottom: 0; + display: block; + left: 0; + margin: auto; + max-width: $toast-max-width; + position: absolute; + right: 0; + transform: translate3d(0, 100%, 0); + z-index: $z-index-overlay-wrapper; +} diff --git a/ionic/components/toast/toast.ts b/ionic/components/toast/toast.ts new file mode 100644 index 00000000000..b7d216b600e --- /dev/null +++ b/ionic/components/toast/toast.ts @@ -0,0 +1,263 @@ +import {Component, ElementRef, Renderer} from 'angular2/core'; +import {NgClass, NgIf, NgFor} from 'angular2/common'; + +import {Button} from '../button/button'; +import {Icon} from '../icon/icon'; +import {ActionSheet, ActionSheetOptions} from '../action-sheet/action-sheet'; +import {Animation} from '../../animations/animation'; +import {Transition, TransitionOptions} from '../../transitions/transition'; + +import {Config} from '../../config/config'; +import {isPresent} from '../../util/util'; +import {NavParams} from '../nav/nav-params'; +import {NavController} from '../nav/nav-controller'; +import {ViewController} from '../nav/view-controller'; + +/** + * @name Toast + * @description + * An Toast is a small message that appears in the lower part of the screen. + * It's useful for displaying success messages, error messages, etc. + * `title`, `subTitle` and `message`. + * + * @usage + * ```ts + * constructor(nav: NavController) { + * this.nav = nav; + * } + * + * presentToast() { + * let toast = Toast.create({ + * message: 'User was added successfully', + * duration: 3000 + * }); + * this.nav.present(toast); + * } + * ``` + * + * @demo /docs/v2/demos/toast/ + */ +export class Toast extends ViewController { + + constructor(opts: ToastOptions = {}) { + console.log('Toast Constructor'); + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + + super(ToastCmp, opts); + + this.viewType = 'toast'; + this.isOverlay = false; + + // by default, toasts should not fire lifecycle events of other views + // for example, when an toast enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = 'toast' + (direction === 'back' ? 'Leave' : 'Enter'); + return this._nav && this._nav.config.get(key); + } + + /** + * @param {string} message Toast message content + */ + setMessage(message: string) { + this.data.message = message; + } + + /** + * + * Toast options + * + * | Property | Type | Description | + * |-----------------------|-----------|--------------------------------------------------------------------------- | + * | message | `string` | The message for the toast | + * | duration | `number` | The amount of time in milliseconds the toast should appear (optional) | + * | cssClass | `string` | Any additional class for the toast (optional) | + * | showCloseButton | `boolean` | Whether or not to show an optional button to close the toast. (optional) | + * | closeButtonText | `string` | Text to display in the close button. (optional) | + * | enableBackdropDismiss | `boolean` | Whether the the toast should be dismissed by tapping the backdrop (optional) | + * + * @param {object} opts Toast. See the tabel above + */ + static create(opts: ToastOptions = {}) { + return new Toast(opts); + } + +} + + +/** +* @private +*/ +@Component({ + selector: 'ion-toast', + template: ` + +
+
+
{{d.message}}
+ +
+
+ `, + host: { + 'role': 'dialog', + '[attr.aria-labelledby]': 'hdrId', + '[attr.aria-describedby]': 'descId' + }, + directives: [NgIf, Icon, Button] +}) +class ToastCmp { + private d: any; + private descId: string; + private hdrId: string; + private id: number; + private created: number; + + constructor( + private _nav: NavController, + private _viewCtrl: ViewController, + private _config: Config, + private _elementRef: ElementRef, + params: NavParams, + renderer: Renderer + ) { + + this.d = params.data; + this.created = Date.now(); + + if (this.d.cssClass) { + renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++toastIds); + + if (this.d.message) { + this.hdrId = 'acst-hdr-' + this.id; + } + } + + onPageDidEnter() { + let activeElement: any = document.activeElement; + if (document.activeElement) { + activeElement.blur(); + } + + let focusableEle = this._elementRef.nativeElement.querySelector('button'); + + if (focusableEle) { + focusableEle.focus(); + } + + // if there's a `duration` set, automatically dismiss. + setTimeout(() => this.dismiss('backdrop'), this.d.duration ? this.d.duration : 3000) + + } + + click(button, dismissDelay?) { + if (!this.isEnabled()) { + return; + } + let shouldDismiss = true; + + if (shouldDismiss) { + setTimeout(() => { + this.dismiss(button.role); + }, dismissDelay || this._config.get('pageTransitionDelay')); + } + } + + bdClick() { + if (this.isEnabled() && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + dismiss(role): Promise { + return this._viewCtrl.dismiss(null, role); + } + + isEnabled() { + let tm = this._config.getNumber('overlayCreatedDiff', 750); + return (this.created + tm < Date.now()); + } + +} + +export interface ToastOptions { + title?: string; + cssClass?: string; + buttons?: Array; + duration?: number, + showCloseButton?: boolean; + closeButtonText?: string; + enableBackdropDismiss?: boolean; +} + +class ToastSlideIn extends Transition { + constructor(enteringView, leavingView, opts: TransitionOptions) { + super(opts); + + let ele = enteringView.pageRef().nativeElement; + let wrapper = new Animation(ele.querySelector('.toast-wrapper')); + + wrapper.fromTo('translateY', '100%', '0%'); + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper); + } +} + +class ToastSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = leavingView.pageRef().nativeElement; + let wrapper = new Animation(ele.querySelector('.toast-wrapper')); + + wrapper.fromTo('translateY', '0%', '100%'); + this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(wrapper); + } +} + +class ToastMdSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.toast-wrapper')); + + backdrop.fromTo('opacity', 0, 0); + wrapper.fromTo('translateY', '100%', '0%'); + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper) + } +} + +class ToastMdSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = leavingView.pageRef().nativeElement; + let wrapper = new Animation(ele.querySelector('.toast-wrapper')); + let backdrop = new Animation(ele.querySelector('.backdrop')); + + wrapper.fromTo('translateY', '0%', '100%'); + backdrop.fromTo('opacity', 0, 0); + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); + } +} + +Transition.register('toast-slide-in', ToastSlideIn); +Transition.register('toast-slide-out', ToastSlideOut); +Transition.register('toast-md-slide-in', ToastMdSlideIn); +Transition.register('toast-md-slide-out', ToastMdSlideOut); + + +let toastIds = -1; diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 590952479ab..55901bfcf8a 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -9,6 +9,9 @@ Config.setModeConfig('ios', { actionSheetEnter: 'action-sheet-slide-in', actionSheetLeave: 'action-sheet-slide-out', + toastEnter: 'toast-slide-in', + toastLeave: 'toast-slide-out', + alertEnter: 'alert-pop-in', alertLeave: 'alert-pop-out', @@ -41,6 +44,9 @@ Config.setModeConfig('md', { actionSheetEnter: 'action-sheet-md-slide-in', actionSheetLeave: 'action-sheet-md-slide-out', + toastEnter: 'toast-md-slide-in', + toastLeave: 'toast-md-slide-out', + alertEnter: 'alert-md-pop-in', alertLeave: 'alert-md-pop-out', @@ -76,6 +82,9 @@ Config.setModeConfig('wp', { actionSheetEnter: 'action-sheet-wp-slide-in', actionSheetLeave: 'action-sheet-wp-slide-out', + toastEnter: 'toast-wp-slide-in', + toastLeave: 'toast-wp-slide-out', + alertEnter: 'alert-wp-pop-in', alertLeave: 'alert-wp-pop-out',