Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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