feat(toast): implement new shapes (#29936)
Issue number: internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? The shape of the toast component could not be customized. ## What is the new behavior? - I have introduced a new shape property which determines whether the toast widget has soft, round or rectangular corners. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
@ -2376,6 +2376,7 @@ ion-toast,prop,message,IonicSafeString | string | undefined,undefined,false,fals
|
||||
ion-toast,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-toast,prop,position,"bottom" | "middle" | "top",'bottom',false,false
|
||||
ion-toast,prop,positionAnchor,HTMLElement | string | undefined,undefined,false,false
|
||||
ion-toast,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
|
||||
ion-toast,prop,swipeGesture,"vertical" | undefined,undefined,false,false
|
||||
ion-toast,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
ion-toast,prop,translucent,boolean,false,false,false
|
||||
|
8
core/src/components.d.ts
vendored
@ -3771,6 +3771,10 @@ export namespace Components {
|
||||
* Present the toast overlay after it has been created.
|
||||
*/
|
||||
"present": () => Promise<void>;
|
||||
/**
|
||||
* Set to `"soft"` for a toast with slightly rounded corners, `"round"` for a toast with fully rounded corners, or `"rectangular"` for a toast without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* If set to 'vertical', the Toast can be dismissed with a swipe gesture. The swipe direction is determined by the value of the `position` property: `top`: The Toast can be swiped up to dismiss. `bottom`: The Toast can be swiped down to dismiss. `middle`: The Toast can be swiped up or down to dismiss.
|
||||
*/
|
||||
@ -9161,6 +9165,10 @@ declare namespace LocalJSX {
|
||||
* The element to anchor the toast's position to. Can be set as a direct reference or the ID of the element. With `position="bottom"`, the toast will sit above the chosen element. With `position="top"`, the toast will sit below the chosen element. With `position="middle"`, the value of `positionAnchor` is ignored.
|
||||
*/
|
||||
"positionAnchor"?: HTMLElement | string;
|
||||
/**
|
||||
* Set to `"soft"` for a toast with slightly rounded corners, `"round"` for a toast with fully rounded corners, or `"rectangular"` for a toast without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* If set to 'vertical', the Toast can be dismissed with a swipe gesture. The swipe direction is determined by the value of the `position` property: `top`: The Toast can be swiped up to dismiss. `bottom`: The Toast can be swiped down to dismiss. `middle`: The Toast can be swiped up or down to dismiss.
|
||||
*/
|
||||
|
66
core/src/components/toast/test/shape/index.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Toast - Shape</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>
|
||||
</head>
|
||||
<script type="module">
|
||||
import { toastController } from '../../../../dist/ionic/index.esm.js';
|
||||
window.toastController = toastController;
|
||||
</script>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Toast - Shape</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding" id="content">
|
||||
(Only available for ionic theme)
|
||||
<button class="expand" id="default-toast" onclick="openToast({ message: 'Hello, world!', duration: 2000 })">
|
||||
Default Toast
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="expand"
|
||||
id="soft-shape-toast"
|
||||
onclick="openToast({ message: 'Hello, world!', duration: 2000, shape: 'soft' })"
|
||||
>
|
||||
Soft Toast
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="expand"
|
||||
id="round-shape-toast"
|
||||
onclick="openToast({ message: 'Hello, world!', duration: 2000, shape: 'round' })"
|
||||
>
|
||||
Round Toast
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="expand"
|
||||
id="rect-shape-toast"
|
||||
onclick="openToast({ message: 'Hello, world!', duration: 2000, shape: 'rectangular' })"
|
||||
>
|
||||
Rectangular Toast
|
||||
</button>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
<script>
|
||||
async function openToast(opts) {
|
||||
const toast = await toastController.create(opts);
|
||||
await toast.present();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
78
core/src/components/toast/test/shape/toast.e2e.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { expect, type Locator } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import type { E2EPage, E2EPageOptions, ScreenshotFn, EventSpy } from '@utils/test/playwright';
|
||||
|
||||
class ToastFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private ionToastDidPresent!: EventSpy;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async goto(config: E2EPageOptions) {
|
||||
const { page } = this;
|
||||
await page.goto(`/src/components/toast/test/shape`, config);
|
||||
this.ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
}
|
||||
|
||||
async openToast(selector: string) {
|
||||
const { page, ionToastDidPresent } = this;
|
||||
const button = page.locator(selector);
|
||||
await button.click();
|
||||
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
return {
|
||||
toast: page.locator('ion-toast'),
|
||||
container: page.locator('ion-toast .toast-container'),
|
||||
};
|
||||
}
|
||||
|
||||
async screenshot(screenshotModifier: string, screenshot: ScreenshotFn, el?: Locator) {
|
||||
const { page } = this;
|
||||
|
||||
const screenshotString = screenshot(`toast-${screenshotModifier}`);
|
||||
|
||||
if (el === undefined) {
|
||||
await expect(page).toHaveScreenshot(screenshotString);
|
||||
} else {
|
||||
await expect(el).toHaveScreenshot(screenshotString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('toast: shape'), () => {
|
||||
let toastFixture: ToastFixture;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
toastFixture = new ToastFixture(page);
|
||||
await toastFixture.goto(config);
|
||||
});
|
||||
|
||||
test('should render the default toast', async () => {
|
||||
await toastFixture.openToast('#default-toast');
|
||||
await toastFixture.screenshot('shape-round', screenshot);
|
||||
});
|
||||
|
||||
test('should render a soft toast', async () => {
|
||||
await toastFixture.openToast('#soft-shape-toast');
|
||||
await toastFixture.screenshot('shape-soft', screenshot);
|
||||
});
|
||||
|
||||
test('should render a round toast', async () => {
|
||||
await toastFixture.openToast('#round-shape-toast');
|
||||
await toastFixture.screenshot('shape-round', screenshot);
|
||||
});
|
||||
|
||||
test('should render a rectangular toast', async () => {
|
||||
await toastFixture.openToast('#rect-shape-toast');
|
||||
await toastFixture.screenshot('shape-rectangular', screenshot);
|
||||
});
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 17 KiB |
@ -6,7 +6,6 @@
|
||||
|
||||
:host {
|
||||
--background: #{globals.$ionic-color-neutral-1200};
|
||||
--border-radius: #{globals.$ionic-border-radius-400};
|
||||
--box-shadow: #{globals.$ionic-elevation-400};
|
||||
--button-color: #{globals.$ionic-color-base-white};
|
||||
--color: #{globals.$ionic-color-base-white};
|
||||
@ -36,6 +35,21 @@
|
||||
@include globals.padding(globals.$ionic-space-300, globals.$ionic-space-400);
|
||||
}
|
||||
|
||||
// Toast Shapes
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.toast-shape-soft) {
|
||||
--border-radius: #{globals.$ionic-border-radius-200};
|
||||
}
|
||||
|
||||
:host(.toast-shape-round) {
|
||||
--border-radius: #{globals.$ionic-border-radius-400};
|
||||
}
|
||||
|
||||
:host(.toast-shape-rectangular) {
|
||||
--border-radius: #{globals.$ionic-border-radius-0};
|
||||
}
|
||||
|
||||
// Toast Header
|
||||
// --------------------------------------------------
|
||||
|
||||
|
@ -172,6 +172,15 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() positionAnchor?: HTMLElement | string;
|
||||
|
||||
/**
|
||||
* Set to `"soft"` for a toast with slightly rounded corners,
|
||||
* `"round"` for a toast with fully rounded corners, or `"rectangular"`
|
||||
* for a toast without rounded corners.
|
||||
*
|
||||
* Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
@Prop() shape?: 'soft' | 'round' | 'rectangular';
|
||||
|
||||
/**
|
||||
* An array of buttons for the toast.
|
||||
*/
|
||||
@ -484,6 +493,21 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private getShape(): string | undefined {
|
||||
const { shape } = this;
|
||||
|
||||
// TODO(ROU-11300): Remove theme check when shapes are defined for all themes.
|
||||
if (getIonTheme(this) !== 'ionic') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (shape === undefined) {
|
||||
return 'round';
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element specified by the positionAnchor prop,
|
||||
* or undefined if prop's value is an ID string and the element
|
||||
@ -696,6 +720,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
const startButtons = allButtons.filter((b) => b.side === 'start');
|
||||
const endButtons = allButtons.filter((b) => b.side !== 'start');
|
||||
const theme = getIonTheme(this);
|
||||
const shape = this.getShape();
|
||||
const wrapperClass = {
|
||||
'toast-wrapper': true,
|
||||
[`toast-${this.position}`]: true,
|
||||
@ -725,6 +750,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
...getClassMap(this.cssClass),
|
||||
'overlay-hidden': true,
|
||||
'toast-translucent': this.translucent,
|
||||
[`toast-shape-${shape}`]: shape !== undefined,
|
||||
})}
|
||||
onIonToastWillDismiss={this.dispatchCancelHandler}
|
||||
>
|
||||
|
@ -2341,7 +2341,7 @@ export declare interface IonTitle extends Components.IonTitle {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
|
||||
})
|
||||
@Component({
|
||||
@ -2349,7 +2349,7 @@ export declare interface IonTitle extends Components.IonTitle {}
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
})
|
||||
export class IonToast {
|
||||
protected el: HTMLElement;
|
||||
|
@ -2092,7 +2092,7 @@ export declare interface IonTitle extends Components.IonTitle {}
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonToast,
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
|
||||
})
|
||||
@Component({
|
||||
@ -2100,7 +2100,7 @@ export declare interface IonTitle extends Components.IonTitle {}
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
|
||||
standalone: true
|
||||
})
|
||||
export class IonToast {
|
||||
|
@ -25,7 +25,7 @@ export const IonLoading = /*@__PURE__*/ defineOverlayContainer<JSX.IonLoading>('
|
||||
|
||||
export const IonPickerLegacy = /*@__PURE__*/ defineOverlayContainer<JSX.IonPickerLegacy>('ion-picker-legacy', defineIonPickerLegacyCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'theme', 'trigger']);
|
||||
|
||||
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'theme', 'translucent', 'trigger']);
|
||||
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger']);
|
||||
|
||||
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'focusTrap', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'shape', 'showBackdrop', 'theme', 'trigger'], true);
|
||||
|
||||
|