fix(vue): tabs correctly fire lifecycle events (#22479)

resolves #22466
This commit is contained in:
Liam DeBeasi
2020-11-19 15:30:32 -05:00
committed by GitHub
parent bb519b8724
commit cdc2fb652f
3 changed files with 278 additions and 9 deletions

View File

@ -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);
}
};

View File

@ -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

View 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);
});
});