feat(toast): add stacked buttons functionality (#26790)
57
core/src/components/toast/test/layout/index.html
Normal file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Toast - Layout</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<script type="module">
|
||||
import { toastController } from '../../../../dist/ionic/index.esm.js';
|
||||
window.toastController = toastController;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Toast - Layout</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button id="baseline" onclick="openToast(baselineConfig)">Open Baseline Layout Toast</ion-button>
|
||||
<ion-button id="stacked" onclick="openToast(stackedConfig)">Open Stacked Layout Toast</ion-button>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
async function openToast(opts) {
|
||||
const toast = await toastController.create(opts);
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
const baselineConfig = {
|
||||
icon: 'globe',
|
||||
header: 'Toast Header',
|
||||
message: 'This is an inline layout toast.',
|
||||
buttons: [
|
||||
{ side: 'start', text: 'Start Button', icon: 'alarm' },
|
||||
{ side: 'end', text: 'End Button', icon: 'bonfire' },
|
||||
],
|
||||
};
|
||||
|
||||
const stackedConfig = {
|
||||
...baselineConfig,
|
||||
message: 'This is a stacked layout toast.',
|
||||
layout: 'stacked',
|
||||
};
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
15
core/src/components/toast/test/layout/toast.e2e.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('toast: stacked layout', () => {
|
||||
test('should render stacked buttons', async ({ page }) => {
|
||||
await page.goto('/src/components/toast/test/layout');
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
|
||||
await page.click('#stacked');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
const toastWrapper = page.locator('ion-toast .toast-wrapper');
|
||||
expect(await toastWrapper.screenshot()).toMatchSnapshot(`toast-stacked-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 29 KiB |
@ -12,6 +12,7 @@ export interface ToastOptions {
|
||||
animated?: boolean;
|
||||
icon?: string;
|
||||
htmlAttributes?: ToastAttributes;
|
||||
layout?: ToastLayout;
|
||||
|
||||
color?: Color;
|
||||
mode?: Mode;
|
||||
@ -27,6 +28,8 @@ export interface ToastOptions {
|
||||
*/
|
||||
export type ToastAttributes = { [key: string]: any };
|
||||
|
||||
export type ToastLayout = 'baseline' | 'stacked';
|
||||
|
||||
export interface ToastButton {
|
||||
text?: string;
|
||||
icon?: string;
|
||||
|
||||
@ -49,14 +49,22 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
.toast-button-group-start {
|
||||
.toast-layout-baseline .toast-button-group-start {
|
||||
@include margin(null, null, null, 8px);
|
||||
}
|
||||
|
||||
.toast-button-group-end {
|
||||
.toast-layout-stacked .toast-button-group-start {
|
||||
@include margin(8px, 8px, null, null);
|
||||
}
|
||||
|
||||
.toast-layout-baseline .toast-button-group-end {
|
||||
@include margin(null, 8px, null, null);
|
||||
}
|
||||
|
||||
.toast-layout-stacked .toast-button-group-end {
|
||||
@include margin(null, 8px, 8px, null);
|
||||
}
|
||||
|
||||
.toast-button {
|
||||
@include padding($toast-md-button-padding-top, $toast-md-button-padding-end, $toast-md-button-padding-bottom, $toast-md-button-padding-start);
|
||||
|
||||
|
||||
@ -112,7 +112,11 @@
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
.toast-layout-stacked .toast-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toast-layout-baseline .toast-content {
|
||||
display: flex;
|
||||
|
||||
flex: 1;
|
||||
@ -134,6 +138,12 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.toast-layout-stacked .toast-button-group {
|
||||
justify-content: end;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-button {
|
||||
border: 0;
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
OverlayInterface,
|
||||
ToastButton,
|
||||
} from '../../interface';
|
||||
import { printIonWarning } from '../../utils/logging';
|
||||
import { dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
|
||||
import type { IonicSafeString } from '../../utils/sanitization';
|
||||
import { sanitizeDOMString } from '../../utils/sanitization';
|
||||
@ -20,7 +21,7 @@ import { iosEnterAnimation } from './animations/ios.enter';
|
||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
import { mdEnterAnimation } from './animations/md.enter';
|
||||
import { mdLeaveAnimation } from './animations/md.leave';
|
||||
import type { ToastAttributes, ToastPosition } from './toast-interface';
|
||||
import type { ToastAttributes, ToastPosition, ToastLayout } from './toast-interface';
|
||||
|
||||
// TODO(FW-2832): types
|
||||
|
||||
@ -87,6 +88,15 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() header?: string;
|
||||
|
||||
/**
|
||||
* Defines how the message and buttons are laid out in the toast.
|
||||
* 'baseline': The message and the buttons will appear on the same line.
|
||||
* Message text may wrap within the message container.
|
||||
* 'stacked': The buttons containers and message will stack on top
|
||||
* of each other. Use this if you have long text in your buttons.
|
||||
*/
|
||||
@Prop() layout: ToastLayout = 'baseline';
|
||||
|
||||
/**
|
||||
* Message to be shown in the toast.
|
||||
*/
|
||||
@ -290,6 +300,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layout, el } = this;
|
||||
const allButtons = this.getButtons();
|
||||
const startButtons = allButtons.filter((b) => b.side === 'start');
|
||||
const endButtons = allButtons.filter((b) => b.side !== 'start');
|
||||
@ -297,9 +308,21 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
const wrapperClass = {
|
||||
'toast-wrapper': true,
|
||||
[`toast-${this.position}`]: true,
|
||||
[`toast-layout-${layout}`]: true,
|
||||
};
|
||||
const role = allButtons.length > 0 ? 'dialog' : 'status';
|
||||
|
||||
/**
|
||||
* Stacked buttons are only meant to be
|
||||
* used with one type of button.
|
||||
*/
|
||||
if (layout === 'stacked' && startButtons.length > 0 && endButtons.length > 0) {
|
||||
printIonWarning(
|
||||
'This toast is using start and end buttons with the stacked toast layout. We recommend following the best practice of using either start or end buttons with the stacked toast layout.',
|
||||
el
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-live="polite"
|
||||
|
||||