diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx
index 0a2dd0f8a8..7d877fe39a 100644
--- a/core/src/components/modal/modal.tsx
+++ b/core/src/components/modal/modal.tsx
@@ -447,10 +447,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.currentBreakpoint = this.initialBreakpoint;
const { inline, delegate } = this.getDelegate(true);
- this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
+ /**
+ * Emit ionMount so JS Frameworks have an opportunity
+ * to add the child component to the DOM. The child
+ * component will be assigned to this.usersElement below.
+ */
this.ionMount.emit();
+ this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
+
/**
* When using the lazy loaded build of Stencil, we need to wait
* for every Stencil component instance to be ready before presenting
diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx
index d32856eedb..476b150d74 100644
--- a/core/src/components/popover/popover.tsx
+++ b/core/src/components/popover/popover.tsx
@@ -449,6 +449,14 @@ export class Popover implements ComponentInterface, PopoverInterface {
const { el } = this;
const { inline, delegate } = this.getDelegate(true);
+
+ /**
+ * Emit ionMount so JS Frameworks have an opportunity
+ * to add the child component to the DOM. The child
+ * component will be assigned to this.usersElement below.
+ */
+ this.ionMount.emit();
+
this.usersElement = await attachComponent(
delegate,
el,
@@ -463,8 +471,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
}
this.configureDismissInteraction();
- this.ionMount.emit();
-
/**
* When using the lazy loaded build of Stencil, we need to wait
* for every Stencil component instance to be ready before presenting
diff --git a/core/src/components/popover/test/async/index.html b/core/src/components/popover/test/async/index.html
new file mode 100644
index 0000000000..de480f6328
--- /dev/null
+++ b/core/src/components/popover/test/async/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Popover - Async
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/components/popover/test/async/popover.e2e.ts b/core/src/components/popover/test/async/popover.e2e.ts
new file mode 100644
index 0000000000..f1630ec19a
--- /dev/null
+++ b/core/src/components/popover/test/async/popover.e2e.ts
@@ -0,0 +1,54 @@
+import { expect } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * This behavior does not vary across modes/directions
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+ test.describe(title('popover: alignment with async component'), async () => {
+ test('should align popover centered with button when component is added async', async ({ page }) => {
+ await page.goto('/src/components/popover/test/async', config);
+
+ const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+
+ const button = page.locator('#button');
+ await button.click();
+
+ await ionPopoverDidPresent.next();
+
+ await expect(page).toHaveScreenshot(screenshot(`popover-async`));
+ });
+ /**
+ * Framework delegate should fall back to returning the host
+ * component when no child content is passed otherwise
+ * the overlay will get stuck when trying to re-present.
+ */
+ test('should open popover even if nothing was passed', async ({ page }) => {
+ await page.setContent(
+ `
+
+ `,
+ config
+ );
+
+ const popover = page.locator('ion-popover');
+ const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+ const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss');
+
+ await popover.evaluate((el: HTMLIonPopoverElement) => el.present());
+
+ await ionPopoverDidPresent.next();
+ await expect(popover).toBeVisible();
+
+ await popover.evaluate((el: HTMLIonPopoverElement) => el.dismiss());
+
+ await ionPopoverDidDismiss.next();
+ await expect(popover).toBeHidden();
+
+ await popover.evaluate((el: HTMLIonPopoverElement) => el.present());
+
+ await ionPopoverDidPresent.next();
+ await expect(popover).toBeVisible();
+ });
+ });
+});
diff --git a/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 0000000000..1dbf1755ef
Binary files /dev/null and b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 0000000000..c1be140389
Binary files /dev/null and b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Safari-linux.png b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 0000000000..3b0f0b0601
Binary files /dev/null and b/core/src/components/popover/test/async/popover.e2e.ts-snapshots/popover-async-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/utils/framework-delegate.ts b/core/src/utils/framework-delegate.ts
index 6c8d33a41c..6a2f7041cb 100644
--- a/core/src/utils/framework-delegate.ts
+++ b/core/src/utils/framework-delegate.ts
@@ -56,6 +56,7 @@ export const CoreDelegate = () => {
cssClasses: string[] = []
) => {
BaseComponent = parentElement;
+ let ChildComponent;
/**
* If passing in a component via the `component` props
* we need to append it inside of our overlay component.
@@ -87,6 +88,8 @@ export const CoreDelegate = () => {
*/
BaseComponent.appendChild(el);
+ ChildComponent = el;
+
await new Promise((resolve) => componentOnReady(el, resolve));
} else if (
BaseComponent.children.length > 0 &&
@@ -96,7 +99,7 @@ export const CoreDelegate = () => {
* The delegate host wrapper el is only needed for modals and popovers
* because they allow the dev to provide custom content to the overlay.
*/
- const root = BaseComponent.children[0] as HTMLElement;
+ const root = (ChildComponent = BaseComponent.children[0] as HTMLElement);
if (!root.classList.contains('ion-delegate-host')) {
/**
* If the root element is not a delegate host, it means
@@ -111,6 +114,13 @@ export const CoreDelegate = () => {
el.append(...BaseComponent.children);
// Append the new parent element to the original parent element.
BaseComponent.appendChild(el);
+
+ /**
+ * Update the ChildComponent to be the
+ * newly created div in the event that one
+ * does not already exist.
+ */
+ ChildComponent = el;
}
}
@@ -130,7 +140,18 @@ export const CoreDelegate = () => {
app.appendChild(BaseComponent);
- return BaseComponent;
+ /**
+ * We return the child component rather than the overlay
+ * reference itself since modal and popover will
+ * use this to wait for any Ionic components in the child view
+ * to be ready (i.e. componentOnReady) when using the
+ * lazy loaded component bundle.
+ *
+ * However, we fall back to returning BaseComponent
+ * in the event that a modal or popover is presented
+ * with no child content.
+ */
+ return ChildComponent ?? BaseComponent;
};
const removeViewFromDom = () => {