mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
fix(vue): tabs correctly fire lifecycle events (#22479)
resolves #22466
This commit is contained in:
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
246
packages/vue/test-app/tests/unit/tabs.spec.ts
Normal file
246
packages/vue/test-app/tests/unit/tabs.spec.ts
Normal file
@ -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: '<ion-app><ion-router-outlet /></ion-app>',
|
||||
}
|
||||
|
||||
const Tabs = {
|
||||
components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonLabel },
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-tabs>
|
||||
<ion-tab-bar slot="top">
|
||||
<ion-tab-button tab="tab1" href="/tab1">
|
||||
<ion-label>Tab 1</ion-label>
|
||||
</ion-tab-button>
|
||||
<ion-tab-button tab="tab2" href="/tab2">
|
||||
<ion-label>Tab 2</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
</ion-page>
|
||||
`,
|
||||
}
|
||||
const Tab1 = {
|
||||
components: { IonPage },
|
||||
template: `<ion-page>Tab 1</ion-page>`
|
||||
}
|
||||
const Tab2 = {
|
||||
components: { IonPage },
|
||||
template: `<ion-page>Tab 2</ion-page>`
|
||||
}
|
||||
|
||||
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: `<ion-page>Sibling Page</ion-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: `<ion-page>Child Page</ion-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);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user