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 = () => {