From ab65e9a7b51c3a3f8c59962d3e1faff1564ab801 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Thu, 21 Jul 2022 16:14:42 -0400 Subject: [PATCH 1/3] fix(vue): input v-model accepts numbers (#25666) Resolves #25575 --- core/package-lock.json | 14 +++++++------- core/package.json | 2 +- packages/vue/src/vue-component-lib/utils.ts | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) 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/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); From db28794f0b75f2824ae26c101a8c52af70f43ffd Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Mon, 25 Jul 2022 14:13:00 -0400 Subject: [PATCH 2/3] fix(nav): pop() will unmount views within a modal (#25638) Resolves #25637, #21831 --- core/src/components/nav/nav.tsx | 20 +++- .../nav/test/modal-navigation/index.html | 103 ++++++++++++++++++ .../nav/test/modal-navigation/nav.e2e.ts | 77 +++++++++++++ 3 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 core/src/components/nav/test/modal-navigation/index.html create mode 100644 core/src/components/nav/test/modal-navigation/nav.e2e.ts 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(); +}; From 57a21adb38331ee5d74dacd1b0a2568f41a2d21e Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 26 Jul 2022 13:40:05 -0400 Subject: [PATCH 3/3] fix(datetime): switching presentation closes month/year picker (#25667) --- core/src/components/datetime/datetime.tsx | 7 +++++++ .../test/presentation/datetime.e2e.ts | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 86b9b7ca4e..8008e4a8fe 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1027,6 +1027,13 @@ export class Datetime implements ComponentInterface { this.destroyInteractionListeners(); 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; } private processValue = (value?: string | null) => { 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', () => {