fix(vue): dynamic tabs are now correct recognized (#23212)

resolves #22847
This commit is contained in:
Liam DeBeasi
2021-04-21 15:14:30 -04:00
committed by GitHub
parent 7139b3f39e
commit 004885bfd4
3 changed files with 115 additions and 54 deletions

View File

@ -11,30 +11,53 @@ interface Tab {
ref: VNode
}
const isTabButton = (child: any) => child.type?.name === 'IonTabButton';
const getTabs = (nodes: VNode[]) => {
let tabs: VNode[] = [];
nodes.forEach((node: VNode) => {
if (isTabButton(node)) {
tabs.push(node);
} else if (Array.isArray(node.children) && node.children.length > 1) {
const childTabs = getTabs(node.children as VNode[]);
tabs = [...tabs, ...childTabs];
}
});
return tabs;
}
export const IonTabBar = defineComponent({
name: 'IonTabBar',
props: {
_tabsWillChange: { type: Function, default: () => {} },
_tabsDidChange: { type: Function, default: () => {} }
},
mounted() {
const ionRouter: any = inject('navManager');
const tabState: TabState = {
data() {
return {
tabState: {
activeTab: undefined,
tabs: {}
};
const currentInstance = getCurrentInstance();
const isTabButton = (child: any) => child.type?.name === 'IonTabButton';
/**
* For each tab, we need to keep track of its
* base href as well as any child page that
* is active in its stack so that when we go back
* to a tab from another tab, we can correctly
* show any child pages if necessary.
*/
const children = (currentInstance.subTree.children || []) as VNode[];
children.forEach((child: VNode) => {
if (isTabButton(child)) {
},
tabVnodes: []
}
},
updated() {
this.setupTabState(inject('navManager'));
},
methods: {
setupTabState(ionRouter: any) {
/**
* For each tab, we need to keep track of its
* base href as well as any child page that
* is active in its stack so that when we go back
* to a tab from another tab, we can correctly
* show any child pages if necessary.
*/
const tabState: TabState = this.$data.tabState;
const currentInstance = getCurrentInstance();
const tabs = this.$data.tabVnodes = getTabs((currentInstance.subTree.children || []) as VNode[]);
tabs.forEach(child => {
tabState.tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href,
@ -47,12 +70,15 @@ export const IonTabBar = defineComponent({
* ion-tab-bar is managing for it.
*/
child.component.props._getTabState = () => tabState;
}
});
});
const checkActiveTab = (currentRoute: any) => {
const childNodes = (currentInstance.subTree.children || []) as VNode[];
const { tabs, activeTab: prevActiveTab } = tabState;
this.checkActiveTab(ionRouter);
},
checkActiveTab(ionRouter: any) {
const currentRoute = ionRouter.getCurrentRouteInfo();
const childNodes = this.$data.tabVnodes;
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
const tabState = this.$data.tabState;
const tabKeys = Object.keys(tabs);
const activeTab = tabKeys
.find(key => {
@ -66,21 +92,19 @@ export const IonTabBar = defineComponent({
* it in the tabs state.
*/
childNodes.forEach((child: VNode) => {
if (isTabButton(child)) {
const tab = tabs[child.props.tab];
if (!tab || (tab.originalHref !== child.props.href)) {
const tab = tabs[child.props.tab];
if (!tab || (tab.originalHref !== child.props.href)) {
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href,
ref: child
}
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href,
ref: child
}
}
});
if (activeTab && prevActiveTab) {
const prevHref = tabState.tabs[prevActiveTab].currentHref;
const prevHref = this.$data.tabState.tabs[prevActiveTab].currentHref;
/**
* If the tabs change or the url changes,
* update the currentHref for the active tab.
@ -128,10 +152,14 @@ export const IonTabBar = defineComponent({
tabBar.selectedTab = tabState.activeTab = '';
}
}
};
}
},
mounted() {
const ionRouter: any = inject('navManager');
ionRouter.registerHistoryChangeListener(checkActiveTab.bind(this));
checkActiveTab(ionRouter.getCurrentRouteInfo());
this.setupTabState(ionRouter);
ionRouter.registerHistoryChangeListener(() => this.checkActiveTab(ionRouter));
},
setup(_, { slots }) {
return () => {

View File

@ -3,20 +3,17 @@
<ion-content>
<ion-tabs id="tabs">
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1" href="/tabs/tab1">
<ion-icon :icon="triangle" />
<ion-label>Tab 1</ion-label>
<ion-tab-button
v-for="tab in tabs"
:tab="'tab' + tab.id"
:href="'/tabs/tab' + tab.id"
:key="tab.id"
>
<ion-icon :icon="tab.icon" />
<ion-label>Tab {{ tab.id }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2" href="/tabs/tab2">
<ion-icon :icon="ellipse" />
<ion-label>Tab 2</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3" href="/tabs/tab3">
<ion-icon :icon="square" />
<ion-label>Tab 3</ion-label>
</ion-tab-button>
<ion-button id="add-tab" @click="addTab()">Add Tab</ion-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
@ -24,18 +21,33 @@
</template>
<script lang="ts">
import { IonTabBar, IonTabButton, IonTabs, IonContent, IonLabel, IonIcon, IonPage } from '@ionic/vue';
import { ellipse, square, triangle } from 'ionicons/icons';
import { IonButton, IonTabBar, IonTabButton, IonTabs, IonContent, IonLabel, IonIcon, IonPage } from '@ionic/vue';
import { ellipse, square, triangle, shield } from 'ionicons/icons';
import { useRouter } from 'vue-router';
import { ref, defineComponent } from 'vue';
export default {
export default defineComponent({
name: 'Tabs',
components: { IonContent, IonLabel, IonTabs, IonTabBar, IonTabButton, IonIcon, IonPage },
components: { IonButton, IonContent, IonLabel, IonTabs, IonTabBar, IonTabButton, IonIcon, IonPage },
setup() {
return {
ellipse,
square,
triangle,
const tabs = ref([
{ id: 1, icon: triangle },
{ id: 2, icon: ellipse },
{ id: 3, icon: square }
])
const router = useRouter();
const addTab = () => {
router.addRoute({ path: '/tabs/tab4', component: () => import('@/views/Tab4.vue') });
tabs.value = [
...tabs.value,
{
id: 4,
icon: shield
}
]
}
return { tabs, addTab }
}
}
});
</script>

View File

@ -264,6 +264,27 @@ describe('Tabs', () => {
cy.url().should('include', '/tabs/tab1');
});
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/22847
it('should support dynamic tabs', () => {
cy.visit('http://localhost:8080/tabs/tab1');
cy.ionPageVisible('tab1');
cy.get('ion-tab-button').its('length').should('equal', 3);
cy.get('ion-tab-button#tab-button-tab1').should('have.class', 'tab-selected');
cy.get('#add-tab').click();
cy.get('ion-tab-button').its('length').should('equal', 4);
cy.get('ion-tab-button#tab-button-tab4').click();
cy.ionPageVisible('tab4');
cy.ionPageHidden('tab1');
cy.get('ion-tab-button#tab-button-tab1').should('not.have.class', 'tab-selected');
cy.get('ion-tab-button#tab-button-tab4').should('have.class', 'tab-selected');
});
})
describe('Tabs - Swipe to Go Back', () => {