diff --git a/packages/vue-router/src/viewStacks.ts b/packages/vue-router/src/viewStacks.ts index 3688d193eb..45bd6d31a0 100644 --- a/packages/vue-router/src/viewStacks.ts +++ b/packages/vue-router/src/viewStacks.ts @@ -22,16 +22,16 @@ export const createViewStacks = (router: Router) => { viewItem.ionRoute = true; } - const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => { - return findViewItemByPath(routeInfo.pathname, outletId); + const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, useDeprecatedRouteSetup: boolean = false) => { + return findViewItemByPath(routeInfo.pathname, outletId, false, useDeprecatedRouteSetup); } - const findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, mustBeIonRoute: boolean = true) => { - return findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute); + const findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, mustBeIonRoute: boolean = true, useDeprecatedRouteSetup: boolean = false) => { + return findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute, useDeprecatedRouteSetup); } - const findViewItemByPathname = (pathname: string, outletId?: number) => { - return findViewItemByPath(pathname, outletId); + const findViewItemByPathname = (pathname: string, outletId?: number, useDeprecatedRouteSetup: boolean = false) => { + return findViewItemByPath(pathname, outletId, false, useDeprecatedRouteSetup); } const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => { @@ -44,7 +44,7 @@ export const createViewStacks = (router: Router) => { }) } - const findViewItemByPath = (path: string, outletId?: number, mustBeIonRoute: boolean = false): ViewItem | undefined => { + const findViewItemByPath = (path: string, outletId?: number, mustBeIonRoute: boolean = false, useDeprecatedRouteSetup: boolean = false): ViewItem | undefined => { const matchView = (viewItem: ViewItem) => { if ( (mustBeIonRoute && !viewItem.ionRoute) || @@ -54,7 +54,13 @@ export const createViewStacks = (router: Router) => { } const resolvedPath = router.resolve(path); - const findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute && (path === viewItem.pathname || matchedRoute.path.includes(':'))); + let findMatchedRoute; + // TODO: Remove in Ionic Vue v6.0 + if (useDeprecatedRouteSetup) { + findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute && (path === viewItem.pathname || matchedRoute.path.includes(':'))); + } else { + findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute); + } if (findMatchedRoute) { return viewItem; diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts index 9530f22a6a..7fdbb2bd9b 100644 --- a/packages/vue/src/components/IonRouterOutlet.ts +++ b/packages/vue/src/components/IonRouterOutlet.ts @@ -21,10 +21,17 @@ export const IonRouterOutlet = defineComponent({ const injectedRoute = inject(routeLocationKey)!; const route = useRoute(); const depth = inject(viewDepthKey, 0); + let usingDeprecatedRouteSetup = false; + + // TODO: Remove in Ionic Vue v6.0 + if (attrs.tabs && route.matched[depth]?.children?.length > 0) { + console.warn('[@ionic/vue Deprecation]: Your child routes are nested inside of each tab in your routing config. This format will not be supported in Ionic Vue v6.0. Instead, write your child routes as sibling routes. See https://ionicframework.com/docs/vue/navigation#child-routes-within-tabs for more information.'); + usingDeprecatedRouteSetup = true; + } const matchedRouteRef: any = computed(() => { const matchedRoute = route.matched[depth]; - if (matchedRoute && attrs.tabs && route.matched[depth + 1]) { + if (matchedRoute && attrs.tabs && route.matched[depth + 1] && usingDeprecatedRouteSetup) { return route.matched[route.matched.length - 1]; } @@ -76,15 +83,15 @@ export const IonRouterOutlet = defineComponent({ * to make sure the view is in the outlet we want. */ const routeInfo = ionRouter.getCurrentRouteInfo(); - const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id); + const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup); return !!enteringViewItem; } const onStart = async () => { const routeInfo = ionRouter.getCurrentRouteInfo(); const { routerAnimation } = routeInfo; - const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id); - const leavingViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id); + const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup); + const leavingViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup); if (leavingViewItem) { let animationBuilder = routerAnimation; @@ -139,7 +146,7 @@ export const IonRouterOutlet = defineComponent({ * re-hide the page that was going to enter. */ const routeInfo = ionRouter.getCurrentRouteInfo(); - const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id); + const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup); enteringViewItem.ionPageElement.setAttribute('aria-hidden', 'true'); enteringViewItem.ionPageElement.classList.add('ion-page-hidden'); } @@ -194,14 +201,14 @@ export const IonRouterOutlet = defineComponent({ const routeInfo = ionRouter.getCurrentRouteInfo(); const { routerDirection, routerAction, routerAnimation, prevRouteLastPathname } = routeInfo; - const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id); - let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id); + const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup); + let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id, true, usingDeprecatedRouteSetup); const enteringEl = enteringViewItem.ionPageElement; if (enteringViewItem === leavingViewItem) return; if (!leavingViewItem && prevRouteLastPathname) { - leavingViewItem = viewStacks.findViewItemByPathname(prevRouteLastPathname, id); + leavingViewItem = viewStacks.findViewItemByPathname(prevRouteLastPathname, id, usingDeprecatedRouteSetup); } fireLifecycle(enteringViewItem.vueComponent, enteringViewItem.vueComponentRef, LIFECYCLE_WILL_ENTER); @@ -296,7 +303,7 @@ export const IonRouterOutlet = defineComponent({ } const currentRoute = ionRouter.getCurrentRouteInfo(); - let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id); + let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id, usingDeprecatedRouteSetup); if (!enteringViewItem) { enteringViewItem = viewStacks.createViewItem(id, matchedRouteRef.value.components.default, matchedRouteRef.value, currentRoute); diff --git a/packages/vue/test-app/src/router/index.ts b/packages/vue/test-app/src/router/index.ts index 5ec99223b9..0ac97bdd76 100644 --- a/packages/vue/test-app/src/router/index.ts +++ b/packages/vue/test-app/src/router/index.ts @@ -110,6 +110,39 @@ const routes: Array = [ } ] }, + { + path: '/tabs-new/', + component: () => import('@/views/Tabs.vue'), + children: [ + { + path: '', + redirect: '/tabs-new/tab1' + }, + { + path: 'tab1', + component: () => import('@/views/Tab1.vue'), + }, + { + path: 'tab1/child-one', + component: () => import('@/views/Tab1ChildOne.vue') + }, + { + path: 'tab1/child-two', + component: () => import('@/views/Tab1ChildTwo.vue') + }, + { + path: 'tab2', + component: () => import('@/views/Tab2.vue') + }, + { + path: 'tab3', + beforeEnter: (to, from, next) => { + next({ path: '/tabs/tab1' }); + }, + component: () => import('@/views/Tab3.vue') + } + ] + }, { path: '/tabs-secondary/', component: () => import('@/views/TabsSecondary.vue'), diff --git a/packages/vue/test-app/tests/e2e/specs/tabs.js b/packages/vue/test-app/tests/e2e/specs/tabs.js index 0af23bad18..020af3fa5e 100644 --- a/packages/vue/test-app/tests/e2e/specs/tabs.js +++ b/packages/vue/test-app/tests/e2e/specs/tabs.js @@ -172,7 +172,41 @@ describe('Tabs', () => { cy.ionPageVisible('tab1'); cy.ionPageHidden('routing'); cy.ionPageHidden('tab2'); - }) + }); + + // Verifies 1 of 2 fixes for https://github.com/ionic-team/ionic-framework/issues/22519 + it('should not create a new tabs instance when switching between tabbed and non-tabbed contexts', () => { + cy.visit('http://localhost:8080/tabs/tab1'); + + cy.routerPush('/'); + cy.ionPageHidden('tabs'); + cy.ionPageVisible('home'); + + cy.routerPush('/tabs/tab2'); + cy.ionPageHidden('tab1'); + + cy.ionPageHidden('home'); + + cy.ionPageVisible('tab2'); + cy.ionPageVisible('tabs'); + }); + + // Verifies 1 of 2 fixes for https://github.com/ionic-team/ionic-framework/issues/22519 + it('should not create a new tabs instance when switching between tabbed and non-tabbed contexts - new tabs setup', () => { + cy.visit('http://localhost:8080/tabs-new/tab1'); + + cy.routerPush('/'); + cy.ionPageHidden('tabs'); + cy.ionPageVisible('home'); + + cy.routerPush('/tabs-new/tab2'); + cy.ionPageHidden('tab1'); + + cy.ionPageHidden('home'); + + cy.ionPageVisible('tab2'); + cy.ionPageVisible('tabs'); + }); }) describe('Tabs - Swipe to Go Back', () => { diff --git a/packages/vue/test-app/tests/unit/tab-bar.spec.ts b/packages/vue/test-app/tests/unit/tab-bar.spec.ts index 3021ca7d94..38b99eef45 100644 --- a/packages/vue/test-app/tests/unit/tab-bar.spec.ts +++ b/packages/vue/test-app/tests/unit/tab-bar.spec.ts @@ -68,6 +68,7 @@ describe('ion-tab-bar', () => { }); const innerHTML = wrapper.find('ion-tabs').html(); + // TODO: Remove tabs="true" in Ionic Vue v6.0 expect(innerHTML).toContain(`
`); }); @@ -100,6 +101,7 @@ describe('ion-tab-bar', () => { }); const innerHTML = wrapper.find('ion-tabs').html(); + // TODO: Remove tabs="true" in Ionic Vue v6.0 expect(innerHTML).toContain(`
`) });