Skip to content

Commit

Permalink
fix(toast): update toast design to match MD spec (#16323)
Browse files Browse the repository at this point in the history
Updates the Material Design Toast to closer match the spec: https://material.io/design/components/snackbars.html

- Updates the animation to use opacity, not translate
- Updates design with the right background, box-shadow, etc.
- Fixes the broken position middle of toast and updates e2e test to include this
- Allows for line breaks to be passed in the message

fixes #16271
  • Loading branch information
brandyscarney committed Nov 14, 2018
1 parent adae8d4 commit 188a635
Show file tree
Hide file tree
Showing 16 changed files with 172 additions and 102 deletions.
Expand Up @@ -41,7 +41,7 @@ export interface Animation {
hasCompleted: boolean;
}

export type AnimationBuilder = (Animation: Animation, baseEl: HTMLElement, opts?: any) => Promise<Animation>;
export type AnimationBuilder = (Animation: Animation, baseEl: any, opts?: any) => Promise<Animation>;

export interface PlayOptions {
duration?: number;
Expand Down
15 changes: 9 additions & 6 deletions core/src/components/toast/animations/ios.enter.ts
Expand Up @@ -3,12 +3,15 @@ import { Animation } from '../../../interface';
/**
* iOS Toast Enter Animation
*/
export function iosEnterAnimation(AnimationC: Animation, baseEl: HTMLElement, position: string): Promise<Animation> {
export function iosEnterAnimation(AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise<Animation> {
const baseAnimation = new AnimationC();

const wrapperAnimation = new AnimationC();
const wrapperEle = baseEl.querySelector('.toast-wrapper') as HTMLElement;
wrapperAnimation.addElement(wrapperEle);

const hostEl = baseEl.host || baseEl;
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;

wrapperAnimation.addElement(wrapperEl);

const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
Expand All @@ -19,17 +22,17 @@ export function iosEnterAnimation(AnimationC: Animation, baseEl: HTMLElement, po
break;
case 'middle':
const topPosition = Math.floor(
baseEl.clientHeight / 2 - wrapperEle.clientHeight / 2
hostEl.clientHeight / 2 - wrapperEl.clientHeight / 2
);
wrapperEle.style.top = `${topPosition}px`;
wrapperEl.style.top = `${topPosition}px`;
wrapperAnimation.fromTo('opacity', 0.01, 1);
break;
default:
wrapperAnimation.fromTo('translateY', '100%', bottom);
break;
}
return Promise.resolve(baseAnimation
.addElement(baseEl)
.addElement(hostEl)
.easing('cubic-bezier(.155,1.105,.295,1.12)')
.duration(400)
.add(wrapperAnimation));
Expand Down
11 changes: 7 additions & 4 deletions core/src/components/toast/animations/ios.leave.ts
Expand Up @@ -3,12 +3,15 @@ import { Animation } from '../../../interface';
/**
* iOS Toast Leave Animation
*/
export function iosLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement, position: string): Promise<Animation> {
export function iosLeaveAnimation(AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise<Animation> {
const baseAnimation = new AnimationC();

const wrapperAnimation = new AnimationC();
const wrapperEle = baseEl.querySelector('.toast-wrapper') as HTMLElement;
wrapperAnimation.addElement(wrapperEle);

const hostEl = baseEl.host || baseEl;
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;

wrapperAnimation.addElement(wrapperEl);

const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
Expand All @@ -25,7 +28,7 @@ export function iosLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement, po
break;
}
return Promise.resolve(baseAnimation
.addElement(baseEl)
.addElement(hostEl)
.easing('cubic-bezier(.36,.66,.04,1)')
.duration(300)
.add(wrapperAnimation));
Expand Down
24 changes: 16 additions & 8 deletions core/src/components/toast/animations/md.enter.ts
Expand Up @@ -3,30 +3,38 @@ import { Animation } from '../../../interface';
/**
* MD Toast Enter Animation
*/
export function mdEnterAnimation(AnimationC: Animation, baseEl: HTMLElement, position: string): Promise<Animation> {
export function mdEnterAnimation(AnimationC: Animation, baseEl: ShadowRoot, position: string): Promise<Animation> {
const baseAnimation = new AnimationC();

const wrapperAnimation = new AnimationC();
const wrapperEle = baseEl.querySelector('.toast-wrapper') as HTMLElement;
wrapperAnimation.addElement(wrapperEle);

const hostEl = baseEl.host || baseEl;
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;

wrapperAnimation.addElement(wrapperEl);

const bottom = `calc(8px + var(--ion-safe-area-bottom, 0px))`;
const top = `calc(8px + var(--ion-safe-area-top, 0px))`;

switch (position) {
case 'top':
wrapperAnimation.fromTo('translateY', '-100%', '0%');
wrapperEl.style.top = top;
wrapperAnimation.fromTo('opacity', 0.01, 1);
break;
case 'middle':
const topPosition = Math.floor(
baseEl.clientHeight / 2 - wrapperEle.clientHeight / 2
hostEl.clientHeight / 2 - wrapperEl.clientHeight / 2
);
wrapperEle.style.top = `${topPosition}px`;
wrapperEl.style.top = `${topPosition}px`;
wrapperAnimation.fromTo('opacity', 0.01, 1);
break;
default:
wrapperAnimation.fromTo('translateY', '100%', '0%');
wrapperEl.style.bottom = bottom;
wrapperAnimation.fromTo('opacity', 0.01, 1);
break;
}
return Promise.resolve(baseAnimation
.addElement(baseEl)
.addElement(hostEl)
.easing('cubic-bezier(.36,.66,.04,1)')
.duration(400)
.add(wrapperAnimation));
Expand Down
24 changes: 9 additions & 15 deletions core/src/components/toast/animations/md.leave.ts
Expand Up @@ -3,26 +3,20 @@ import { Animation } from '../../../interface';
/**
* md Toast Leave Animation
*/
export function mdLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement, position: string): Promise<Animation> {
export function mdLeaveAnimation(AnimationC: Animation, baseEl: ShadowRoot): Promise<Animation> {
const baseAnimation = new AnimationC();

const wrapperAnimation = new AnimationC();
const wrapperEle = baseEl.querySelector('.toast-wrapper') as HTMLElement;
wrapperAnimation.addElement(wrapperEle);

switch (position) {
case 'top':
wrapperAnimation.fromTo('translateY', '0px', '-100%');
break;
case 'middle':
wrapperAnimation.fromTo('opacity', 0.99, 0);
break;
default:
wrapperAnimation.fromTo('translateY', `0px`, '100%');
break;
}
const hostEl = baseEl.host || baseEl;
const wrapperEl = baseEl.querySelector('.toast-wrapper') as HTMLElement;

wrapperAnimation.addElement(wrapperEl);

wrapperAnimation.fromTo('opacity', 0.99, 0);

return Promise.resolve(baseAnimation
.addElement(baseEl)
.addElement(hostEl)
.easing('cubic-bezier(.36,.66,.04,1)')
.duration(300)
.add(wrapperAnimation));
Expand Down
49 changes: 46 additions & 3 deletions core/src/components/toast/test/basic/e2e.ts
Expand Up @@ -5,21 +5,64 @@ test('toast: basic', async () => {
url: '/src/components/toast/test/basic?ionic:_testing=true'
});

const button = await page.find('#showBottomToast');
// Show bottom toast
let button = await page.find('#showBottomToast');
await button.click();

let toast = await page.find('ion-toast');
await toast.waitForVisible();
await page.waitFor(250);

let compare = await page.compareScreenshot();
let compare = await page.compareScreenshot(`bottom toast`);
expect(compare).toMatchScreenshot();

await toast.callMethod('dismiss');
await toast.waitForNotVisible();
await page.waitFor(250);

compare = await page.compareScreenshot('dismissed');
compare = await page.compareScreenshot('dismissed bottom toast');
expect(compare).toMatchScreenshot();

toast = await page.find('ion-toast');
expect(toast).toBeNull();

// Show middle toast
button = await page.find('#showMiddleToast');
await button.click();

toast = await page.find('ion-toast');
await toast.waitForVisible();
await page.waitFor(250);

compare = await page.compareScreenshot(`middle toast`);
expect(compare).toMatchScreenshot();

await toast.callMethod('dismiss');
await toast.waitForNotVisible();
await page.waitFor(250);

compare = await page.compareScreenshot('dismissed middle toast');
expect(compare).toMatchScreenshot();

toast = await page.find('ion-toast');
expect(toast).toBeNull();

// Show top toast
button = await page.find('#showTopToast');
await button.click();

toast = await page.find('ion-toast');
await toast.waitForVisible();
await page.waitFor(250);

compare = await page.compareScreenshot(`top toast`);
expect(compare).toMatchScreenshot();

await toast.callMethod('dismiss');
await toast.waitForNotVisible();
await page.waitFor(250);

compare = await page.compareScreenshot('dismissed top toast');
expect(compare).toMatchScreenshot();

toast = await page.find('ion-toast');
Expand Down
6 changes: 3 additions & 3 deletions core/src/components/toast/test/basic/index.html
Expand Up @@ -21,9 +21,9 @@

<ion-content id="content" padding>
<ion-button expand="block" id="showBottomToast" onclick="presentToast('bottom')">Show Toast Bottom</ion-button>
<ion-button expand="block" onclick="presentToast('top')">Show Toast Top</ion-button>
<ion-button expand="block" onclick="presentToast('middle')">Show Toast Middle</ion-button>
<ion-button expand="block" onclick="presentToastWithOptions({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: 2000})">Show Toast with long message</ion-button>
<ion-button expand="block" id="showTopToast" onclick="presentToast('top')">Show Toast Top</ion-button>
<ion-button expand="block" id="showMiddleToast" onclick="presentToast('middle')">Show Toast Middle</ion-button>
<ion-button expand="block" onclick="presentToastWithOptions({message: 'Two-line message\nwith action.', showCloseButton: true, closeButtonText: 'Action'})">Show Toast with long message</ion-button>
<ion-button expand="block" onclick="presentToastWithOptions({message: 'click to close', showCloseButton: true})">Show Toast with Close Button</ion-button>
<ion-button expand="block" onclick="presentToastWithOptions({message: 'click to close', showCloseButton: true, closeButtonText: 'closing time'})">Show Toast with Custom Close Button Text</ion-button>
<ion-button expand="block" onclick="presentToastWithOptions({message: 'click to close', showCloseButton: true, translucent: true})">Show Translucent Toast</ion-button>
Expand Down
16 changes: 16 additions & 0 deletions core/src/components/toast/toast.ios.scss
Expand Up @@ -30,10 +30,26 @@
backdrop-filter: $toast-ios-translucent-filter;
}

.toast-wrapper.toast-top {
@include transform(translate3d(0, -100%, 0));

top: 0;
}

.toast-wrapper.toast-middle {
opacity: .01;
}

.toast-wrapper.toast-bottom {
@include transform(translate3d(0, 100%, 0));

bottom: 0;
}

.toast-message {
@include padding($toast-ios-title-padding-top, $toast-ios-title-padding-end, $toast-ios-title-padding-bottom, $toast-ios-title-padding-start);
}

.toast-button {
font-size: $toast-button-font-size;
}
29 changes: 14 additions & 15 deletions core/src/components/toast/toast.md.scss
Expand Up @@ -5,37 +5,36 @@
// --------------------------------------------------

:host {
--button-color: #{ion-color(primary, base)};
--background: #{$toast-md-background};
--color: #{$toast-md-title-color};
--color: #{$toast-md-color};

font-size: $toast-md-title-font-size;
font-size: $toast-md-font-size;
}

.toast-wrapper {
@include position-horizontal(0, 0);
@include border-radius(4px);
@include position-horizontal(8px, 8px);
@include margin(auto);

display: block;
position: absolute;

width: $toast-width;
max-width: $toast-max-width;

z-index: $z-index-overlay-wrapper;
}
box-shadow: $toast-md-box-shadow;

.toast-wrapper.toast-top {
padding-top: var(--ion-safe-area-top, 0);
}
opacity: .01;

.toast-wrapper.toast-bottom {
padding-bottom: var(--ion-safe-area-bottom, 0);
z-index: $z-index-overlay-wrapper;
}

.toast-wrapper.toast-middle {
opacity: .01;
.toast-message {
@include padding($toast-md-message-padding-top, $toast-md-message-padding-end, $toast-md-message-padding-bottom, $toast-md-message-padding-start);

line-height: $toast-md-message-line-height;
}

.toast-message {
@include padding($toast-md-title-padding-top, $toast-md-title-padding-end, $toast-md-title-padding-bottom, $toast-md-title-padding-start);
.toast-button {
--margin-end: 0;
}
34 changes: 20 additions & 14 deletions core/src/components/toast/toast.md.vars.scss
Expand Up @@ -3,23 +3,29 @@
// Material Design Toast
// --------------------------------------------------

/// @prop - Background of the toast wrapper
$toast-md-background: $text-color-step-150 !default;
/// @prop - Background of the toast
$toast-md-background: $text-color-step-200 !default;

/// @prop - Color of the toast title
$toast-md-title-color: $background-color !default;
/// @prop - Box shadow of the toast
$toast-md-box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12) !default;

/// @prop - Font size of the toast title
$toast-md-title-font-size: 15px !default;
/// @prop - Font size of the toast
$toast-md-font-size: 14px !default;

/// @prop - Padding top of the toast title
$toast-md-title-padding-top: 19px !default;
/// @prop - Color of the toast
$toast-md-color: $background-color-step-50 !default;

/// @prop - Padding end of the toast title
$toast-md-title-padding-end: 16px !default;
/// @prop - Font size of the toast message
$toast-md-message-line-height: 20px !default;

/// @prop - Padding bottom of the toast title
$toast-md-title-padding-bottom: 17px !default;
/// @prop - Padding top of the toast message
$toast-md-message-padding-top: 14px !default;

/// @prop - Padding start of the toast title
$toast-md-title-padding-start: $toast-md-title-padding-end !default;
/// @prop - Padding end of the toast message
$toast-md-message-padding-end: 16px !default;

/// @prop - Padding bottom of the toast message
$toast-md-message-padding-bottom: $toast-md-message-padding-top !default;

/// @prop - Padding start of the toast message
$toast-md-message-padding-start: $toast-md-message-padding-end !default;

0 comments on commit 188a635

Please sign in to comment.