diff --git a/core/package-lock.json b/core/package-lock.json index e622f693c6..700354524b 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -25,7 +25,7 @@ "@stencil/angular-output-target": "^0.4.0", "@stencil/react-output-target": "^0.2.1", "@stencil/sass": "^1.5.2", - "@stencil/vue-output-target": "^0.6.1", + "@stencil/vue-output-target": "^0.6.2", "@types/jest": "^26.0.20", "@types/node": "^14.6.0", "@types/swiper": "5.4.0", @@ -1839,9 +1839,9 @@ } }, "node_modules/@stencil/vue-output-target": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.1.tgz", - "integrity": "sha512-JGyl3Bi2NJRDz64c2lFAP6zdRwMD12ruWcbT75VdcLVDmCwo+wqWs/Shj4ZWXlcNhzjxbf9vydtQFwVMld/NrA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz", + "integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==", "dev": true, "peerDependencies": { "@stencil/core": "^2.9.0" @@ -15570,9 +15570,9 @@ "dev": true }, "@stencil/vue-output-target": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.1.tgz", - "integrity": "sha512-JGyl3Bi2NJRDz64c2lFAP6zdRwMD12ruWcbT75VdcLVDmCwo+wqWs/Shj4ZWXlcNhzjxbf9vydtQFwVMld/NrA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz", + "integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==", "dev": true }, "@stylelint/postcss-css-in-js": { diff --git a/core/package.json b/core/package.json index 3222b2a625..8447400701 100644 --- a/core/package.json +++ b/core/package.json @@ -47,7 +47,7 @@ "@stencil/angular-output-target": "^0.4.0", "@stencil/react-output-target": "^0.2.1", "@stencil/sass": "^1.5.2", - "@stencil/vue-output-target": "^0.6.1", + "@stencil/vue-output-target": "^0.6.2", "@types/jest": "^26.0.20", "@types/node": "^14.6.0", "@types/swiper": "5.4.0", diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 5ed5967bc9..2bd214e74d 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1128,6 +1128,13 @@ export class Datetime implements ComponentInterface { this.initializeListeners(); + /** + * The month/year picker from the date interface + * should be closed as it is not available in non-date + * interfaces. + */ + this.showMonthAndYear = false; + raf(() => { this.ionRender.emit(); }); diff --git a/core/src/components/datetime/test/presentation/datetime.e2e.ts b/core/src/components/datetime/test/presentation/datetime.e2e.ts index 8f254747c2..137c625fad 100644 --- a/core/src/components/datetime/test/presentation/datetime.e2e.ts +++ b/core/src/components/datetime/test/presentation/datetime.e2e.ts @@ -83,6 +83,27 @@ test.describe('datetime: presentation', () => { expect(ionChangeSpy.length).toBe(1); }); + + test('switching presentation should close month/year picker', async ({ page }, testInfo) => { + await test.skip(testInfo.project.metadata.rtl === true, 'This feature does not have RTL specific behaviors.'); + + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const datetime = page.locator('ion-datetime'); + const monthYearButton = page.locator('ion-datetime .calendar-month-year'); + await monthYearButton.click(); + + await expect(datetime).toHaveClass(/show-month-and-year/); + + await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.presentation = 'time')); + await page.waitForChanges(); + + await expect(datetime).not.toHaveClass(/show-month-and-year/); + }); }); test.describe('datetime: presentation: time', () => { diff --git a/core/src/components/nav/nav.tsx b/core/src/components/nav/nav.tsx index bf90a19e35..3f0efdcbfa 100644 --- a/core/src/components/nav/nav.tsx +++ b/core/src/components/nav/nav.tsx @@ -131,6 +131,10 @@ export class Nav implements NavOutlet { this.swipeGestureChanged(); } + connectedCallback() { + this.destroyed = false; + } + disconnectedCallback() { for (const view of this.views) { lifecycle(view.element!, LIFECYCLE_WILL_UNLOAD); @@ -879,9 +883,13 @@ export class Nav implements NavOutlet { leavingView: ViewController | undefined, opts: NavOptions ): NavResult { - const cleanupView = hasCompleted ? enteringView : leavingView; - if (cleanupView) { - this.cleanup(cleanupView); + /** + * If the transition did not complete, the leavingView will still be the active + * view on the stack. Otherwise unmount all the views after the enteringView. + */ + const activeView = hasCompleted ? enteringView : leavingView; + if (activeView) { + this.unmountInactiveViews(activeView); } return { @@ -944,9 +952,13 @@ export class Nav implements NavOutlet { } /** + * Unmounts all inactive views after the specified active view. + * * DOM WRITE + * + * @param activeView The view that is actively visible in the stack. Used to calculate which views to unmount. */ - private cleanup(activeView: ViewController) { + private unmountInactiveViews(activeView: ViewController) { // ok, cleanup time!! Destroy all of the views that are // INACTIVE and come after the active view // only do this if the views exist, though diff --git a/core/src/components/nav/test/modal-navigation/index.html b/core/src/components/nav/test/modal-navigation/index.html new file mode 100644 index 0000000000..a5dedf3a97 --- /dev/null +++ b/core/src/components/nav/test/modal-navigation/index.html @@ -0,0 +1,103 @@ + + + + + Nav - Modal Navigation + + + + + + + + + + + + + Modal Navigation + + + + Open Modal + + + + Modal + + Close + + + + + + + + + + + + + diff --git a/core/src/components/nav/test/modal-navigation/nav.e2e.ts b/core/src/components/nav/test/modal-navigation/nav.e2e.ts new file mode 100644 index 0000000000..71a7308c81 --- /dev/null +++ b/core/src/components/nav/test/modal-navigation/nav.e2e.ts @@ -0,0 +1,77 @@ +import { expect } from '@playwright/test'; +import type { E2EPage } from '@utils/test/playwright'; +import { test } from '@utils/test/playwright'; + +test.describe('nav: modal-navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/nav/test/modal-navigation`); + await openModal(page); + }); + + test('should render the root page', async ({ page }) => { + const pageOne = page.locator('page-one'); + const pageOneHeading = page.locator('page-one h1'); + + await expect(pageOne).toBeVisible(); + await expect(pageOneHeading).toHaveText('Page One'); + }); + + test('should push to the next page', async ({ page }) => { + await page.click('#goto-page-two'); + + const pageTwo = page.locator('page-two'); + const pageTwoHeading = page.locator('page-two h1'); + + await expect(pageTwo).toBeVisible(); + await expect(pageTwoHeading).toHaveText('Page Two'); + }); + + test('should pop to the previous page', async ({ page }) => { + await page.click('#goto-page-two'); + await page.click('#goto-page-three'); + + const pageThree = page.locator('page-three'); + const pageThreeHeading = page.locator('page-three h1'); + + await expect(pageThree).toBeVisible(); + await expect(pageThreeHeading).toHaveText('Page Three'); + + await page.click('#go-back'); + + const pageTwo = page.locator('page-two'); + const pageTwoHeading = page.locator('page-two h1'); + + // Verifies the leavingView was unmounted + await expect(pageThree).toHaveCount(0); + await expect(pageTwo).toBeVisible(); + await expect(pageTwoHeading).toHaveText('Page Two'); + }); + + test.describe('popping to the root', () => { + test('should render the root page', async ({ page }) => { + const pageTwo = page.locator('page-two'); + const pageThree = page.locator('page-three'); + + await page.click('#goto-page-two'); + await page.click('#goto-page-three'); + + await page.click('#goto-root'); + + const pageOne = page.locator('page-one'); + const pageOneHeading = page.locator('page-one h1'); + + // Verifies all views besides the root were unmounted + await expect(pageTwo).toHaveCount(0); + await expect(pageThree).toHaveCount(0); + + await expect(pageOne).toBeVisible(); + await expect(pageOneHeading).toHaveText('Page One'); + }); + }); +}); + +const openModal = async (page: E2EPage) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + await page.click('#openModal'); + await ionModalDidPresent.next(); +}; diff --git a/packages/vue/src/vue-component-lib/utils.ts b/packages/vue/src/vue-component-lib/utils.ts index c04c013970..e48debacfa 100644 --- a/packages/vue/src/vue-component-lib/utils.ts +++ b/packages/vue/src/vue-component-lib/utils.ts @@ -1,7 +1,7 @@ import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue'; -export interface InputProps { - modelValue?: string | boolean; +export interface InputProps { + modelValue?: T; } const UPDATE_VALUE_EVENT = 'update:modelValue'; @@ -49,7 +49,7 @@ const getElementClasses = (ref: Ref, componentClasses: * @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been * correctly updated when a user's event callback fires. */ -export const defineContainer = ( +export const defineContainer = ( name: string, defineCustomElement: any, componentProps: string[] = [], @@ -67,7 +67,7 @@ export const defineContainer = ( defineCustomElement(); } - const Container = defineComponent((props: any, { attrs, slots, emit }) => { + const Container = defineComponent>((props: any, { attrs, slots, emit }) => { let modelPropValue = props[modelProp]; const containerRef = ref(); const classes = new Set(getComponentClasses(attrs.class)); @@ -76,7 +76,7 @@ export const defineContainer = ( if (vnode.el) { const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent]; eventsNames.forEach((eventName: string) => { - vnode.el.addEventListener(eventName.toLowerCase(), (e: Event) => { + vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => { modelPropValue = (e?.target as any)[modelProp]; emit(UPDATE_VALUE_EVENT, modelPropValue);