Skip to content

Commit

Permalink
feat(toast): add stacked buttons functionality (#26790)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamdebeasi committed Feb 15, 2023
1 parent daa89a2 commit fc5fcc0
Show file tree
Hide file tree
Showing 24 changed files with 134 additions and 6 deletions.
1 change: 1 addition & 0 deletions angular/src/index.ts
Expand Up @@ -130,6 +130,7 @@ export {
TextareaCustomEvent,
ToastOptions,
ToastButton,
ToastLayout,
ToggleChangeEventDetail,
ToggleCustomEvent,
} from '@ionic/core';
1 change: 1 addition & 0 deletions core/api.txt
Expand Up @@ -1386,6 +1386,7 @@ ion-toast,prop,header,string | undefined,undefined,false,false
ion-toast,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
ion-toast,prop,icon,string | undefined,undefined,false,false
ion-toast,prop,keyboardClose,boolean,false,false,false
ion-toast,prop,layout,"baseline" | "stacked",'baseline',false,false
ion-toast,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-toast,prop,message,IonicSafeString | string | undefined,undefined,false,false
ion-toast,prop,mode,"ios" | "md",undefined,false,false
Expand Down
10 changes: 9 additions & 1 deletion core/src/components.d.ts
Expand Up @@ -14,7 +14,7 @@ import { PickerInternalChangeEventDetail } from "./components/picker-internal/pi
import { PinFormatter } from "./components/range/range-interface";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SelectCompareFn } from "./components/select/select-interface";
import { ToastAttributes, ToastPosition } from "./components/toast/toast-interface";
import { ToastAttributes, ToastLayout, ToastPosition } from "./components/toast/toast-interface";
export namespace Components {
interface IonAccordion {
/**
Expand Down Expand Up @@ -2976,6 +2976,10 @@ export namespace Components {
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
"keyboardClose": boolean;
/**
* 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.
*/
"layout": ToastLayout;
/**
* Animation to use when the toast is dismissed.
*/
Expand Down Expand Up @@ -6977,6 +6981,10 @@ declare namespace LocalJSX {
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
"keyboardClose"?: boolean;
/**
* 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.
*/
"layout"?: ToastLayout;
/**
* Animation to use when the toast is dismissed.
*/
Expand Down
57 changes: 57 additions & 0 deletions core/src/components/toast/test/layout/index.html
@@ -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 changes: 15 additions & 0 deletions core/src/components/toast/test/layout/toast.e2e.ts
@@ -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`);
});
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions core/src/components/toast/toast-interface.ts
Expand Up @@ -12,6 +12,7 @@ export interface ToastOptions {
animated?: boolean;
icon?: string;
htmlAttributes?: ToastAttributes;
layout?: ToastLayout;

color?: Color;
mode?: Mode;
Expand All @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions core/src/components/toast/toast.md.scss
Expand Up @@ -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);

Expand Down
12 changes: 11 additions & 1 deletion core/src/components/toast/toast.scss
Expand Up @@ -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;
Expand All @@ -134,6 +138,12 @@
display: flex;
}

.toast-layout-stacked .toast-button-group {
justify-content: end;

width: 100%;
}

.toast-button {
border: 0;

Expand Down
25 changes: 24 additions & 1 deletion core/src/components/toast/toast.tsx
Expand Up @@ -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';
Expand All @@ -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

Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -290,16 +300,29 @@ 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');
const mode = getIonMode(this);
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"
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Expand Up @@ -81,6 +81,7 @@ export {
TextareaCustomEvent,
ToastOptions,
ToastButton,
ToastLayout,
ToggleChangeEventDetail,
ToggleCustomEvent,
} from '@ionic/core/components';
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/src/components/Overlays.ts
Expand Up @@ -27,7 +27,7 @@ export const IonLoading = /*@__PURE__*/ defineOverlayContainer<JSX.IonLoading>('

export const IonPicker = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicker>('ion-picker', defineIonPickerCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop'], pickerController);

export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController);
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController);

export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'swipeToClose', 'trigger']);

Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/index.ts
Expand Up @@ -121,6 +121,7 @@ export {
TextareaCustomEvent,
ToastOptions,
ToastButton,
ToastLayout,
ToggleChangeEventDetail,
ToggleCustomEvent,
} from "@ionic/core/components";

0 comments on commit fc5fcc0

Please sign in to comment.