fix(vue): modal, popover, and nav are now created within application context (#22282)

resolves #22079
This commit is contained in:
Liam DeBeasi
2020-10-12 15:07:49 -04:00
committed by GitHub
parent 181d322192
commit 6026c65b1a
20 changed files with 405 additions and 136 deletions

View File

@@ -83,6 +83,7 @@ export const config: Config = {
'ion-popover',
'ion-toast',
'ion-app',
'ion-icon'
],
routerLinkComponents: [

View File

@@ -0,0 +1,38 @@
import { h, defineComponent, shallowRef, VNode } from 'vue';
const userComponents = shallowRef([]);
export const IonApp = defineComponent({
name: 'IonApp',
setup(_, { attrs, slots }) {
return () => {
return h(
'ion-app',
{
...attrs
},
[slots.default && slots.default(), ...userComponents.value]
)
}
}
});
/**
* When rendering user components inside of
* ion-modal, ion-popover, or ion-nav, the component
* needs to be created inside of the current application
* context otherwise libraries such as vue-i18n or vuex
* will not work properly.
*
* `userComponents` renders teleported components as children
* of `ion-app` within the current application context.
*/
export const addTeleportedUserComponent = (component: VNode) => {
userComponents.value = [
...userComponents.value,
component
]
}
export const removeTeleportedUserComponent = (component: VNode) => {
userComponents.value = userComponents.value.filter(cmp => cmp !== component);
}

View File

@@ -1,7 +1,8 @@
import { createVNode, render } from 'vue';
import { h, Teleport, VNode } from 'vue';
import { addTeleportedUserComponent, removeTeleportedUserComponent } from './components/IonApp';
export const VueDelegate = () => {
const attachViewToDom = (parentElement: HTMLElement, component: any, componentProps: any, classes?: string[]) => {
let Component: VNode | undefined;
const attachViewToDom = (parentElement: HTMLElement, component: any, componentProps: any = {}, classes?: string[]) => {
/**
* Ionic Framework passes in modal and popover element
* refs as props, but if these are not defined
@@ -10,20 +11,24 @@ export const VueDelegate = () => {
*/
delete componentProps['modal'];
delete componentProps['popover'];
const vueInstance = createVNode(component, componentProps);
const div = document.createElement('div');
classes && div.classList.add(...classes);
parentElement.appendChild(div);
render(vueInstance, div);
Component = h(
Teleport,
{ to: div },
h(component, { ...componentProps })
);
addTeleportedUserComponent(Component);
return div;
}
const removeViewFromDom = (_: HTMLElement, childElement: any) => {
render(null, childElement);
const removeViewFromDom = () => {
Component && removeTeleportedUserComponent(Component);
return Promise.resolve();
}

View File

@@ -12,6 +12,8 @@ export { IonTabs } from './components/IonTabs';
export { IonTabBar } from './components/IonTabBar';
export { IonNav } from './components/IonNav';
export { IonIcon } from './components/IonIcon';
export { IonApp } from './components/IonApp';
export * from './components/Overlays';
export { IonKeyboardRef, IonRouter, useBackButton, useIonRouter, useKeyboard } from './hooks';

View File

@@ -8,9 +8,6 @@ import { JSX } from '@ionic/core';
export const IonApp = /*@__PURE__*/ defineContainer<JSX.IonApp>('ion-app');
export const IonAvatar = /*@__PURE__*/ defineContainer<JSX.IonAvatar>('ion-avatar');

View File

@@ -16,8 +16,6 @@ export const defineOverlayContainer = <Props extends object>(name: string, compo
const Container = defineComponent<Props & OverlayProps>((props, { slots, emit }) => {
const overlay = ref();
const content = ref();
const onVnodeMounted = async () => {
const isOpen = props.isOpen;
isOpen && (await present(props))
@@ -39,7 +37,7 @@ export const defineOverlayContainer = <Props extends object>(name: string, compo
}
const present = async (props: Readonly<Props>) => {
const component = (slots) ? h('div', { ref: content }, slots) : undefined;
const component = slots.default && slots.default()[0];
overlay.value = await controller.create({
...props,
component

View File

@@ -1306,27 +1306,27 @@
}
},
"@ionic/core": {
"version": "5.4.0-dev.202010091911.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-dev.202010091911.bfc0b25.tgz",
"integrity": "sha512-gDWwMcgV+kPeTfBToZ8oLoSI7FygkRYEgi/DEAIHye3W8T/Z/NmqrxaNqXrXU0J0RKvfzrIlrw6uB67Xz1xRcw==",
"version": "5.4.0-dev.202010081857.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-dev.202010081857.bfc0b25.tgz",
"integrity": "sha512-yCH1GTlTTTS3dt9kYk/y5K82UdKOLidRqziGTsFeOoqTJI2w87SgRx0V0Il92dcB1XaWPB/SNnaM7Tt+GD7+Lg==",
"requires": {
"ionicons": "^5.1.2",
"tslib": "^1.10.0"
}
},
"@ionic/vue": {
"version": "5.4.0-dev.202010091911.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-dev.202010091911.bfc0b25.tgz",
"integrity": "sha512-zrF/u3sMq4k3mLMtlA/VcS+RFeWEXrhOnzJAo8uPuDGT4ZcttJv0sVIFJMqZF7zM+6+xxep5B5mMoB1cigwG3A==",
"version": "5.4.0-dev.202010081857.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-dev.202010081857.bfc0b25.tgz",
"integrity": "sha512-A2s0skOjoytlwC2xJVo+jmv6ZcYKV+HAaGitqKs08bPHATFDh+yzpJLXRkS0poZ3N2Bk4ossXsd5gn4eXC7cKw==",
"requires": {
"@ionic/core": "5.4.0-dev.202010091911.bfc0b25",
"@ionic/core": "5.4.0-dev.202010081857.bfc0b25",
"ionicons": "^5.1.2"
}
},
"@ionic/vue-router": {
"version": "5.4.0-dev.202010091911.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-dev.202010091911.bfc0b25.tgz",
"integrity": "sha512-moa388WPxJjHBcIVILoijS+xrTcS7jaK4T/rP3U9Lri1bSYrDpYNWmd/0sktGt80m3gaa8VXS5E995zDiyLAfQ=="
"version": "5.4.0-dev.202010081857.bfc0b25",
"resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-dev.202010081857.bfc0b25.tgz",
"integrity": "sha512-qlFRY32CuttCZHqFFTZoDOERFFLDursJvMeb75KjE5lO/gD+dbelb7Cn0dOyaKyrM06X/lwb8eXgjCwSsGbHxg=="
},
"@jest/console": {
"version": "24.9.0",

View File

@@ -12,8 +12,8 @@
"sync": "sh ./scripts/sync.sh"
},
"dependencies": {
"@ionic/vue": "5.4.0-dev.202010091911.bfc0b25",
"@ionic/vue-router": "5.4.0-dev.202010091911.bfc0b25",
"@ionic/vue": "5.4.0-dev.202010081857.bfc0b25",
"@ionic/vue-router": "5.4.0-dev.202010081857.bfc0b25",
"core-js": "^3.6.5",
"vue": "^3.0.0-0",
"vue-router": "^4.0.0-0"

View File

@@ -0,0 +1,16 @@
<template>
<ion-nav :root="NavRoot"></ion-nav>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { IonNav } from '@ionic/vue';
import NavRoot from '@/components/NavRoot.vue';
export default defineComponent({
components: { IonNav },
setup() {
return { NavRoot }
}
});
</script>

View File

@@ -0,0 +1,39 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Nav - Child</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" id="nav-child-content">
{{ title }}
</ion-content>
</template>
<script lang="ts">
import {
IonButtons,
IonBackButton,
IonContent,
IonHeader,
IonTitle,
IonToolbar
} from '@ionic/vue';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
title: { type: String, default: 'Default Title' }
},
components: {
IonButtons,
IonBackButton,
IonContent,
IonHeader,
IonTitle,
IonToolbar
}
})
</script>

View File

@@ -0,0 +1,47 @@
<template>
<ion-header>
<ion-toolbar>
<ion-buttons>
<ion-button @click="dismiss">Dismiss</ion-button>
</ion-buttons>
<ion-title>Nav - Root</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" @click="pushPage" id="push-nav-child">Go to Nav Child</ion-button>
</ion-content>
</template>
<script lang="ts">
import {
IonButtons,
IonButton,
IonContent,
IonHeader,
IonTitle,
IonToolbar,
modalController
} from '@ionic/vue';
import { defineComponent } from 'vue';
import NavChild from '@/components/NavChild.vue';
export default defineComponent({
components: {
IonButtons,
IonButton,
IonContent,
IonHeader,
IonTitle,
IonToolbar
},
methods: {
pushPage: function() {
const ionNav = document.querySelector('ion-nav') as any;
ionNav.push(NavChild, { title: 'Custom Title' });
},
dismiss: async function() {
await modalController.dismiss();
}
}
})
</script>

View File

@@ -27,13 +27,16 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/DefaultHref.vue')
},
{
path: '/navigation',
name: 'Navigation',
component: () => import('@/views/Navigation.vue')
path: '/routing',
component: () => import('@/views/Routing.vue')
},
{
path: '/navigation/child',
component: () => import('@/views/NavigationChild.vue')
path: '/routing/child',
component: () => import('@/views/RoutingChild.vue')
},
{
path: '/navigation',
component: () => import('@/views/Navigation.vue')
},
{
path: '/nested',
@@ -81,7 +84,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/Tab3.vue')
}
]
}
},
]
const router = createRouter({

View File

@@ -26,6 +26,9 @@
<ion-item router-link="/navigation" id="navigation">
<ion-label>Navigation</ion-label>
</ion-item>
<ion-item router-link="/routing" id="routing">
<ion-label>Routing</ion-label>
</ion-item>
<ion-item router-link="/default-href" id="default-href">
<ion-label>Default Href</ion-label>
</ion-item>

View File

@@ -16,56 +16,47 @@
</ion-toolbar>
</ion-header>
<ion-item button @click="setRouteParams" id="route-params">
<ion-label>Set Route Parameters</ion-label>
</ion-item>
<ion-item button router-link="/navigation/child" id="child">
<ion-label>Go to Child Page</ion-label>
</ion-item>
<div class="ion-padding">
<ion-button expand="block" @click="openModal" id="open-nav-modal">Open Modal</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import {
IonButton,
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar
IonToolbar,
modalController
} from '@ionic/vue';
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import Nav from '@/components/Nav.vue';
export default defineComponent({
name: 'Navigation',
components: {
IonButton,
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar
},
setup() {
const router = useRouter();
const setRouteParams = () => {
router.push({
query: {
search: 'liamwashere'
}
const openModal = async () => {
const modal = await modalController.create({
component: Nav
});
}
return { setRouteParams }
await modal.present();
}
return { openModal }
}
});
</script>

View File

@@ -90,6 +90,7 @@
<ion-modal
:is-open="isModalOpen"
:componentProps="overlayProps"
@onDidDismiss="setModalRef(false)"
>
<ModalContent></ModalContent>
@@ -97,6 +98,7 @@
<ion-popover
:is-open="isPopoverOpen"
:componentProps="overlayProps"
:event="popoverEvent"
@onDidDismiss="setPopoverRef(false)"
>
@@ -231,6 +233,10 @@ export default defineComponent({
}
]
const overlayProps = {
title: 'Custom Title'
}
const openActionSheet = async () => {
const actionSheet = await actionSheetController.create({ buttons: actionSheetButtons });
await actionSheet.present();
@@ -252,12 +258,12 @@ export default defineComponent({
}
const openModal = async () => {
const modal = await modalController.create({ component: ModalContent });
const modal = await modalController.create({ component: ModalContent, componentProps: overlayProps });
await modal.present();
}
const openPopover = async (event: Event) => {
const popover = await popoverController.create({ component: PopoverContent, event });
const popover = await popoverController.create({ component: PopoverContent, event, componentProps: overlayProps });
await popover.present();
}
@@ -309,6 +315,7 @@ export default defineComponent({
}
return {
overlayProps,
present,
componentType,
presentationType,

View File

@@ -0,0 +1,70 @@
<template>
<ion-page data-pageid="routing">
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Routing</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Routing</ion-title>
</ion-toolbar>
</ion-header>
<ion-item button @click="setRouteParams" id="route-params">
<ion-label>Set Route Parameters</ion-label>
</ion-item>
<ion-item button router-link="/routing/child" id="child">
<ion-label>Go to Child Page</ion-label>
</ion-item>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import {
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar
} from '@ionic/vue';
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
export default defineComponent({
components: {
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar
},
setup() {
const router = useRouter();
const setRouteParams = () => {
router.push({
query: {
search: 'liamwashere'
}
});
}
return { setRouteParams }
}
});
</script>

View File

@@ -1,23 +1,23 @@
<template>
<ion-page data-pageid="navigationchild">
<ion-page data-pageid="routingchild">
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Navigation Child</ion-title>
<ion-title>Routing Child</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Navigation Child</ion-title>
<ion-title size="large">Routing Child</ion-title>
</ion-toolbar>
</ion-header>
<div class="ion-padding">
Navigation Child Page
Routing Child Page
</div>
</ion-content>
</ion-page>
@@ -37,7 +37,6 @@ import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'NavigationChild',
components: {
IonBackButton,
IonButtons,

View File

@@ -1,74 +1,13 @@
describe('Navigation', () => {
it('should go to sibling page', () => {
cy.visit('http://localhost:8080');
cy.get('ion-item#navigation').click();
cy.wait(500)
cy.ionPageVisible('navigation')
cy.ionPageHidden('home')
});
it('should set query params and keep view in stack', () => {
cy.visit('http://localhost:8080/navigation');
cy.get('#route-params').click();
cy.ionPageVisible('navigation');
});
it('should go back home', () => {
cy.visit('http://localhost:8080');
cy.get('ion-item#navigation').click();
cy.ionBackClick('navigation');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('navigation');
});
it('should go back home with default href', () => {
cy.visit('http://localhost:8080/default-href');
cy.ionBackClick('defaulthref');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('defaulthref');
});
it('should show back button', () => {
cy.visit('http://localhost:8080');
cy.get('#navigation').click();
cy.get('#child').click();
cy.ionBackClick('navigationchild');
cy.ionBackClick('navigation');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('navigation');
cy.ionPageDoesNotExist('navigationchild')
})
});
describe('Navigation - Swipe to Go Back', () => {
beforeEach(() => {
cy.viewport(320, 568);
cy.visit('http://localhost:8080?ionic:mode=ios');
cy.get('#navigation').click();
cy.ionPageHidden('home');
cy.ionPageVisible('navigation')
cy.visit('http://localhost:8080/navigation');
});
it('should swipe and abort', () => {
cy.ionSwipeToGoBack();
cy.ionPageHidden('home');
cy.ionPageVisible('navigation');
});
it('should push a page with props', () => {
cy.get('#open-nav-modal').click();
it('should swipe and complete', () => {
cy.ionSwipeToGoBack(true);
cy.ionPageVisible('home');
cy.get('#push-nav-child').click();
// TODO: Vue router does not go back in cypress with router.back()
//cy.ionPageDoesNotExist('navigation');
cy.get('#nav-child-content').should('have.text', 'Custom Title');
});
})
});

View File

@@ -48,14 +48,54 @@ describe('Overlays', () => {
}
it(`should open and close ion-toast via component`, () => {
cy.get(`ion-radio#ion-toast`).click();
cy.get('ion-radio#component').click();
cy.get(`ion-radio#ion-toast`).click();
cy.get('ion-radio#component').click();
cy.get('ion-button#present-overlay').click();
cy.get('ion-toast').should('exist');
cy.get('ion-button#present-overlay').click();
cy.get('ion-toast').should('exist');
cy.get('ion-toast').find('button').click();
cy.get('ion-toast').find('button').click();
cy.get('ion-toast').should('not.exist');
});
cy.get('ion-toast').should('not.exist');
});
it('it should pass props to modal via controller', () => {
cy.get('ion-radio#ion-modal').click();
cy.get('ion-radio#controller').click();
cy.get('ion-button#present-overlay').click();
cy.get('ion-modal').should('exist');
cy.get('ion-modal ion-content').should('have.text', 'Custom Title');
});
it('it should pass props to modal via component', () => {
cy.get('ion-radio#ion-modal').click();
cy.get('ion-radio#component').click();
cy.get('ion-button#present-overlay').click();
cy.get('ion-modal').should('exist');
cy.get('ion-modal ion-content').should('have.text', 'Custom Title');
});
it('it should pass props to popover via controller', () => {
cy.get('ion-radio#ion-popover').click();
cy.get('ion-radio#controller').click();
cy.get('ion-button#present-overlay').click();
cy.get('ion-popover').should('exist');
cy.get('ion-popover ion-content').should('have.text', 'Custom Title');
});
it('it should pass props to popover via component', () => {
cy.get('ion-radio#ion-popover').click();
cy.get('ion-radio#component').click();
cy.get('ion-button#present-overlay').click();
cy.get('ion-popover').should('exist');
cy.get('ion-popover ion-content').should('have.text', 'Custom Title');
});
})

View File

@@ -0,0 +1,74 @@
describe('Routing', () => {
it('should go to sibling page', () => {
cy.visit('http://localhost:8080');
cy.get('ion-item#routing').click();
cy.wait(500)
cy.ionPageVisible('routing')
cy.ionPageHidden('home')
});
it('should set query params and keep view in stack', () => {
cy.visit('http://localhost:8080/routing');
cy.get('#route-params').click();
cy.ionPageVisible('routing');
});
it('should go back home', () => {
cy.visit('http://localhost:8080');
cy.get('ion-item#routing').click();
cy.ionBackClick('routing');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('routing');
});
it('should go back home with default href', () => {
cy.visit('http://localhost:8080/default-href');
cy.ionBackClick('defaulthref');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('defaulthref');
});
it('should show back button', () => {
cy.visit('http://localhost:8080');
cy.get('#routing').click();
cy.get('#child').click();
cy.ionBackClick('routingchild');
cy.ionBackClick('routing');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('routing');
cy.ionPageDoesNotExist('routingchild')
})
});
describe('Routing - Swipe to Go Back', () => {
beforeEach(() => {
cy.viewport(320, 568);
cy.visit('http://localhost:8080?ionic:mode=ios');
cy.get('#routing').click();
cy.ionPageHidden('home');
cy.ionPageVisible('routing')
});
it('should swipe and abort', () => {
cy.ionSwipeToGoBack();
cy.ionPageHidden('home');
cy.ionPageVisible('routing');
});
it('should swipe and complete', () => {
cy.ionSwipeToGoBack(true);
cy.ionPageVisible('home');
// TODO: Vue router does not go back in cypress with router.back()
//cy.ionPageDoesNotExist('navigation');
});
})