mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 10:01:59 +08:00
fix(toast): update toast design to match MD spec (#16323)
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
This commit is contained in:
@ -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;
|
||||
|
@ -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))`;
|
||||
@ -19,9 +22,9 @@ 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:
|
||||
@ -29,7 +32,7 @@ export function iosEnterAnimation(AnimationC: Animation, baseEl: HTMLElement, po
|
||||
break;
|
||||
}
|
||||
return Promise.resolve(baseAnimation
|
||||
.addElement(baseEl)
|
||||
.addElement(hostEl)
|
||||
.easing('cubic-bezier(.155,1.105,.295,1.12)')
|
||||
.duration(400)
|
||||
.add(wrapperAnimation));
|
||||
|
@ -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))`;
|
||||
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
||||
box-shadow: $toast-md-box-shadow;
|
||||
|
||||
opacity: .01;
|
||||
|
||||
z-index: $z-index-overlay-wrapper;
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-top {
|
||||
padding-top: var(--ion-safe-area-top, 0);
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-bottom {
|
||||
padding-bottom: var(--ion-safe-area-bottom, 0);
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-middle {
|
||||
opacity: .01;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
@include padding($toast-md-title-padding-top, $toast-md-title-padding-end, $toast-md-title-padding-bottom, $toast-md-title-padding-start);
|
||||
@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-button {
|
||||
--margin-end: 0;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -4,20 +4,20 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--button-color: inherit;
|
||||
|
||||
/**
|
||||
* @prop --background: Background of the toast
|
||||
* @prop --button-color: Color of the button text
|
||||
* @prop --color: Color of the toast text
|
||||
*/
|
||||
--button-color: inherit;
|
||||
|
||||
@include position(0, null, null, 0);
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: $toast-width;
|
||||
height: $toast-width;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: var(--color);
|
||||
|
||||
@ -38,18 +38,6 @@
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-top {
|
||||
@include transform(translate3d(0, -100%, 0));
|
||||
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-bottom {
|
||||
@include transform(translate3d(0, 100%, 0));
|
||||
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
display: flex;
|
||||
|
||||
@ -61,10 +49,10 @@
|
||||
|
||||
.toast-button {
|
||||
color: var(--button-color);
|
||||
|
||||
font-size: $toast-button-font-size;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
|
||||
white-space: pre;
|
||||
}
|
||||
|
@ -28,9 +28,12 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
animation: Animation | undefined;
|
||||
|
||||
@Prop({ connect: 'ion-animation-controller' }) animationCtrl!: HTMLIonAnimationControllerElement;
|
||||
|
||||
@Prop({ context: 'config' }) config!: Config;
|
||||
|
||||
/** @internal */
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@Prop() overlayIndex!: number;
|
||||
|
||||
/**
|
||||
|
@ -3,9 +3,6 @@
|
||||
// Toast
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - Width of the toast
|
||||
$toast-width: 100% !default;
|
||||
|
||||
/// @prop - Max width of the toast
|
||||
$toast-max-width: 700px !default;
|
||||
|
||||
|
@ -48,9 +48,24 @@
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-tabs>
|
||||
<ion-tab label="Lists" icon="list">
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="list">
|
||||
<ion-label>Lists</ion-label>
|
||||
<ion-icon name="list"></ion-icon>
|
||||
</ion-tab-button>
|
||||
<ion-tab-button tab="colors">
|
||||
<ion-label>Colors</ion-label>
|
||||
<ion-icon name="brush"></ion-icon>
|
||||
</ion-tab-button>
|
||||
<ion-tab-button tab="other">
|
||||
<ion-label>Other</ion-label>
|
||||
<ion-icon name="more"></ion-icon>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
||||
<ion-tab tab="list">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Lists</ion-title>
|
||||
|
||||
<div class="right-container">
|
||||
@ -280,7 +295,7 @@
|
||||
</ion-content>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab label="Colors" icon="brush">
|
||||
<ion-tab tab="colors">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Colors</ion-title>
|
||||
@ -636,7 +651,7 @@
|
||||
</ion-content>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab label="Other" icon="more">
|
||||
<ion-tab tab="other">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Other</ion-title>
|
||||
@ -656,12 +671,7 @@
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tabbar></ion-tabbar>
|
||||
</ion-tabs>
|
||||
|
||||
|
||||
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
|
@ -132,7 +132,7 @@ function getAppRoot(doc: Document) {
|
||||
async function overlayAnimation(
|
||||
overlay: OverlayInterface,
|
||||
animationBuilder: AnimationBuilder,
|
||||
baseEl: HTMLElement,
|
||||
baseEl: any,
|
||||
opts: any
|
||||
): Promise<boolean> {
|
||||
if (overlay.animation) {
|
||||
|
@ -218,7 +218,7 @@ export interface TransitionOptions extends NavOptions {
|
||||
queue: QueueApi;
|
||||
progressCallback?: ((ani: Animation | undefined) => void);
|
||||
window: Window;
|
||||
baseEl: HTMLElement;
|
||||
baseEl: any;
|
||||
enteringEl: HTMLElement;
|
||||
leavingEl: HTMLElement | undefined;
|
||||
}
|
||||
|
Reference in New Issue
Block a user