Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2016,9 +2016,11 @@ ion-toast,css-prop,--width,md
ion-toast,part,button
ion-toast,part,button cancel
ion-toast,part,container
ion-toast,part,content
ion-toast,part,header
ion-toast,part,icon
ion-toast,part,message
ion-toast,part,wrapper

ion-toggle,shadow
ion-toggle,prop,alignment,"center" | "start" | undefined,undefined,false,false
Expand Down
133 changes: 133 additions & 0 deletions core/src/components/toast/test/custom/toast.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

/**
* This behavior does not vary across directions
*/
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('toast: custom'), () => {
test('should be able to customize toast wrapper, container, and content using css parts', async ({ page }) => {
await page.setContent(
`
<ion-toast is-open="true" header="Header" message="Hello World"></ion-toast>

<style>
ion-toast::part(wrapper) {
background-color: red;
}
ion-toast::part(container) {
background-color: green;
}
ion-toast::part(content) {
background-color: blue;
}
</style>
`,
config
);

const wrapperColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="wrapper"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).backgroundColor : null;
});

expect(wrapperColor).toBe('rgb(255, 0, 0)');

const containerColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="container"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).backgroundColor : null;
});

expect(containerColor).toBe('rgb(0, 128, 0)');

const contentColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="content"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).backgroundColor : null;
});

expect(contentColor).toBe('rgb(0, 0, 255)');
});

test('should be able to customize toast header and message using css parts', async ({ page }) => {
await page.setContent(
`
<ion-toast is-open="true" header="Header" message="Hello World"></ion-toast>

<style>
ion-toast::part(header) {
color: red;
}
ion-toast::part(message) {
color: green;
}
</style>
`,
config
);

const headerColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="header"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).color : null;
});

expect(headerColor).toBe('rgb(255, 0, 0)');

const messageColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="message"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).color : null;
});

expect(messageColor).toBe('rgb(0, 128, 0)');
});

test('should be able to customize toast icon, button, and button cancel using css parts', async ({ page }) => {
await page.setContent(
`
<ion-toast is-open="true" header="Header" message="Hello World" icon="alert"></ion-toast>

<style>
ion-toast::part(icon) {
color: red;
}
ion-toast::part(button) {
color: green;
}
ion-toast::part(button cancel) {
color: blue;
}
</style>

<script>
const toast = document.querySelector('ion-toast');
toast.buttons = [
{ text: 'Cancel', role: 'cancel' },
{ text: 'OK' }
];
</script>
`,
config
);

const iconColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="icon"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).color : null;
});

expect(iconColor).toBe('rgb(255, 0, 0)');

const buttonColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="button"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).color : null;
});

expect(buttonColor).toBe('rgb(0, 128, 0)');

const buttonCancelColor = await page.locator('ion-toast').evaluate((el: any) => {
const partEl = el.shadowRoot?.querySelector('[part="button cancel"]') as HTMLElement | null;
return partEl ? getComputedStyle(partEl).color : null;
});

expect(buttonCancelColor).toBe('rgb(0, 0, 255)');
});
});
});
12 changes: 7 additions & 5 deletions core/src/components/toast/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ import type {
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @part button - Any button element that is displayed inside of the toast.
* @part button cancel - Any button element with role "cancel" that is displayed inside of the toast.
* @part container - The element that wraps all child elements.
* @part wrapper - The outer wrapper for the toast overlay.
* @part container - Groups the icon, content, and buttons.
* @part content - The live region that contains the header and message.
* @part header - The header text of the toast.
* @part message - The body text of the toast.
* @part icon - The icon that appears next to the toast content.
* @part button - Any button element that is displayed inside of the toast.
* @part button cancel - Any button element with role "cancel" that is displayed inside of the toast.
*/
@Component({
tag: 'ion-toast',
Expand Down Expand Up @@ -727,7 +729,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
})}
onIonToastWillDismiss={this.dispatchCancelHandler}
>
<div class={wrapperClass}>
<div class={wrapperClass} part="wrapper">
<div class="toast-container" part="container">
{this.renderButtons(startButtons, 'start')}

Expand All @@ -746,7 +748,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
not interrupt the user which is why this has
a "status" role and a "polite" presentation.
*/}
<div class="toast-content" role="status" aria-atomic="true" aria-live="polite">
<div class="toast-content" part="content" role="status" aria-atomic="true" aria-live="polite">
{/*
This logic below is done to improve consistency
across platforms when showing and updating live regions.
Expand Down
Loading