mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
chore(): sync feature-6.2 with main
This commit is contained in:
14
core/package-lock.json
generated
14
core/package-lock.json
generated
@ -25,7 +25,7 @@
|
|||||||
"@stencil/angular-output-target": "^0.4.0",
|
"@stencil/angular-output-target": "^0.4.0",
|
||||||
"@stencil/react-output-target": "^0.2.1",
|
"@stencil/react-output-target": "^0.2.1",
|
||||||
"@stencil/sass": "^1.5.2",
|
"@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/jest": "^26.0.20",
|
||||||
"@types/node": "^14.6.0",
|
"@types/node": "^14.6.0",
|
||||||
"@types/swiper": "5.4.0",
|
"@types/swiper": "5.4.0",
|
||||||
@ -1839,9 +1839,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@stencil/vue-output-target": {
|
"node_modules/@stencil/vue-output-target": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz",
|
||||||
"integrity": "sha512-JGyl3Bi2NJRDz64c2lFAP6zdRwMD12ruWcbT75VdcLVDmCwo+wqWs/Shj4ZWXlcNhzjxbf9vydtQFwVMld/NrA==",
|
"integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@stencil/core": "^2.9.0"
|
"@stencil/core": "^2.9.0"
|
||||||
@ -15570,9 +15570,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@stencil/vue-output-target": {
|
"@stencil/vue-output-target": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz",
|
||||||
"integrity": "sha512-JGyl3Bi2NJRDz64c2lFAP6zdRwMD12ruWcbT75VdcLVDmCwo+wqWs/Shj4ZWXlcNhzjxbf9vydtQFwVMld/NrA==",
|
"integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@stylelint/postcss-css-in-js": {
|
"@stylelint/postcss-css-in-js": {
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
"@stencil/angular-output-target": "^0.4.0",
|
"@stencil/angular-output-target": "^0.4.0",
|
||||||
"@stencil/react-output-target": "^0.2.1",
|
"@stencil/react-output-target": "^0.2.1",
|
||||||
"@stencil/sass": "^1.5.2",
|
"@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/jest": "^26.0.20",
|
||||||
"@types/node": "^14.6.0",
|
"@types/node": "^14.6.0",
|
||||||
"@types/swiper": "5.4.0",
|
"@types/swiper": "5.4.0",
|
||||||
|
@ -1128,6 +1128,13 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
this.initializeListeners();
|
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(() => {
|
raf(() => {
|
||||||
this.ionRender.emit();
|
this.ionRender.emit();
|
||||||
});
|
});
|
||||||
|
@ -83,6 +83,27 @@ test.describe('datetime: presentation', () => {
|
|||||||
|
|
||||||
expect(ionChangeSpy.length).toBe(1);
|
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(`
|
||||||
|
<ion-datetime presentation="date"></ion-datetime>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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', () => {
|
test.describe('datetime: presentation: time', () => {
|
||||||
|
@ -131,6 +131,10 @@ export class Nav implements NavOutlet {
|
|||||||
this.swipeGestureChanged();
|
this.swipeGestureChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.destroyed = false;
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
for (const view of this.views) {
|
for (const view of this.views) {
|
||||||
lifecycle(view.element!, LIFECYCLE_WILL_UNLOAD);
|
lifecycle(view.element!, LIFECYCLE_WILL_UNLOAD);
|
||||||
@ -879,9 +883,13 @@ export class Nav implements NavOutlet {
|
|||||||
leavingView: ViewController | undefined,
|
leavingView: ViewController | undefined,
|
||||||
opts: NavOptions
|
opts: NavOptions
|
||||||
): NavResult {
|
): NavResult {
|
||||||
const cleanupView = hasCompleted ? enteringView : leavingView;
|
/**
|
||||||
if (cleanupView) {
|
* If the transition did not complete, the leavingView will still be the active
|
||||||
this.cleanup(cleanupView);
|
* view on the stack. Otherwise unmount all the views after the enteringView.
|
||||||
|
*/
|
||||||
|
const activeView = hasCompleted ? enteringView : leavingView;
|
||||||
|
if (activeView) {
|
||||||
|
this.unmountInactiveViews(activeView);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -944,9 +952,13 @@ export class Nav implements NavOutlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Unmounts all inactive views after the specified active view.
|
||||||
|
*
|
||||||
* DOM WRITE
|
* 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
|
// ok, cleanup time!! Destroy all of the views that are
|
||||||
// INACTIVE and come after the active view
|
// INACTIVE and come after the active view
|
||||||
// only do this if the views exist, though
|
// only do this if the views exist, though
|
||||||
|
103
core/src/components/nav/test/modal-navigation/index.html
Normal file
103
core/src/components/nav/test/modal-navigation/index.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Nav - Modal Navigation</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Modal Navigation</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-button id="openModal">Open Modal</ion-button>
|
||||||
|
<ion-modal trigger="openModal">
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Modal</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button onclick="dismiss()"> Close </ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-nav></ion-nav>
|
||||||
|
</ion-content>
|
||||||
|
</ion-modal>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const modal = document.querySelector('ion-modal');
|
||||||
|
const nav = document.querySelector('ion-nav');
|
||||||
|
|
||||||
|
modal.addEventListener('willPresent', () => {
|
||||||
|
nav.setRoot('page-one');
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismiss = () => modal.dismiss();
|
||||||
|
|
||||||
|
const navigate = (component, componentProps) => {
|
||||||
|
nav.push(component, componentProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateBack = () => {
|
||||||
|
nav.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToRoot = () => {
|
||||||
|
nav.popToRoot();
|
||||||
|
};
|
||||||
|
|
||||||
|
class PageOne extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Page One</h1>
|
||||||
|
<ion-button id="goto-page-two" onclick="navigate('page-two')">Go to Page Two</ion-button>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageTwo extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Page Two</h1>
|
||||||
|
<ion-button id="goto-page-three" onclick="navigate('page-three')">Go to Page Three</ion-button>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageThree extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Page Three</h1>
|
||||||
|
<ion-button id="go-back" onclick="navigateBack()">Go Back</ion-button>
|
||||||
|
<ion-button id="goto-root"onclick="navigateToRoot()">Go to Root</ion-button>
|
||||||
|
</ion-content>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('page-one', PageOne);
|
||||||
|
customElements.define('page-two', PageTwo);
|
||||||
|
customElements.define('page-three', PageThree);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
77
core/src/components/nav/test/modal-navigation/nav.e2e.ts
Normal file
77
core/src/components/nav/test/modal-navigation/nav.e2e.ts
Normal file
@ -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();
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue';
|
import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue';
|
||||||
|
|
||||||
export interface InputProps {
|
export interface InputProps<T> {
|
||||||
modelValue?: string | boolean;
|
modelValue?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UPDATE_VALUE_EVENT = 'update:modelValue';
|
const UPDATE_VALUE_EVENT = 'update:modelValue';
|
||||||
@ -49,7 +49,7 @@ const getElementClasses = (ref: Ref<HTMLElement | undefined>, 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
|
* @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.
|
* correctly updated when a user's event callback fires.
|
||||||
*/
|
*/
|
||||||
export const defineContainer = <Props>(
|
export const defineContainer = <Props, VModelType=string|number|boolean>(
|
||||||
name: string,
|
name: string,
|
||||||
defineCustomElement: any,
|
defineCustomElement: any,
|
||||||
componentProps: string[] = [],
|
componentProps: string[] = [],
|
||||||
@ -67,7 +67,7 @@ export const defineContainer = <Props>(
|
|||||||
defineCustomElement();
|
defineCustomElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = defineComponent<Props & InputProps>((props: any, { attrs, slots, emit }) => {
|
const Container = defineComponent<Props & InputProps<VModelType>>((props: any, { attrs, slots, emit }) => {
|
||||||
let modelPropValue = props[modelProp];
|
let modelPropValue = props[modelProp];
|
||||||
const containerRef = ref<HTMLElement>();
|
const containerRef = ref<HTMLElement>();
|
||||||
const classes = new Set(getComponentClasses(attrs.class));
|
const classes = new Set(getComponentClasses(attrs.class));
|
||||||
@ -76,7 +76,7 @@ export const defineContainer = <Props>(
|
|||||||
if (vnode.el) {
|
if (vnode.el) {
|
||||||
const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent];
|
const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent];
|
||||||
eventsNames.forEach((eventName: string) => {
|
eventsNames.forEach((eventName: string) => {
|
||||||
vnode.el.addEventListener(eventName.toLowerCase(), (e: Event) => {
|
vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => {
|
||||||
modelPropValue = (e?.target as any)[modelProp];
|
modelPropValue = (e?.target as any)[modelProp];
|
||||||
emit(UPDATE_VALUE_EVENT, modelPropValue);
|
emit(UPDATE_VALUE_EVENT, modelPropValue);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user