Feature/toast position (#6804)

* refactor(toast): refactored toast to support positions and clean up transitions a bit

refactored toast to support positions and clean up transitions a bit

* test(toast): added additional test

added additional tests
This commit is contained in:
Dan Bucholtz
2016-06-08 10:54:31 -05:00
committed by Brandy Carney
parent 9f293e8549
commit 82adf6f3d3
10 changed files with 288 additions and 92 deletions

View File

@ -8,9 +8,11 @@ import {ionicBootstrap, Toast, NavController} from 'ionic-angular';
class ApiPage {
constructor(private nav: NavController) { }
showToast() {
showToast(position: string) {
const toast = Toast.create({
message: 'User was created successfully',
position: position,
duration: 3000
});
toast.onDismiss(this.dismissHandler);
@ -20,6 +22,7 @@ class ApiPage {
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.',
duration: 3000
});
toast.onDismiss(this.dismissHandler);

View File

@ -3,7 +3,9 @@
</ion-toolbar>
<ion-content padding>
<button block (click)="showToast()">Show Toast</button>
<button block (click)="showToast('bottom')">Show Toast Bottom Position</button>
<button block (click)="showToast('top')">Show Toast Top Position</button>
<button block (click)="showToast('middle')">Show Toast Middle Position</button>
<button block margin-bottom (click)="showLongToast()">Show Long Toast</button>
<button block (click)="showDismissDurationToast()">Show Custom Duration Toast</button>

View File

@ -61,11 +61,12 @@ class E2EPage {
this.nav.present(toast);
}
showToastWithCloseButton() {
showToastWithCloseButton(positionString:string) {
const toast = Toast.create({
message: 'Your internet connection appears to be offline. Data integrity is not gauranteed.',
showCloseButton: true,
closeButtonText: 'Ok'
closeButtonText: 'Ok',
position: positionString
});
toast.onDismiss(this.dismissHandler);
this.nav.present(toast);

View File

@ -7,7 +7,9 @@
<button block (click)="showLongToast()">Show Long Toast</button>
<br />
<button block (click)="showDismissDurationToast()">Custom (1.5s) Duration</button>
<button block (click)="showToastWithCloseButton()" class="e2eOpenToast">With closeButtonText</button>
<button block (click)="showToastWithCloseButton('bottom')" class="e2eOpenToast">With closeButtonText</button>
<button block (click)="showToastWithCloseButton('top')" class="e2eOpenToast">Top With closeButtonText</button>
<button block (click)="showToastWithCloseButton('middle')" class="e2eOpenToast">Middle With closeButtonText</button>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacinia purus ac turpis fermentum, nec accumsan nulla rutrum. Aenean lorem est, luctus id iaculis ac, ultricies quis odio. Aenean imperdiet imperdiet ex et vehicula. Suspendisse vulputate turpis quis ultricies porttitor. Proin malesuada tortor at libero laoreet, eu eleifend enim pulvinar. Nulla facilisi. Fusce sit amet mauris mauris. Mauris consequat libero sed egestas tincidunt.

View File

@ -11,11 +11,47 @@ describe('Toast', () => {
message: 'Please Wait...',
showCloseButton: true
});
expect(toast.data.position).toEqual('bottom');
expect(toast.data.message).toEqual('Please Wait...');
expect(toast.data.showCloseButton).toEqual(true);
});
it('should create toast with position top', () => {
let toast = Toast.create({
message: 'Please Wait...',
position: 'top'
});
expect(toast.data.position).toEqual('top');
});
it('should create toast with position middle', () => {
let toast = Toast.create({
message: 'Please Wait...',
position: 'middle'
});
expect(toast.data.position).toEqual('middle');
});
it('should create toast with position bottom', () => {
let toast = Toast.create({
message: 'Please Wait...',
position: 'bottom'
});
expect(toast.data.position).toEqual('bottom');
});
it('should set a duration', () => {
let toast = Toast.create({
message: 'Please Wait...',
duration: 3000
});
expect(toast.data.duration).toEqual(3000);
});
});
});

View File

@ -12,22 +12,10 @@ $toast-ios-title-color: #fff !default;
$toast-ios-title-font-size: 1.4rem !default;
$toast-ios-title-padding: 1.5rem !default;
ion-toast {
position: absolute;
top: 0;
left: 0;
z-index: $z-index-overlay;
display: block;
width: $toast-width;
height: $toast-width;
}
.toast-wrapper {
position: absolute;
right: 10px;
bottom: 10px;
left: 10px;
z-index: $z-index-overlay-wrapper;
display: block;
@ -40,7 +28,21 @@ ion-toast {
background: $toast-ios-background;
transform: translate3d(0, 100%, 0);
&.toast-top {
top: 0;
transform: translate3d(0, -100%, 0);
}
&.toast-bottom {
bottom: 0;
transform: translate3d(0, 100%, 0);
}
&.toast-middle {
opacity: .01;
}
}
.toast-message {

View File

@ -12,22 +12,9 @@ $toast-md-title-color: #fff !default;
$toast-md-title-font-size: 1.5rem !default;
$toast-md-title-padding: 19px 16px 17px !default;
ion-toast {
position: absolute;
top: 0;
left: 0;
z-index: $z-index-overlay;
display: block;
width: $toast-width;
height: $toast-width;
}
.toast-wrapper {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: $z-index-overlay-wrapper;
display: block;
@ -39,7 +26,21 @@ ion-toast {
background: $toast-md-background;
transform: translate3d(0, 100%, 0);
&.toast-top {
top: 0;
transform: translate3d(0, -100%, 0);
}
&.toast-bottom {
bottom: 0;
transform: translate3d(0, 100%, 0);
}
&.toast-middle {
opacity: .01;
}
}
.toast-message {

View File

@ -39,18 +39,3 @@ ion-toast {
.toast-message {
flex: 1;
}
.toast-wrapper {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: $z-index-overlay-wrapper;
display: block;
margin: auto;
max-width: $toast-max-width;
transform: translate3d(0, 100%, 0);
}

View File

@ -13,12 +13,11 @@ import {ViewController} from '../nav/view-controller';
/**
* @name Toast
* @description
* A Toast is a subtle notification that appears at the bottom of the
* screen. It can be used to provide feedback about an operation or to
* A Toast is a subtle notification commonly used in modern applications.
* It can be used to provide feedback about an operation or to
* display a system message. The toast appears on top of the app's content,
* and can be dismissed by the app to resume user interaction with
* the app. It includes a backdrop, which can optionally be clicked to
* dismiss the toast.
* the app.
*
* ### Creating
* All of the toast options should be passed in the first argument of
@ -27,14 +26,18 @@ import {ViewController} from '../nav/view-controller';
* true in order to display a close button on the toast. See the [create](#create)
* method below for all available options.
*
* ### Positioning
* Toasts can be positioned at the top, bottom or middle of the
* view port. The position can be passed to the `Toast.create(opts)` method.
* The position option is a string, and the values accepted are `top`, `bottom` and `middle`.
* If the position is not specified, the toast will be displayed at the bottom of the view port.
*
* ### Dismissing
* The toast can be dismissed automatically after a specific amount of time
* by passing the number of milliseconds to display it in the `duration` of
* the toast options. It can also be dismissed by clicking on the backdrop,
* unless `enableBackdropDismiss` is set to `false` upon creation. If `showCloseButton`
* is set to true, then the close button will dismiss the toast. To dismiss
* the toast after creation, call the `dismiss()` method on the Toast instance.
* The `onDismiss` function can be called to perform an action after the toast
* the toast options. If `showCloseButton` is set to true, then the close button
* will dismiss the toast. To dismiss the toast after creation, call the `dismiss()`
* method on the Toast instance. The `onDismiss` function can be called to perform an action after the toast
* is dismissed.
*
* @usage
@ -46,7 +49,8 @@ import {ViewController} from '../nav/view-controller';
* presentToast() {
* let toast = Toast.create({
* message: 'User was added successfully',
* duration: 3000
* duration: 3000,
* position: 'top'
* });
*
* toast.onDismiss(() => {
@ -63,8 +67,11 @@ export class Toast extends ViewController {
constructor(opts: ToastOptions = {}) {
opts.dismissOnPageChange = isPresent(opts.dismissOnPageChange) ? !!opts.dismissOnPageChange : false;
super(ToastCmp, opts);
// set the position to the bottom if not provided
if ( ! opts.position || ! this.isVaidPosition(opts.position)) {
opts.position = TOAST_POSITION_BOTTOM;
}
this.viewType = 'toast';
this.isOverlay = true;
this.usePortal = true;
@ -76,7 +83,6 @@ export class Toast extends ViewController {
}
/**
* @private
*/
@ -85,6 +91,13 @@ export class Toast extends ViewController {
return this._nav && this._nav.config.get(key);
}
/**
* @private
*/
isVaidPosition(position: string) {
return position === TOAST_POSITION_TOP || position === TOAST_POSITION_MIDDLE || position === TOAST_POSITION_BOTTOM;
}
/**
* @param {string} message Toast message content
*/
@ -100,6 +113,7 @@ export class Toast extends ViewController {
* |-----------------------|-----------|-----------------|---------------------------------------------------------------------------------------------------------------|
* | message | `string` | - | The message for the toast. Long strings will wrap and the toast container will expand. |
* | duration | `number` | - | How many milliseconds to wait before hiding the toast. By default, it will show until `dismiss()` is called. |
* | position | `string` | "bottom" | The position of the toast on the screen. "top", "middle", and "bottom" are the accepted values. |
* | cssClass | `string` | - | Any additional class for custom styles. |
* | showCloseButton | `boolean` | false | Whether or not to show a button to close the toast. |
* | closeButtonText | `string` | "Close" | Text to display in the close button. |
@ -110,9 +124,12 @@ export class Toast extends ViewController {
static create(opts: ToastOptions = {}) {
return new Toast(opts);
}
}
/* Don't expose these for now - let's move to an enum or something long term */
const TOAST_POSITION_TOP: string = 'top';
const TOAST_POSITION_MIDDLE: string = 'middle';
const TOAST_POSITION_BOTTOM: string = 'bottom';
/**
* @private
@ -120,7 +137,11 @@ export class Toast extends ViewController {
@Component({
selector: 'ion-toast',
template: `
<div class="toast-wrapper">
<div class="toast-wrapper"
[class.toast-bottom]="d.position === 'bottom'"
[class.toast-middle]="d.position === 'middle'"
[class.toast-top]="d.position === 'top'"
>
<div class="toast-container">
<div class="toast-message" id="{{hdrId}}" *ngIf="d.message">{{d.message}}</div>
<button clear class="toast-button" *ngIf="d.showCloseButton" (click)="cbClick()">
@ -212,17 +233,43 @@ export interface ToastOptions {
showCloseButton?: boolean;
closeButtonText?: string;
dismissOnPageChange?: boolean;
position?: string;
}
class ToastSlideIn extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM READS
let ele = enteringView.pageRef().nativeElement;
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
const wrapperEle = <HTMLElement> ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
wrapper.fromTo('translateY', '120%', '0%');
this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper);
if ( enteringView.data && enteringView.data.position === TOAST_POSITION_TOP ) {
// top
// by default, it is -100% hidden (above the screen)
// so move from that to 10px below top: 0px;
wrapper.fromTo('translateY', '-100%', `${10}px`);
}
else if ( enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just center it and fade it in
let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2);
// DOM WRITE
wrapperEle.style.top = `${topPosition}px`;
wrapper.fromTo('opacity', '0.01', '1.0');
}
else {
// bottom
// by default, it is 100% hidden (below the screen),
// so move from that to 10 px above bottom: 0px
wrapper.fromTo('translateY', '100%', `${0 - 10}px`);
}
const EASE: string = 'cubic-bezier(.36,.66,.04,1)';
const DURATION: number = 400;
// DOM WRITES
this.easing(EASE).duration(DURATION).add(wrapper);
}
}
@ -230,11 +277,31 @@ class ToastSlideOut extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM reads
let ele = leavingView.pageRef().nativeElement;
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
const wrapperEle = <HTMLElement> ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
wrapper.fromTo('translateY', '0%', '120%');
this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(wrapper);
if ( leavingView.data && leavingView.data.position === TOAST_POSITION_TOP ) {
// top
// reverse arguments from enter transition
wrapper.fromTo('translateY', `${10}px`, '-100%');
}
else if ( leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just fade it out
wrapper.fromTo('opacity', '1.0', '0.0');
}
else {
// bottom
// reverse arguments from enter transition
wrapper.fromTo('translateY', `${0 - 10}px`, '100%');
}
const EASE: string = 'cubic-bezier(.36,.66,.04,1)';
const DURATION: number = 300;
// DOM writes
this.easing(EASE).duration(DURATION).add(wrapper);
}
}
@ -242,13 +309,36 @@ class ToastMdSlideIn extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM reads
let ele = enteringView.pageRef().nativeElement;
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
const wrapperEle = ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
backdrop.fromTo('opacity', 0, 0);
wrapper.fromTo('translateY', '120%', '0%');
this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper);
if ( enteringView.data && enteringView.data.position === TOAST_POSITION_TOP ) {
// top
// by default, it is -100% hidden (above the screen)
// so move from that to top: 0px;
wrapper.fromTo('translateY', '-100%', `0px`);
}
else if ( enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just center it and fade it in
let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2);
// DOM WRITE
wrapperEle.style.top = `${topPosition}px`;
wrapper.fromTo('opacity', '0.01', '1.0');
}
else {
// bottom
// by default, it is 100% hidden (below the screen),
// so move from that to bottom: 0px
wrapper.fromTo('translateY', '100%', `0px`);
}
const EASE: string = 'cubic-bezier(.36,.66,.04,1)';
const DURATION: number = 400;
this.easing(EASE).duration(DURATION).add(wrapper);
}
}
@ -256,13 +346,33 @@ class ToastMdSlideOut extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM reads
let ele = leavingView.pageRef().nativeElement;
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
const wrapperEle = ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
wrapper.fromTo('translateY', '0%', '120%');
backdrop.fromTo('opacity', 0, 0);
this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper);
if ( leavingView.data && leavingView.data.position === TOAST_POSITION_TOP ) {
// top
// reverse arguments from enter transition
wrapper.fromTo('translateY', `${0}px`, '-100%');
}
else if ( leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just fade it out
wrapper.fromTo('opacity', '1.0', '0.0');
}
else {
// bottom
// reverse arguments from enter transition
wrapper.fromTo('translateY', `${0}px`, '100%');
}
// DOM writes
const EASE: string = 'cubic-bezier(.36,.66,.04,1)';
const DURATION: number = 450;
this.easing(EASE).duration(DURATION).add(wrapper);
}
}
@ -270,14 +380,35 @@ class ToastWpPopIn extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM reads
let ele = enteringView.pageRef().nativeElement;
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
const wrapperEle = ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.3', '1');
backdrop.fromTo('opacity', 0, 0);
if ( enteringView.data && enteringView.data.position === TOAST_POSITION_TOP ) {
// top
wrapper.fromTo('opacity', '0.01', '1');
wrapper.fromTo('scale', '1.3', '1');
}
else if ( enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just center it and fade it in
let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2);
// DOM WRITE
wrapperEle.style.top = `${topPosition}px`;
wrapper.fromTo('opacity', '0.01', '1.0');
wrapper.fromTo('scale', '1.3', '1');
}
else {
// bottom
wrapper.fromTo('opacity', '0.01', '1');
wrapper.fromTo('scale', '1.3', '1');
}
this.easing('cubic-bezier(0,0 0.05,1)').duration(200).add(backdrop).add(wrapper);
// DOM writes
const EASE: string = 'cubic-bezier(0,0 0.05,1)';
const DURATION: number = 200;
this.easing(EASE).duration(DURATION).add(wrapper);
}
}
@ -285,14 +416,34 @@ class ToastWpPopOut extends Transition {
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
super(opts);
// DOM reads
let ele = leavingView.pageRef().nativeElement;
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
let wrapper = new Animation(ele.querySelector('.toast-wrapper'));
const wrapperEle = ele.querySelector('.toast-wrapper');
let wrapper = new Animation(wrapperEle);
wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '1.3');
backdrop.fromTo('opacity', 0, 0);
if ( leavingView.data && leavingView.data.position === TOAST_POSITION_TOP ) {
// top
// reverse arguments from enter transition
wrapper.fromTo('opacity', '1', '0.00');
wrapper.fromTo('scale', '1', '1.3');
}
else if ( leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE ) {
// Middle
// just fade it out
wrapper.fromTo('opacity', '1.0', '0.00');
wrapper.fromTo('scale', '1', '1.3');
}
else {
// bottom
// reverse arguments from enter transition
wrapper.fromTo('opacity', '1', '0.00');
wrapper.fromTo('scale', '1', '1.3');
}
this.easing('ease-out').duration(150).add(backdrop).add(wrapper);
// DOM writes
const EASE: string = 'ease-out';
const DURATION: number = 150;
this.easing(EASE).duration(DURATION).add(wrapper);
}
}

View File

@ -13,10 +13,9 @@ $toast-wp-title-color: #fff !default;
$toast-wp-title-font-size: 1.4rem !default;
$toast-wp-title-padding: 1.5rem !default;
.toast-wrapper {
position: absolute;
bottom: 0;
right: 0;
left: 0;
z-index: $z-index-overlay-wrapper;
display: block;
@ -28,7 +27,21 @@ $toast-wp-title-padding: 1.5rem !default;
border-radius: $toast-wp-border-radius;
background: $toast-wp-background;
transform: translate3d(0, 100%, 0);
&.toast-top {
top: 0;
opacity: .01;
}
&.toast-bottom {
bottom: 0;
opacity: .01;
}
&.toast-middle {
opacity: .01;
}
}
.toast-message {