Skip to content

Commit 4d10ae6

Browse files
committed
chore(): sync feature-6.2 with main
2 parents 4997331 + 57a21ad commit 4d10ae6

File tree

8 files changed

+237
-17
lines changed

8 files changed

+237
-17
lines changed

core/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@stencil/angular-output-target": "^0.4.0",
4848
"@stencil/react-output-target": "^0.2.1",
4949
"@stencil/sass": "^1.5.2",
50-
"@stencil/vue-output-target": "^0.6.1",
50+
"@stencil/vue-output-target": "^0.6.2",
5151
"@types/jest": "^26.0.20",
5252
"@types/node": "^14.6.0",
5353
"@types/swiper": "5.4.0",

core/src/components/datetime/datetime.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,13 @@ export class Datetime implements ComponentInterface {
11281128

11291129
this.initializeListeners();
11301130

1131+
/**
1132+
* The month/year picker from the date interface
1133+
* should be closed as it is not available in non-date
1134+
* interfaces.
1135+
*/
1136+
this.showMonthAndYear = false;
1137+
11311138
raf(() => {
11321139
this.ionRender.emit();
11331140
});

core/src/components/datetime/test/presentation/datetime.e2e.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ test.describe('datetime: presentation', () => {
8383

8484
expect(ionChangeSpy.length).toBe(1);
8585
});
86+
87+
test('switching presentation should close month/year picker', async ({ page }, testInfo) => {
88+
await test.skip(testInfo.project.metadata.rtl === true, 'This feature does not have RTL specific behaviors.');
89+
90+
await page.setContent(`
91+
<ion-datetime presentation="date"></ion-datetime>
92+
`);
93+
94+
await page.waitForSelector('.datetime-ready');
95+
96+
const datetime = page.locator('ion-datetime');
97+
const monthYearButton = page.locator('ion-datetime .calendar-month-year');
98+
await monthYearButton.click();
99+
100+
await expect(datetime).toHaveClass(/show-month-and-year/);
101+
102+
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.presentation = 'time'));
103+
await page.waitForChanges();
104+
105+
await expect(datetime).not.toHaveClass(/show-month-and-year/);
106+
});
86107
});
87108

88109
test.describe('datetime: presentation: time', () => {

core/src/components/nav/nav.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ export class Nav implements NavOutlet {
131131
this.swipeGestureChanged();
132132
}
133133

134+
connectedCallback() {
135+
this.destroyed = false;
136+
}
137+
134138
disconnectedCallback() {
135139
for (const view of this.views) {
136140
lifecycle(view.element!, LIFECYCLE_WILL_UNLOAD);
@@ -879,9 +883,13 @@ export class Nav implements NavOutlet {
879883
leavingView: ViewController | undefined,
880884
opts: NavOptions
881885
): NavResult {
882-
const cleanupView = hasCompleted ? enteringView : leavingView;
883-
if (cleanupView) {
884-
this.cleanup(cleanupView);
886+
/**
887+
* If the transition did not complete, the leavingView will still be the active
888+
* view on the stack. Otherwise unmount all the views after the enteringView.
889+
*/
890+
const activeView = hasCompleted ? enteringView : leavingView;
891+
if (activeView) {
892+
this.unmountInactiveViews(activeView);
885893
}
886894

887895
return {
@@ -944,9 +952,13 @@ export class Nav implements NavOutlet {
944952
}
945953

946954
/**
955+
* Unmounts all inactive views after the specified active view.
956+
*
947957
* DOM WRITE
958+
*
959+
* @param activeView The view that is actively visible in the stack. Used to calculate which views to unmount.
948960
*/
949-
private cleanup(activeView: ViewController) {
961+
private unmountInactiveViews(activeView: ViewController) {
950962
// ok, cleanup time!! Destroy all of the views that are
951963
// INACTIVE and come after the active view
952964
// only do this if the views exist, though
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Nav - Modal Navigation</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
</head>
16+
17+
<body>
18+
<ion-app>
19+
<ion-header>
20+
<ion-toolbar>
21+
<ion-title>Modal Navigation</ion-title>
22+
</ion-toolbar>
23+
</ion-header>
24+
<ion-content>
25+
<ion-button id="openModal">Open Modal</ion-button>
26+
<ion-modal trigger="openModal">
27+
<ion-header>
28+
<ion-toolbar>
29+
<ion-title>Modal</ion-title>
30+
<ion-buttons slot="end">
31+
<ion-button onclick="dismiss()"> Close </ion-button>
32+
</ion-buttons>
33+
</ion-toolbar>
34+
</ion-header>
35+
<ion-content>
36+
<ion-nav></ion-nav>
37+
</ion-content>
38+
</ion-modal>
39+
</ion-content>
40+
</ion-app>
41+
42+
<script>
43+
const modal = document.querySelector('ion-modal');
44+
const nav = document.querySelector('ion-nav');
45+
46+
modal.addEventListener('willPresent', () => {
47+
nav.setRoot('page-one');
48+
});
49+
50+
const dismiss = () => modal.dismiss();
51+
52+
const navigate = (component, componentProps) => {
53+
nav.push(component, componentProps);
54+
};
55+
56+
const navigateBack = () => {
57+
nav.pop();
58+
};
59+
60+
const navigateToRoot = () => {
61+
nav.popToRoot();
62+
};
63+
64+
class PageOne extends HTMLElement {
65+
connectedCallback() {
66+
this.innerHTML = `
67+
<ion-content class="ion-padding">
68+
<h1>Page One</h1>
69+
<ion-button id="goto-page-two" onclick="navigate('page-two')">Go to Page Two</ion-button>
70+
</ion-content>
71+
`;
72+
}
73+
}
74+
75+
class PageTwo extends HTMLElement {
76+
connectedCallback() {
77+
this.innerHTML = `
78+
<ion-content class="ion-padding">
79+
<h1>Page Two</h1>
80+
<ion-button id="goto-page-three" onclick="navigate('page-three')">Go to Page Three</ion-button>
81+
</ion-content>
82+
`;
83+
}
84+
}
85+
86+
class PageThree extends HTMLElement {
87+
connectedCallback() {
88+
this.innerHTML = `
89+
<ion-content class="ion-padding">
90+
<h1>Page Three</h1>
91+
<ion-button id="go-back" onclick="navigateBack()">Go Back</ion-button>
92+
<ion-button id="goto-root"onclick="navigateToRoot()">Go to Root</ion-button>
93+
</ion-content>
94+
`;
95+
}
96+
}
97+
98+
customElements.define('page-one', PageOne);
99+
customElements.define('page-two', PageTwo);
100+
customElements.define('page-three', PageThree);
101+
</script>
102+
</body>
103+
</html>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect } from '@playwright/test';
2+
import type { E2EPage } from '@utils/test/playwright';
3+
import { test } from '@utils/test/playwright';
4+
5+
test.describe('nav: modal-navigation', () => {
6+
test.beforeEach(async ({ page }) => {
7+
await page.goto(`/src/components/nav/test/modal-navigation`);
8+
await openModal(page);
9+
});
10+
11+
test('should render the root page', async ({ page }) => {
12+
const pageOne = page.locator('page-one');
13+
const pageOneHeading = page.locator('page-one h1');
14+
15+
await expect(pageOne).toBeVisible();
16+
await expect(pageOneHeading).toHaveText('Page One');
17+
});
18+
19+
test('should push to the next page', async ({ page }) => {
20+
await page.click('#goto-page-two');
21+
22+
const pageTwo = page.locator('page-two');
23+
const pageTwoHeading = page.locator('page-two h1');
24+
25+
await expect(pageTwo).toBeVisible();
26+
await expect(pageTwoHeading).toHaveText('Page Two');
27+
});
28+
29+
test('should pop to the previous page', async ({ page }) => {
30+
await page.click('#goto-page-two');
31+
await page.click('#goto-page-three');
32+
33+
const pageThree = page.locator('page-three');
34+
const pageThreeHeading = page.locator('page-three h1');
35+
36+
await expect(pageThree).toBeVisible();
37+
await expect(pageThreeHeading).toHaveText('Page Three');
38+
39+
await page.click('#go-back');
40+
41+
const pageTwo = page.locator('page-two');
42+
const pageTwoHeading = page.locator('page-two h1');
43+
44+
// Verifies the leavingView was unmounted
45+
await expect(pageThree).toHaveCount(0);
46+
await expect(pageTwo).toBeVisible();
47+
await expect(pageTwoHeading).toHaveText('Page Two');
48+
});
49+
50+
test.describe('popping to the root', () => {
51+
test('should render the root page', async ({ page }) => {
52+
const pageTwo = page.locator('page-two');
53+
const pageThree = page.locator('page-three');
54+
55+
await page.click('#goto-page-two');
56+
await page.click('#goto-page-three');
57+
58+
await page.click('#goto-root');
59+
60+
const pageOne = page.locator('page-one');
61+
const pageOneHeading = page.locator('page-one h1');
62+
63+
// Verifies all views besides the root were unmounted
64+
await expect(pageTwo).toHaveCount(0);
65+
await expect(pageThree).toHaveCount(0);
66+
67+
await expect(pageOne).toBeVisible();
68+
await expect(pageOneHeading).toHaveText('Page One');
69+
});
70+
});
71+
});
72+
73+
const openModal = async (page: E2EPage) => {
74+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
75+
await page.click('#openModal');
76+
await ionModalDidPresent.next();
77+
};

packages/vue/src/vue-component-lib/utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue';
22

3-
export interface InputProps {
4-
modelValue?: string | boolean;
3+
export interface InputProps<T> {
4+
modelValue?: T;
55
}
66

77
const UPDATE_VALUE_EVENT = 'update:modelValue';
@@ -49,7 +49,7 @@ const getElementClasses = (ref: Ref<HTMLElement | undefined>, componentClasses:
4949
* @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
5050
* correctly updated when a user's event callback fires.
5151
*/
52-
export const defineContainer = <Props>(
52+
export const defineContainer = <Props, VModelType=string|number|boolean>(
5353
name: string,
5454
defineCustomElement: any,
5555
componentProps: string[] = [],
@@ -67,7 +67,7 @@ export const defineContainer = <Props>(
6767
defineCustomElement();
6868
}
6969

70-
const Container = defineComponent<Props & InputProps>((props: any, { attrs, slots, emit }) => {
70+
const Container = defineComponent<Props & InputProps<VModelType>>((props: any, { attrs, slots, emit }) => {
7171
let modelPropValue = props[modelProp];
7272
const containerRef = ref<HTMLElement>();
7373
const classes = new Set(getComponentClasses(attrs.class));
@@ -76,7 +76,7 @@ export const defineContainer = <Props>(
7676
if (vnode.el) {
7777
const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent];
7878
eventsNames.forEach((eventName: string) => {
79-
vnode.el.addEventListener(eventName.toLowerCase(), (e: Event) => {
79+
vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => {
8080
modelPropValue = (e?.target as any)[modelProp];
8181
emit(UPDATE_VALUE_EVENT, modelPropValue);
8282

0 commit comments

Comments
 (0)