diff --git a/packages/vue/src/components/IonTabBar.ts b/packages/vue/src/components/IonTabBar.ts
index 682c366483..343fab1558 100644
--- a/packages/vue/src/components/IonTabBar.ts
+++ b/packages/vue/src/components/IonTabBar.ts
@@ -13,6 +13,10 @@ interface Tab {
export const IonTabBar = defineComponent({
name: 'IonTabBar',
+ props: {
+ _tabsWillChange: { type: Function, default: () => {} },
+ _tabsDidChange: { type: Function, default: () => {} }
+ },
mounted() {
const ionRouter: any = inject('navManager');
const tabState: TabState = {
@@ -102,12 +106,16 @@ export const IonTabBar = defineComponent({
}
}
- const activeChild = childNodes.find((child: VNode) => child.el.tab === activeTab);
+ const activeChild = childNodes.find((child: VNode) => child.props.tab === activeTab);
const tabBar = this.$refs.ionTabBar;
-
+ const tabDidChange = activeTab !== prevActiveTab;
if (activeChild && tabBar) {
+ tabDidChange && this.$props._tabsWillChange(activeTab);
+
ionRouter.handleSetCurrentTab(activeTab);
tabBar.selectedTab = tabState.activeTab = activeTab;
+
+ tabDidChange && this.$props._tabsDidChange(activeTab);
}
};
diff --git a/packages/vue/src/components/IonTabs.ts b/packages/vue/src/components/IonTabs.ts
index 9d4df7ae11..e292d53035 100644
--- a/packages/vue/src/components/IonTabs.ts
+++ b/packages/vue/src/components/IonTabs.ts
@@ -1,10 +1,14 @@
import { h, defineComponent, VNode } from 'vue';
import { IonRouterOutlet } from './IonRouterOutlet';
+const WILL_CHANGE = 'ionTabsWillChange';
+const DID_CHANGE = 'ionTabsDidChange';
+
export const IonTabs = defineComponent({
name: 'IonTabs',
+ emits: [WILL_CHANGE, DID_CHANGE],
render() {
- const { $slots: slots } = this;
+ const { $slots: slots, $emit } = this;
const slottedContent = slots.default && slots.default();
let childrenToRender = [
h('div', {
@@ -25,14 +29,25 @@ export const IonTabs = defineComponent({
* 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';
+ const slottedTabBar = slottedContent.find((child: VNode) => child.type && (child.type as any).name === 'IonTabBar');
+ const hasTopSlotTabBar = slottedTabBar && slottedTabBar.props?.slot === 'top';
- return isTabBar && hasTopSlot;
- });
+ if (slottedTabBar) {
+ if (!slottedTabBar.props) {
+ slottedTabBar.props = {};
+ }
+ /**
+ * ionTabsWillChange and ionTabsDidChange are
+ * fired from `ion-tabs`, so we need to pass these down
+ * as props so they can fire when the active tab changes.
+ * TODO: We may want to move logic from the tab bar into here
+ * so we do not have code split across two components.
+ */
+ slottedTabBar.props._tabsWillChange = (tab: string) => $emit(WILL_CHANGE, { tab });
+ slottedTabBar.props._tabsDidChange = (tab: string) => $emit(DID_CHANGE, { tab });
+ }
- if (topSlottedTabBar) {
+ if (hasTopSlotTabBar) {
childrenToRender = [
...slottedContent,
...childrenToRender
diff --git a/packages/vue/test-app/tests/unit/tabs.spec.ts b/packages/vue/test-app/tests/unit/tabs.spec.ts
new file mode 100644
index 0000000000..0262da6d34
--- /dev/null
+++ b/packages/vue/test-app/tests/unit/tabs.spec.ts
@@ -0,0 +1,246 @@
+import { mount, flushPromises } from '@vue/test-utils';
+import { createRouter, createWebHistory } from '@ionic/vue-router';
+import { IonicVue, IonApp, IonRouterOutlet, IonPage, IonTabs, IonTabBar, IonTabButton, IonLabel } from '@ionic/vue';
+
+const App = {
+ components: { IonApp, IonRouterOutlet },
+ template: '',
+}
+
+const Tabs = {
+ components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonLabel },
+ template: `
+
+
+
+
+ Tab 1
+
+
+ Tab 2
+
+
+
+
+ `,
+}
+const Tab1 = {
+ components: { IonPage },
+ template: `Tab 1`
+}
+const Tab2 = {
+ components: { IonPage },
+ template: `Tab 2`
+}
+
+describe('ion-tabs', () => {
+ (HTMLElement.prototype as HTMLIonRouterOutletElement).commit = jest.fn();
+
+ it('should emit will change and did change events when changing tab', async () => {
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ component: Tabs,
+ children: [
+ {
+ path: '',
+ redirect: 'tab1'
+ },
+ {
+ path: 'tab1',
+ component: Tab1,
+ },
+ {
+ path: 'tab2',
+ component: Tab2
+ }
+ ]
+ }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ const tabs = wrapper.findComponent(IonTabs);
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);
+
+ router.push('/tab2')
+ await flushPromises()
+
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(2);
+ expect(tabs.emitted().ionTabsWillChange[1]).toEqual([{ tab: 'tab2' }]);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(2);
+ expect(tabs.emitted().ionTabsDidChange[1]).toEqual([{ tab: 'tab2' }]);
+ });
+
+ it('should not emit will change and did change events when going to same tab again', async () => {
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ component: Tabs,
+ children: [
+ {
+ path: '',
+ redirect: 'tab1'
+ },
+ {
+ path: 'tab1',
+ component: Tab1,
+ },
+ {
+ path: 'tab2',
+ component: Tab2
+ }
+ ]
+ }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ const tabs = wrapper.findComponent(IonTabs);
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);
+
+ router.push('/tab1')
+ await flushPromises()
+
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ });
+
+ it('should not emit will change and did change events when going to a non tabs page', async () => {
+ const Sibling = {
+ components: { IonPage },
+ template: `Sibling Page`
+ }
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ component: Tabs,
+ children: [
+ {
+ path: '',
+ redirect: 'tab1'
+ },
+ {
+ path: 'tab1',
+ component: Tab1
+ },
+ {
+ path: 'tab2',
+ component: Tab2
+ }
+ ]
+ },
+ {
+ path: '/sibling',
+ component: Sibling
+ }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ const tabs = wrapper.findComponent(IonTabs);
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);
+
+ router.push('/sibling');
+ await flushPromises();
+
+ await new Promise((r) => setTimeout(r, 100));
+
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ });
+
+ it('should not emit will change and did change events when going to child tab page', async () => {
+ const Child = {
+ components: { IonPage },
+ template: `Child Page`
+ }
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ component: Tabs,
+ children: [
+ {
+ path: '',
+ redirect: 'tab1'
+ },
+ {
+ path: 'tab1',
+ component: Tab1,
+ children: [
+ {
+ path: 'child',
+ component: Child
+ }
+ ]
+ },
+ {
+ path: 'tab2',
+ component: Tab2
+ }
+ ]
+ }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ const tabs = wrapper.findComponent(IonTabs);
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);
+
+ router.push('/tab1/child');
+ await flushPromises();
+
+ await new Promise((r) => setTimeout(r, 100));
+
+ expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
+ expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
+ });
+});