diff --git a/angular/src/index.ts b/angular/src/index.ts index a28a12dfefe..38d2130b7bb 100644 --- a/angular/src/index.ts +++ b/angular/src/index.ts @@ -130,6 +130,7 @@ export { TextareaCustomEvent, ToastOptions, ToastButton, + ToastLayout, ToggleChangeEventDetail, ToggleCustomEvent, } from '@ionic/core'; diff --git a/core/api.txt b/core/api.txt index c22df0bde25..8eb31c06047 100644 --- a/core/api.txt +++ b/core/api.txt @@ -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 diff --git a/core/src/components.d.ts b/core/src/components.d.ts index f5f68679914..1164b07ad0a 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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 { /** @@ -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. */ @@ -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. */ diff --git a/core/src/components/toast/test/layout/index.html b/core/src/components/toast/test/layout/index.html new file mode 100644 index 00000000000..3562accab74 --- /dev/null +++ b/core/src/components/toast/test/layout/index.html @@ -0,0 +1,57 @@ + + + + + Toast - Layout + + + + + + + + + + + + + Toast - Layout + + + + + Open Baseline Layout Toast + Open Stacked Layout Toast + + + + + + diff --git a/core/src/components/toast/test/layout/toast.e2e.ts b/core/src/components/toast/test/layout/toast.e2e.ts new file mode 100644 index 00000000000..f5c22cd1b17 --- /dev/null +++ b/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`); + }); +}); diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1a906abf687 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d7f8266d099 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..1d24d41c61e Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1b0edc0efa8 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5108314254f Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Safari-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..fdeff156432 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Chrome-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1ba4b083e3f Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Firefox-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ecab85e31fe Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Safari-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d5827d5ca99 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Chrome-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5a20ad35588 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Firefox-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..379adbf2bda Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Safari-linux.png b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e592c426bd1 Binary files /dev/null and b/core/src/components/toast/test/layout/toast.e2e.ts-snapshots/toast-stacked-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/toast/toast-interface.ts b/core/src/components/toast/toast-interface.ts index 9b417e37414..364ee9f2cd0 100644 --- a/core/src/components/toast/toast-interface.ts +++ b/core/src/components/toast/toast-interface.ts @@ -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; diff --git a/core/src/components/toast/toast.md.scss b/core/src/components/toast/toast.md.scss index 7581c4b8af1..968afe913ab 100644 --- a/core/src/components/toast/toast.md.scss +++ b/core/src/components/toast/toast.md.scss @@ -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); diff --git a/core/src/components/toast/toast.scss b/core/src/components/toast/toast.scss index 0a44e698084..0418e86c7c2 100644 --- a/core/src/components/toast/toast.scss +++ b/core/src/components/toast/toast.scss @@ -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; diff --git a/core/src/components/toast/toast.tsx b/core/src/components/toast/toast.tsx index cc941008e7a..50812b7a066 100644 --- a/core/src/components/toast/toast.tsx +++ b/core/src/components/toast/toast.tsx @@ -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 ( (' export const IonPicker = /*@__PURE__*/ defineOverlayContainer('ion-picker', defineIonPickerCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop'], pickerController); -export const IonToast = /*@__PURE__*/ defineOverlayContainer('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController); +export const IonToast = /*@__PURE__*/ defineOverlayContainer('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('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'swipeToClose', 'trigger']); diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 4a7bab011f0..b3af281c146 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -121,6 +121,7 @@ export { TextareaCustomEvent, ToastOptions, ToastButton, + ToastLayout, ToggleChangeEventDetail, ToggleCustomEvent, } from "@ionic/core/components";