Skip to content

Commit 2a1addc

Browse files
feat(toast): add hue property for the ionic theme (#30333)
Issue number: internal --------- ## What is the current behavior? The toast component does not support the `hue` property. ## What is the new behavior? Adds support for the `subtle` hue for the toast. Defaults to `subtle`. ## Does this introduce a breaking change? - [ ] Yes - [x] No
1 parent 52fba11 commit 2a1addc

File tree

60 files changed

+244
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+244
-9
lines changed

core/api.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2436,6 +2436,7 @@ ion-toast,prop,duration,number,config.getNumber('toastDuration', 0),false,false
24362436
ion-toast,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
24372437
ion-toast,prop,header,string | undefined,undefined,false,false
24382438
ion-toast,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
2439+
ion-toast,prop,hue,"bold" | "subtle" | undefined,'subtle',false,false
24392440
ion-toast,prop,icon,string | undefined,undefined,false,false
24402441
ion-toast,prop,isOpen,boolean,false,false,false
24412442
ion-toast,prop,keyboardClose,boolean,false,false,false

core/src/components.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -3844,6 +3844,10 @@ export namespace Components {
38443844
* Additional attributes to pass to the toast.
38453845
*/
38463846
"htmlAttributes"?: { [key: string]: any };
3847+
/**
3848+
* Set to `"bold"` for a toast with vibrant, bold colors or to `"subtle"` for a toast with muted, subtle colors. Only applies to the `ionic` theme.
3849+
*/
3850+
"hue"?: 'bold' | 'subtle';
38473851
/**
38483852
* The name of the icon to display, or the path to a valid SVG file. See `ion-icon`. https://ionic.io/ionicons
38493853
*/
@@ -9384,6 +9388,10 @@ declare namespace LocalJSX {
93849388
* Additional attributes to pass to the toast.
93859389
*/
93869390
"htmlAttributes"?: { [key: string]: any };
9391+
/**
9392+
* Set to `"bold"` for a toast with vibrant, bold colors or to `"subtle"` for a toast with muted, subtle colors. Only applies to the `ionic` theme.
9393+
*/
9394+
"hue"?: 'bold' | 'subtle';
93879395
/**
93889396
* The name of the icon to display, or the path to a valid SVG file. See `ion-icon`. https://ionic.io/ionicons
93899397
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Toast - Hue</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
</head>
16+
17+
<body>
18+
<ion-app>
19+
<ion-header>
20+
<ion-toolbar>
21+
<ion-title>Toast - Hue</ion-title>
22+
</ion-toolbar>
23+
</ion-header>
24+
25+
<ion-content class="ion-padding" id="content">
26+
<p>
27+
Toasts are presented indefinitely but can be closed by clicking the backdrop. The backdrop has been added for
28+
demo purposes only.
29+
</p>
30+
31+
<button id="show-subtle-toasts" class="expand">Show All Subtle Toasts</button>
32+
<button id="show-bold-toasts" class="expand">Show All Bold Toasts</button>
33+
</ion-content>
34+
35+
<div id="backdrop" class="backdrop"></div>
36+
37+
<ion-toast position="top"></ion-toast>
38+
<ion-toast color="primary" position="top"></ion-toast>
39+
<ion-toast color="secondary" position="top"></ion-toast>
40+
<ion-toast color="tertiary" position="top"></ion-toast>
41+
<ion-toast color="success" position="top"></ion-toast>
42+
<ion-toast color="warning" position="top"></ion-toast>
43+
<ion-toast color="danger" position="top"></ion-toast>
44+
<ion-toast color="light" position="top"></ion-toast>
45+
<ion-toast color="medium" position="top"></ion-toast>
46+
<ion-toast color="dark" position="top"></ion-toast>
47+
</ion-app>
48+
49+
<script type="module">
50+
let lastToast = null;
51+
let toastOffset = 10;
52+
53+
// Show all toasts when the button is clicked
54+
function openAllToasts(isBold) {
55+
toastOffset = 10;
56+
57+
const toasts = document.querySelectorAll('ion-toast');
58+
59+
toasts.forEach((toast, index) => {
60+
toast.removeAttribute('hue');
61+
toast.hue = isBold ? 'bold' : 'subtle';
62+
const hue = toast.hue;
63+
64+
const color = toast.color || 'default';
65+
toast.message = `This is a ${color} toast.`;
66+
67+
toast.icon = 'information-circle';
68+
69+
toast.buttons = [
70+
{
71+
text: 'Action',
72+
},
73+
{
74+
icon: 'close',
75+
role: 'cancel',
76+
},
77+
];
78+
79+
// Set dynamic position for each toast to ensure they don't overlap
80+
toast.style.position = 'absolute';
81+
toast.style.top = `${toastOffset}px`;
82+
toast.style.left = '50%';
83+
toast.style.transform = 'translateX(-50%)';
84+
85+
toast.present();
86+
87+
// Update the toastOffset for the next toast to ensure it's positioned below the previous one
88+
toastOffset += 60;
89+
});
90+
91+
document.getElementById('backdrop').style.display = 'block';
92+
}
93+
94+
// Dismiss all toasts when backdrop is clicked
95+
function dismissAllToasts() {
96+
const toasts = document.querySelectorAll('ion-toast');
97+
toasts.forEach((toast) => toast.dismiss());
98+
99+
document.getElementById('backdrop').style.display = 'none';
100+
}
101+
102+
document.addEventListener('DOMContentLoaded', function () {
103+
document.getElementById('show-subtle-toasts').addEventListener('click', () => openAllToasts(false));
104+
document.getElementById('show-bold-toasts').addEventListener('click', () => openAllToasts(true));
105+
106+
document.getElementById('backdrop').addEventListener('click', dismissAllToasts);
107+
});
108+
</script>
109+
110+
<style>
111+
.backdrop {
112+
position: fixed;
113+
top: 0;
114+
left: 0;
115+
right: 0;
116+
bottom: 0;
117+
background: rgba(0, 0, 0, 0.5);
118+
z-index: 1000;
119+
display: none;
120+
}
121+
</style>
122+
</body>
123+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect } from '@playwright/test';
2+
import type { E2EPage, E2EPageOptions, ScreenshotFn, EventSpy } from '@utils/test/playwright';
3+
import { configs, test } from '@utils/test/playwright';
4+
5+
class ToastFixture {
6+
readonly page: E2EPage;
7+
8+
private ionToastDidPresent!: EventSpy;
9+
10+
constructor(page: E2EPage) {
11+
this.page = page;
12+
}
13+
14+
async goto(config: E2EPageOptions) {
15+
const { page } = this;
16+
await page.goto(`/src/components/toast/test/hue`, config);
17+
this.ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
18+
}
19+
20+
async openToast(selector: string) {
21+
const { page, ionToastDidPresent } = this;
22+
const button = page.locator(selector);
23+
await button.click();
24+
25+
await ionToastDidPresent.next();
26+
27+
return {
28+
toast: page.locator('ion-toast'),
29+
};
30+
}
31+
32+
async screenshot(screenshotModifier: string, screenshot: ScreenshotFn) {
33+
const { page } = this;
34+
35+
const screenshotString = screenshot(`toast-${screenshotModifier}`);
36+
37+
await expect(page).toHaveScreenshot(screenshotString);
38+
}
39+
}
40+
41+
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
42+
test.describe(title('toast: hue'), () => {
43+
let toastFixture: ToastFixture;
44+
test.beforeEach(async ({ page }) => {
45+
toastFixture = new ToastFixture(page);
46+
await toastFixture.goto(config);
47+
});
48+
49+
test('should show all subtle toasts', async () => {
50+
await toastFixture.openToast('#show-subtle-toasts');
51+
await toastFixture.screenshot('subtle', screenshot);
52+
});
53+
54+
test('should show all bold toasts', async () => {
55+
await toastFixture.openToast('#show-bold-toasts');
56+
await toastFixture.screenshot('bold', screenshot);
57+
});
58+
});
59+
});

core/src/components/toast/toast.ionic.scss

+38-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
// --------------------------------------------------
66

77
:host {
8-
--background: #{globals.$ion-primitives-neutral-1200};
98
--box-shadow: #{globals.$ion-elevation-4};
10-
--button-color: #{globals.$ion-primitives-base-white};
11-
--color: #{globals.$ion-primitives-base-white};
129
--max-width: 343px;
1310
--start: 8px;
1411
--end: 8px;
@@ -132,3 +129,41 @@
132129
.toast-button-icon {
133130
font-size: globals.$ion-scale-600;
134131
}
132+
133+
// Bold Toast
134+
// --------------------------------------------------
135+
136+
:host(.toast-hue-bold) {
137+
--background: #{globals.$ion-bg-neutral-boldest-default};
138+
--background-activated: #{globals.$ion-bg-neutral-boldest-press};
139+
--color: #{globals.$ion-text-inverse};
140+
--button-color: #{globals.$ion-text-inverse};
141+
}
142+
143+
:host(.toast-hue-bold.ion-color) .toast-wrapper {
144+
background: globals.current-color(base);
145+
color: globals.current-color(contrast);
146+
}
147+
148+
:host(.toast-hue-bold.ion-color) .toast-button {
149+
color: globals.current-color(contrast);
150+
}
151+
152+
// Subtle Toast
153+
// --------------------------------------------------
154+
155+
:host(.toast-hue-subtle) {
156+
--background: #{globals.$ion-bg-neutral-subtlest-default};
157+
--background-activated: #{globals.$ion-bg-neutral-subtlest-press};
158+
--color: #{globals.$ion-text-default};
159+
--button-color: #{globals.$ion-text-link-default};
160+
}
161+
162+
:host(.toast-hue-subtle) .toast-button-cancel {
163+
color: #{globals.$ion-icon-subtlest};
164+
}
165+
166+
:host(.toast-hue-subtle.ion-color) .toast-wrapper {
167+
background: globals.current-color(base, $subtle: true);
168+
color: globals.current-color(contrast, $subtle: true);
169+
}

core/src/components/toast/toast.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ export class Toast implements ComponentInterface, OverlayInterface {
135135
*/
136136
@Prop() header?: string;
137137

138+
/**
139+
* Set to `"bold"` for a toast with vibrant, bold colors or to `"subtle"` for
140+
* a toast with muted, subtle colors.
141+
*
142+
* Only applies to the `ionic` theme.
143+
*/
144+
@Prop() hue?: 'bold' | 'subtle' = 'subtle';
145+
138146
/**
139147
* Defines how the message and buttons are laid out in the toast.
140148
* 'baseline': The message and the buttons will appear on the same line.
@@ -715,7 +723,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
715723
}
716724

717725
render() {
718-
const { layout, el, revealContentToScreenReader, header, message } = this;
726+
const { layout, el, revealContentToScreenReader, header, hue, message } = this;
719727
const allButtons = this.getButtons();
720728
const startButtons = allButtons.filter((b) => b.side === 'start');
721729
const endButtons = allButtons.filter((b) => b.side !== 'start');
@@ -753,6 +761,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
753761
'overlay-hidden': true,
754762
'toast-translucent': this.translucent,
755763
[`toast-shape-${shape}`]: shape !== undefined,
764+
[`toast-hue-${hue}`]: hue !== undefined,
756765
})}
757766
onIonToastWillDismiss={this.dispatchCancelHandler}
758767
>

packages/angular/src/directives/proxies.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2439,15 +2439,15 @@ export declare interface IonTitle extends Components.IonTitle {}
24392439

24402440

24412441
@ProxyCmp({
2442-
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
2442+
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
24432443
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
24442444
})
24452445
@Component({
24462446
selector: 'ion-toast',
24472447
changeDetection: ChangeDetectionStrategy.OnPush,
24482448
template: '<ng-content></ng-content>',
24492449
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2450-
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
2450+
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
24512451
})
24522452
export class IonToast {
24532453
protected el: HTMLIonToastElement;

packages/angular/standalone/src/directives/proxies.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2202,15 +2202,15 @@ export declare interface IonTitle extends Components.IonTitle {}
22022202

22032203
@ProxyCmp({
22042204
defineCustomElementFn: defineIonToast,
2205-
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
2205+
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
22062206
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
22072207
})
22082208
@Component({
22092209
selector: 'ion-toast',
22102210
changeDetection: ChangeDetectionStrategy.OnPush,
22112211
template: '<ng-content></ng-content>',
22122212
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2213-
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
2213+
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'],
22142214
standalone: true
22152215
})
22162216
export class IonToast {

packages/vue/src/components/Overlays.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ export const IonPickerLegacy = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicke
2828

2929
export const IonPopover = /*@__PURE__*/ defineOverlayContainer<JSX.IonPopover>('ion-popover', defineIonPopoverCustomElement, ['alignment', 'animated', 'arrow', 'backdropDismiss', 'component', 'componentProps', 'dismissOnSelect', 'enterAnimation', 'event', 'focusTrap', 'htmlAttributes', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'reference', 'showBackdrop', 'side', 'size', 'theme', 'translucent', 'trigger', 'triggerAction']);
3030

31-
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']);
31+
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger']);
3232

0 commit comments

Comments
 (0)