diff --git a/packages/vue/src/components/IonTabBar.ts b/packages/vue/src/components/IonTabBar.ts index 4c7a470b25..682c366483 100644 --- a/packages/vue/src/components/IonTabBar.ts +++ b/packages/vue/src/components/IonTabBar.ts @@ -27,7 +27,8 @@ export const IonTabBar = defineComponent({ * to a tab from another tab, we can correctly * show any child pages if necessary. */ - (currentInstance.subTree.children as VNode[]).forEach((child: VNode) => { + const children = (currentInstance.subTree.children || []) as VNode[]; + children.forEach((child: VNode) => { if (child.type && (child.type as any).name === 'IonTabButton') { tabState.tabs[child.props.tab] = { originalHref: child.props.href, @@ -45,7 +46,7 @@ export const IonTabBar = defineComponent({ }); const checkActiveTab = (currentRoute: any) => { - const childNodes = currentInstance.subTree.children as VNode[]; + const childNodes = (currentInstance.subTree.children || []) as VNode[]; const { tabs, activeTab: prevActiveTab } = tabState; const tabKeys = Object.keys(tabs); const activeTab = tabKeys diff --git a/packages/vue/src/components/IonTabs.ts b/packages/vue/src/components/IonTabs.ts index 3d43744a1e..9d4df7ae11 100644 --- a/packages/vue/src/components/IonTabs.ts +++ b/packages/vue/src/components/IonTabs.ts @@ -1,10 +1,50 @@ -import { h, defineComponent } from 'vue'; +import { h, defineComponent, VNode } from 'vue'; import { IonRouterOutlet } from './IonRouterOutlet'; export const IonTabs = defineComponent({ name: 'IonTabs', render() { const { $slots: slots } = this; + const slottedContent = slots.default && slots.default(); + let childrenToRender = [ + h('div', { + class: 'tabs-inner', + style: { + 'position': 'relative', + 'flex': '1', + 'contain': 'layout size style' + } + }, [ + h(IonRouterOutlet, { tabs: true }) + ]) + ]; + + /** + * If ion-tab-bar has slot="top" it needs to be + * rendered before `.tabs-inner` otherwise it will + * not show above the tab content. + */ + if (slottedContent && slottedContent.length > 0) { + const topSlottedTabBar = slottedContent.find((child: VNode) => { + const isTabBar = child.type && (child.type as any).name === 'IonTabBar'; + const hasTopSlot = child.props?.slot === 'top'; + + return isTabBar && hasTopSlot; + }); + + if (topSlottedTabBar) { + childrenToRender = [ + ...slottedContent, + ...childrenToRender + ]; + } else { + childrenToRender = [ + ...childrenToRender, + ...slottedContent + ] + } + } + return h( 'ion-tabs', { @@ -22,23 +62,7 @@ export const IonTabs = defineComponent({ 'z-index': '0' } }, - [ - h( - 'div', - { - class: 'tabs-inner', - style: { - 'position': 'relative', - 'flex': '1', - 'contain': 'layout size style' - } - }, - [ - h(IonRouterOutlet, { tabs: true }) - ] - ), - ...slots.default && slots.default() - ] + childrenToRender ) } }); diff --git a/packages/vue/test-app/src/views/TabsSecondary.vue b/packages/vue/test-app/src/views/TabsSecondary.vue index 491fbd8e38..2319da4234 100644 --- a/packages/vue/test-app/src/views/TabsSecondary.vue +++ b/packages/vue/test-app/src/views/TabsSecondary.vue @@ -2,7 +2,7 @@ - + Tab 1 diff --git a/packages/vue/test-app/tests/unit/tab-bar.spec.ts b/packages/vue/test-app/tests/unit/tab-bar.spec.ts new file mode 100644 index 0000000000..26b4aea2e4 --- /dev/null +++ b/packages/vue/test-app/tests/unit/tab-bar.spec.ts @@ -0,0 +1,105 @@ +import { mount } from '@vue/test-utils'; +import { createRouter, createWebHistory } from '@ionic/vue-router'; +import { IonicVue, IonApp, IonRouterOutlet, IonPage, IonTabs, IonTabBar } from '@ionic/vue'; + +const App = { + components: { IonApp, IonRouterOutlet }, + template: '', +} + +describe('ion-tab-bar', () => { + it('should render in the top slot', async () => { + const Tabs = { + components: { IonPage, IonTabs, IonTabBar }, + template: ` + + + + + + `, + } + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', component: Tabs } + ] + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(App, { + global: { + plugins: [router, IonicVue] + } + }); + + const innerHTML = wrapper.find('ion-tabs').html(); + expect(innerHTML).toContain(`
`); + + }); + + it('should render in the bottom slot', async () => { + const Tabs = { + components: { IonPage, IonTabs, IonTabBar }, + template: ` + + + + + + `, + } + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', component: Tabs } + ] + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(App, { + global: { + plugins: [router, IonicVue] + } + }); + + const innerHTML = wrapper.find('ion-tabs').html(); + expect(innerHTML).toContain(`
`); + + }); + + it('should render in the default slot', async () => { + const Tabs = { + components: { IonPage, IonTabs, IonTabBar }, + template: ` + + + + + + `, + } + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', component: Tabs } + ] + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(App, { + global: { + plugins: [router, IonicVue] + } + }); + + const innerHTML = wrapper.find('ion-tabs').html(); + expect(innerHTML).toContain(`
`) + }); +});