feat(toast): allow custom positioning relative to specific element (#28248)

Issue number: resolves #17499

---------

<!-- 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?
<!-- Please describe the current behavior that you are modifying. -->

Currently, there isn't a way to position toasts such that they don't
overlap navigation elements such as headers, footers, and FABs.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Added the new `positionAnchor` property, which specifies an element that
the toast's position should be anchored to.

While the name can be tweaked, we should take care to keep the relation
between it and the `position` property clear. The `position` acts as a
sort of "origin" point, and the toast is moved from there to sit near
the chosen anchor element. This is important because it helps clarify
why the toast sits above the anchor for `position="bottom"` and vice
versa.

I chose not to rename the `position` prop itself to avoid breaking
changes.

Docs PR: https://github.com/ionic-team/ionic-docs/pull/3158

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------

Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
This commit is contained in:
Amanda Johnston
2023-10-04 14:06:27 -05:00
committed by GitHub
parent 01167fc185
commit 897ff6f749
36 changed files with 376 additions and 29 deletions

View File

@ -1445,6 +1445,7 @@ ion-toast,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefin
ion-toast,prop,message,IonicSafeString | string | undefined,undefined,false,false
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,translucent,boolean,false,false,false
ion-toast,prop,trigger,string | undefined,undefined,false,false
ion-toast,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>

View File

@ -39,7 +39,7 @@ import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./com
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
import { ToastButton, ToastLayout, ToastPosition } from "./components/toast/toast-interface";
import { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions } from "./components/toast/toast-interface";
import { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export { AccordionGroupChangeEventDetail } from "./components/accordion-group/accordion-group-interface";
export { AnimationBuilder, AutocompleteTypes, Color, ComponentProps, ComponentRef, FrameworkDelegate, StyleEventDetail, TextFieldTypes } from "./interface";
@ -75,7 +75,7 @@ export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./com
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
export { ToastButton, ToastLayout, ToastPosition } from "./components/toast/toast-interface";
export { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions } from "./components/toast/toast-interface";
export { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export namespace Components {
interface IonAccordion {
@ -3157,9 +3157,13 @@ export namespace Components {
"onWillDismiss": <T = any>() => Promise<OverlayEventDetail<T>>;
"overlayIndex": number;
/**
* The position of the toast on the screen.
* The starting position of the toast on the screen. Can be tweaked further using the `positionAnchor` property.
*/
"position": ToastPosition;
/**
* 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;
/**
* Present the toast overlay after it has been created.
*/
@ -7307,9 +7311,13 @@ declare namespace LocalJSX {
"onWillPresent"?: (event: IonToastCustomEvent<void>) => void;
"overlayIndex": number;
/**
* The position of the toast on the screen.
* The starting position of the toast on the screen. Can be tweaked further using the `positionAnchor` property.
*/
"position"?: ToastPosition;
/**
* 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;
/**
* If `true`, the toast will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).
*/

View File

@ -2,20 +2,19 @@ import { createAnimation } from '@utils/animation/animation';
import { getElementRoot } from '@utils/helpers';
import type { Animation } from '../../../interface';
import type { ToastPresentOptions } from '../toast-interface';
/**
* iOS Toast Enter Animation
*/
export const iosEnterAnimation = (baseEl: HTMLElement, position: string): Animation => {
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ToastPresentOptions): Animation => {
const baseAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const { position, top, bottom } = opts;
const root = getElementRoot(baseEl);
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
wrapperAnimation.addElement(wrapperEl);
switch (position) {

View File

@ -1,21 +1,19 @@
import { createAnimation } from '@utils/animation/animation';
import { getElementRoot } from '@utils/helpers';
import type { Animation } from '../../../interface';
import type { Animation, ToastDismissOptions } from '../../../interface';
/**
* iOS Toast Leave Animation
*/
export const iosLeaveAnimation = (baseEl: HTMLElement, position: string): Animation => {
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ToastDismissOptions): Animation => {
const baseAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const { position, top, bottom } = opts;
const root = getElementRoot(baseEl);
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
const bottom = `calc(-10px - var(--ion-safe-area-bottom, 0px))`;
const top = `calc(10px + var(--ion-safe-area-top, 0px))`;
wrapperAnimation.addElement(wrapperEl);
switch (position) {

View File

@ -2,20 +2,19 @@ import { createAnimation } from '@utils/animation/animation';
import { getElementRoot } from '@utils/helpers';
import type { Animation } from '../../../interface';
import type { ToastPresentOptions } from '../toast-interface';
/**
* MD Toast Enter Animation
*/
export const mdEnterAnimation = (baseEl: HTMLElement, position: string): Animation => {
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ToastPresentOptions): Animation => {
const baseAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const { position, top, bottom } = opts;
const root = getElementRoot(baseEl);
const wrapperEl = root.querySelector('.toast-wrapper') as HTMLElement;
const bottom = `calc(8px + var(--ion-safe-area-bottom, 0px))`;
const top = `calc(8px + var(--ion-safe-area-top, 0px))`;
wrapperAnimation.addElement(wrapperEl);
switch (position) {

View File

@ -0,0 +1,95 @@
import { win } from '@utils/browser';
import { printIonWarning } from '@utils/logging';
import type { Mode } from 'src/interface';
import type { ToastAnimationPosition, ToastPosition } from '../toast-interface';
/**
* Calculate the CSS top and bottom position of the toast, to be used
* as starting points for the animation keyframes.
*
* Note that MD animates bottom-positioned toasts using style.bottom,
* which calculates from the bottom edge of the screen, while iOS uses
* translateY, which calculates from the top edge of the screen. This
* is why the bottom calculates differ slightly between modes.
*
* @param position The value of the toast's position prop.
* @param positionAnchor The element the toast should be anchored to,
* if applicable.
* @param mode The toast component's mode (md, ios, etc).
* @param toast A reference to the toast element itself.
*/
export function getAnimationPosition(
position: ToastPosition,
positionAnchor: HTMLElement | undefined,
mode: Mode,
toast: HTMLElement
): ToastAnimationPosition {
/**
* Start with a predefined offset from the edge the toast will be
* positioned relative to, whether on the screen or anchor element.
*/
let offset: number;
if (mode === 'md') {
offset = 8;
} else {
offset = position === 'top' ? 10 : -10;
}
/**
* If positionAnchor is defined, add in the distance from the target
* screen edge to the target anchor edge. For position="top", the
* bottom anchor edge is targeted. For position="bottom", the top
* anchor edge is targeted.
*/
if (positionAnchor && win) {
warnIfAnchorIsHidden(positionAnchor, toast);
const box = positionAnchor.getBoundingClientRect();
if (position === 'top') {
offset += box.bottom;
} else if (position === 'bottom') {
/**
* Just box.top is the distance from the top edge of the screen
* to the top edge of the anchor. We want to calculate from the
* bottom edge of the screen instead.
*/
if (mode === 'md') {
offset += win.innerHeight - box.top;
} else {
offset -= win.innerHeight - box.top;
}
}
/**
* We don't include safe area here because that should already be
* accounted for when checking the position of the anchor.
*/
return {
top: `${offset}px`,
bottom: `${offset}px`,
};
} else {
return {
top: `calc(${offset}px + var(--ion-safe-area-top, 0px))`,
bottom:
mode === 'md'
? `calc(${offset}px + var(--ion-safe-area-bottom, 0px))`
: `calc(${offset}px - var(--ion-safe-area-bottom, 0px))`,
};
}
}
/**
* If the anchor element is hidden, getBoundingClientRect()
* will return all 0s for it, which can cause unexpected
* results in the position calculation when animating.
*/
function warnIfAnchorIsHidden(positionAnchor: HTMLElement, toast: HTMLElement) {
if (positionAnchor.offsetParent === null) {
printIonWarning(
'The positionAnchor element for ion-toast was found in the DOM, but appears to be hidden. This may lead to unexpected positioning of the toast.',
toast
);
}
}

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Toast - positionAnchor</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 type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
html {
--ion-safe-area-top: 30px;
--ion-safe-area-bottom: 30px;
}
</style>
</head>
<body>
<ion-app>
<ion-header id="header">
<ion-toolbar>
<ion-title>Toast - positionAnchor</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button id="headerAnchor">Anchor to Header</ion-button>
<ion-button id="footerAnchor">Anchor to Footer</ion-button>
<ion-button id="middleAnchor">Anchor to Header (Middle Position)</ion-button>
<ion-button id="headerElAnchor">Anchor to Header (Element Ref)</ion-button>
<ion-button id="hiddenElAnchor">Anchor to Hidden Element</ion-button>
<ion-toast
id="headerToast"
trigger="headerAnchor"
position="top"
position-anchor="header"
message="Hello World"
duration="2000"
></ion-toast>
<ion-toast
id="footerToast"
trigger="footerAnchor"
position="bottom"
position-anchor="footer"
message="Hello World"
duration="2000"
></ion-toast>
<ion-toast
id="middleToast"
trigger="middleAnchor"
position="middle"
position-anchor="header"
message="Hello World"
duration="2000"
></ion-toast>
<ion-toast
id="headerElToast"
trigger="headerElAnchor"
position="top"
message="Hello World"
duration="2000"
></ion-toast>
<ion-toast
id="hiddenElToast"
trigger="hiddenElAnchor"
position="bottom"
position-anchor="hiddenEl"
message="Hello World"
duration="2000"
></ion-toast>
<div id="hiddenEl" style="display: none">Shh I'm hiding</div>
</ion-content>
<ion-footer id="footer">
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
</ion-app>
<script>
const headerElToast = document.querySelector('#headerElToast');
const header = document.querySelector('ion-header');
headerElToast.positionAnchor = header;
</script>
</body>
</html>

View File

@ -0,0 +1,56 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('toast: positionAnchor'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/toast/test/position-anchor', config);
/**
* We need to screenshot the whole page to ensure the toasts are positioned
* correctly, but we don't need much extra white space between the header
* and footer.
*/
await page.setViewportSize({
width: 425,
height: 425,
});
});
test('should place top-position toast underneath anchor', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#headerAnchor');
await ionToastDidPresent.next();
await expect(page).toHaveScreenshot(screenshot(`toast-header-anchor`));
});
test('should place bottom-position toast above anchor', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#footerAnchor');
await ionToastDidPresent.next();
await expect(page).toHaveScreenshot(screenshot(`toast-footer-anchor`));
});
test('should ignore anchor for middle-position toast', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#middleAnchor');
await ionToastDidPresent.next();
await expect(page).toHaveScreenshot(screenshot(`toast-middle-anchor`));
});
test('should correctly anchor toast when using an element reference', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#headerElAnchor');
await ionToastDidPresent.next();
await expect(page).toHaveScreenshot(screenshot(`toast-header-el-anchor`));
});
});
});

View File

@ -8,6 +8,7 @@ export interface ToastOptions {
duration?: number;
buttons?: (ToastButton | string)[];
position?: 'top' | 'bottom' | 'middle';
positionAnchor?: HTMLElement | string;
translucent?: boolean;
animated?: boolean;
icon?: string;
@ -42,3 +43,15 @@ export interface ToastButton {
}
export type ToastPosition = 'top' | 'bottom' | 'middle';
interface ToastPositionAlias {
position: ToastPosition;
}
export interface ToastAnimationPosition {
top: string;
bottom: string;
}
export type ToastPresentOptions = ToastPositionAlias & ToastAnimationPosition;
export type ToastDismissOptions = ToastPositionAlias & ToastAnimationPosition;

View File

@ -28,7 +28,15 @@ 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 { ToastButton, ToastPosition, ToastLayout } from './toast-interface';
import { getAnimationPosition } from './animations/utils';
import type {
ToastButton,
ToastPosition,
ToastLayout,
ToastPresentOptions,
ToastDismissOptions,
ToastAnimationPosition,
} from './toast-interface';
// TODO(FW-2832): types
@ -57,6 +65,13 @@ export class Toast implements ComponentInterface, OverlayInterface {
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
private durationTimeout?: ReturnType<typeof setTimeout>;
/**
* Holds the position of the toast calculated in the present
* animation, to be passed along to the dismiss animation so
* we don't have to calculate the position twice.
*/
private lastPresentedPosition?: ToastAnimationPosition;
presented = false;
/**
@ -137,10 +152,19 @@ export class Toast implements ComponentInterface, OverlayInterface {
@Prop() keyboardClose = false;
/**
* The position of the toast on the screen.
* The starting position of the toast on the screen. Can be tweaked further
* using the `positionAnchor` property.
*/
@Prop() position: ToastPosition = 'bottom';
/**
* 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.
*/
@Prop() positionAnchor?: HTMLElement | string;
/**
* An array of buttons for the toast.
*/
@ -275,7 +299,21 @@ export class Toast implements ComponentInterface, OverlayInterface {
await this.delegateController.attachViewToDom();
await present<ToastPresentOptions>(this, 'toastEnter', iosEnterAnimation, mdEnterAnimation, this.position);
const { el, position } = this;
const anchor = this.getAnchorElement();
const animationPosition = getAnimationPosition(position, anchor, getIonMode(this), el);
/**
* Cache the calculated position of the toast, so we can re-use it
* in the dismiss animation.
*/
this.lastPresentedPosition = animationPosition;
await present<ToastPresentOptions>(this, 'toastEnter', iosEnterAnimation, mdEnterAnimation, {
position,
top: animationPosition.top,
bottom: animationPosition.bottom,
});
/**
* Content is revealed to screen readers after
@ -304,8 +342,10 @@ export class Toast implements ComponentInterface, OverlayInterface {
async dismiss(data?: any, role?: string): Promise<boolean> {
const unlock = await this.lockController.lock();
if (this.durationTimeout) {
clearTimeout(this.durationTimeout);
const { durationTimeout, position, lastPresentedPosition } = this;
if (durationTimeout) {
clearTimeout(durationTimeout);
}
const dismissed = await dismiss<ToastDismissOptions>(
@ -315,7 +355,16 @@ export class Toast implements ComponentInterface, OverlayInterface {
'toastLeave',
iosLeaveAnimation,
mdLeaveAnimation,
this.position
/**
* Fetch the cached position that was calculated back in the present
* animation. We always want to animate the dismiss from the same
* position the present stopped at, so the animation looks continuous.
*/
{
position,
top: lastPresentedPosition?.top ?? '',
bottom: lastPresentedPosition?.bottom ?? '',
}
);
if (dismissed) {
@ -323,6 +372,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
this.revealContentToScreenReader = false;
}
this.lastPresentedPosition = undefined;
unlock();
return dismissed;
@ -354,6 +404,43 @@ export class Toast implements ComponentInterface, OverlayInterface {
return buttons;
}
/**
* Returns the element specified by the positionAnchor prop,
* or undefined if prop's value is an ID string and the element
* is not found in the DOM.
*/
private getAnchorElement(): HTMLElement | undefined {
const { position, positionAnchor, el } = this;
if (position === 'middle' && positionAnchor !== undefined) {
printIonWarning('The positionAnchor property is ignored when using position="middle".', this.el);
return undefined;
}
if (typeof positionAnchor === 'string') {
/**
* If the anchor is defined as an ID, find the element.
* We do this on every present so the toast doesn't need
* to account for the surrounding DOM changing since the
* last time it was presented.
*/
const foundEl = document.getElementById(positionAnchor);
if (foundEl === null) {
printIonWarning(`An anchor element with an ID of "${positionAnchor}" was not found in the DOM.`, el);
return undefined;
}
return foundEl;
}
if (positionAnchor instanceof HTMLElement) {
return positionAnchor;
}
printIonWarning('Invalid positionAnchor value:', positionAnchor, el);
return undefined;
}
private async buttonClick(button: ToastButton) {
const role = button.role;
if (isCancel(role)) {
@ -577,6 +664,3 @@ const buttonClass = (button: ToastButton): CssClassMap => {
const buttonPart = (button: ToastButton): string => {
return isCancel(button.role) ? 'button cancel' : 'button';
};
type ToastPresentOptions = ToastPosition;
type ToastDismissOptions = ToastPosition;

View File

@ -2256,7 +2256,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', 'translucent', 'trigger'],
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'translucent', 'trigger'],
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
})
@Component({
@ -2264,7 +2264,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', 'translucent', 'trigger'],
inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'translucent', 'trigger'],
})
export class IonToast {
protected el: HTMLElement;

View File

@ -25,7 +25,7 @@ export const IonLoading = /*@__PURE__*/ defineOverlayContainer<JSX.IonLoading>('
export const IonPicker = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicker>('ion-picker', defineIonPickerCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', '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', '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', 'translucent', 'trigger']);
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger'], true);