mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-14 16:52:26 +08:00
fix(tabs, tab-bar): use standalone tab bar in Vue, React (#29940)
Issue number: resolves #29885, resolves #29924 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> React and Vue: Tab bar could be a standalone element within `IonTabs` and would navigate without issues with a router outlet before v8.3: ```tsx <IonTabs> <IonRouterOutlet></IonRouterOutlet> <IonTabBar></IonTabBar> </IonTabs> ``` It would work as if it was written as: ```tsx <IonTabs> <IonRouterOutlet></IonRouterOutlet> <IonTabBar slot="bottom"> <!-- Buttons --> </IonTabBar> </IonTabs> ``` After v8.3, any `ion-tab-bar` that was not a direct child of `ion-tabs` would lose it's expected behavior when used with a router outlet. If a user clicked on a tab button, then the content would not be redirected to that expected view. React only: Users can no longer add a `ref` to the `IonRouterOutlet`, it always returns undefined. ``` <IonTabs> <IonRouterOutlet ref={ref}> <IonTabBar slot="bottom"> <!-- Buttons --> </IonTabBar> </IonTabs> ``` ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> The fixes were already reviewed through PR https://github.com/ionic-team/ionic-framework/pull/29925 and PR https://github.com/ionic-team/ionic-framework/pull/29927. I split them to make it easier to review. React and Vue: The React tabs has been updated to pass data to the tab bar through context instead of passing it through a ref. By using a context, the data will be available for the tab bar to use regardless of its level. React only: Reverted the logic for `routerOutletRef` and added a comment of the importance of it. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> N/A
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { defineCustomElement } from "@ionic/core/components/ion-tab-bar.js";
|
||||
import type { VNode } from "vue";
|
||||
import type { VNode, Ref } from "vue";
|
||||
import { h, defineComponent, getCurrentInstance, inject } from "vue";
|
||||
|
||||
// TODO(FW-2969): types
|
||||
@ -16,6 +16,12 @@ interface Tab {
|
||||
ref: VNode;
|
||||
}
|
||||
|
||||
interface TabBarData {
|
||||
hasRouterOutlet: boolean;
|
||||
_tabsWillChange: Function;
|
||||
_tabsDidChange: Function;
|
||||
}
|
||||
|
||||
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
|
||||
|
||||
const getTabs = (nodes: VNode[]) => {
|
||||
@ -34,20 +40,23 @@ const getTabs = (nodes: VNode[]) => {
|
||||
|
||||
export const IonTabBar = defineComponent({
|
||||
name: "IonTabBar",
|
||||
props: {
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
_tabsWillChange: { type: Function, default: () => {} },
|
||||
_tabsDidChange: { type: Function, default: () => {} },
|
||||
_hasRouterOutlet: { type: Boolean, default: false },
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabState: {
|
||||
activeTab: undefined,
|
||||
tabs: {},
|
||||
/**
|
||||
* Passing this prop to each tab button
|
||||
* lets it be aware of the presence of
|
||||
* the router outlet.
|
||||
*/
|
||||
hasRouterOutlet: false,
|
||||
},
|
||||
tabVnodes: [],
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
_tabsWillChange: { type: Function, default: () => {} },
|
||||
_tabsDidChange: { type: Function, default: () => {} },
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
@ -55,7 +64,7 @@ export const IonTabBar = defineComponent({
|
||||
},
|
||||
methods: {
|
||||
setupTabState(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||
/**
|
||||
* For each tab, we need to keep track of its
|
||||
* base href as well as any child page that
|
||||
@ -75,13 +84,6 @@ export const IonTabBar = defineComponent({
|
||||
ref: child,
|
||||
};
|
||||
|
||||
/**
|
||||
* Passing this prop to each tab button
|
||||
* lets it be aware of the presence of
|
||||
* the router outlet.
|
||||
*/
|
||||
tabState.hasRouterOutlet = hasRouterOutlet;
|
||||
|
||||
/**
|
||||
* Passing this prop to each tab button
|
||||
* lets it be aware of the state that
|
||||
@ -126,7 +128,7 @@ export const IonTabBar = defineComponent({
|
||||
* @param ionRouter
|
||||
*/
|
||||
checkActiveTab(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||
const currentRoute = ionRouter?.getCurrentRouteInfo();
|
||||
const childNodes = this.$data.tabVnodes;
|
||||
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
|
||||
@ -216,7 +218,7 @@ export const IonTabBar = defineComponent({
|
||||
this.tabSwitch(activeTab);
|
||||
},
|
||||
tabSwitch(activeTab: string, ionRouter?: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||
const childNodes = this.$data.tabVnodes;
|
||||
const { activeTab: prevActiveTab } = this.$data.tabState;
|
||||
const tabState = this.$data.tabState;
|
||||
@ -227,7 +229,7 @@ export const IonTabBar = defineComponent({
|
||||
const tabDidChange = activeTab !== prevActiveTab;
|
||||
if (tabBar) {
|
||||
if (activeChild) {
|
||||
tabDidChange && this.$props._tabsWillChange(activeTab);
|
||||
tabDidChange && this.$data._tabsWillChange(activeTab);
|
||||
|
||||
if (hasRouterOutlet && ionRouter !== null) {
|
||||
ionRouter.handleSetCurrentTab(activeTab);
|
||||
@ -235,7 +237,7 @@ export const IonTabBar = defineComponent({
|
||||
|
||||
tabBar.selectedTab = tabState.activeTab = activeTab;
|
||||
|
||||
tabDidChange && this.$props._tabsDidChange(activeTab);
|
||||
tabDidChange && this.$data._tabsDidChange(activeTab);
|
||||
} else {
|
||||
/**
|
||||
* When going to a tab that does
|
||||
@ -250,6 +252,17 @@ export const IonTabBar = defineComponent({
|
||||
},
|
||||
mounted() {
|
||||
const ionRouter: any = inject("navManager", null);
|
||||
/**
|
||||
* Tab bar can be used as a standalone component,
|
||||
* so it cannot be modified directly through
|
||||
* IonTabs. Instead, data will be passed through
|
||||
* the provide/inject.
|
||||
*/
|
||||
const tabBarData = inject<Ref<TabBarData>>("tabBarData");
|
||||
|
||||
this.$data.tabState.hasRouterOutlet = tabBarData.value.hasRouterOutlet;
|
||||
this.$data._tabsWillChange = tabBarData.value._tabsWillChange;
|
||||
this.$data._tabsDidChange = tabBarData.value._tabsDidChange;
|
||||
|
||||
this.setupTabState(ionRouter);
|
||||
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
|
||||
import type { VNode } from "vue";
|
||||
import { h, defineComponent, Fragment, isVNode } from "vue";
|
||||
import {
|
||||
h,
|
||||
defineComponent,
|
||||
Fragment,
|
||||
isVNode,
|
||||
provide,
|
||||
shallowRef,
|
||||
} from "vue";
|
||||
|
||||
import { IonTab } from "../proxies";
|
||||
|
||||
@ -9,6 +16,12 @@ const DID_CHANGE = "ionTabsDidChange";
|
||||
|
||||
// TODO(FW-2969): types
|
||||
|
||||
interface TabBarData {
|
||||
hasRouterOutlet: boolean;
|
||||
_tabsWillChange: Function;
|
||||
_tabsDidChange: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue 3.2.38 fixed an issue where Web Component
|
||||
* names are respected using kebab case instead of pascal case.
|
||||
@ -24,13 +37,6 @@ const isRouterOutlet = (node: VNode) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isTabBar = (node: VNode) => {
|
||||
return (
|
||||
node.type &&
|
||||
((node.type as any).name === "IonTabBar" || node.type === "ion-tab-bar")
|
||||
);
|
||||
};
|
||||
|
||||
const isTab = (node: VNode): boolean => {
|
||||
// The `ion-tab` component was created with the `v-for` directive.
|
||||
if (node.type === Fragment) {
|
||||
@ -49,7 +55,43 @@ const isTab = (node: VNode): boolean => {
|
||||
export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
name: "IonTabs",
|
||||
emits: [WILL_CHANGE, DID_CHANGE],
|
||||
data() {
|
||||
return {
|
||||
hasRouterOutlet: false,
|
||||
};
|
||||
},
|
||||
setup(props, { slots, emit }) {
|
||||
const slottedContent: VNode[] | undefined =
|
||||
slots.default && slots.default();
|
||||
let routerOutlet: VNode | undefined = undefined;
|
||||
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
/**
|
||||
* Developers must pass an ion-router-outlet
|
||||
* inside of ion-tabs if they want to use
|
||||
* the history stack or URL updates associated
|
||||
* with the router.
|
||||
*/
|
||||
routerOutlet = slottedContent.find((child: VNode) =>
|
||||
isRouterOutlet(child)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab bar can be used as a standalone component,
|
||||
* so it cannot be modified directly through
|
||||
* IonTabs. Instead, data will be passed through
|
||||
* the provide/inject.
|
||||
*/
|
||||
provide(
|
||||
"tabBarData",
|
||||
shallowRef<TabBarData>({
|
||||
hasRouterOutlet: !!routerOutlet,
|
||||
_tabsWillChange: (tab: string) => emit(WILL_CHANGE, { tab }),
|
||||
_tabsDidChange: (tab: string) => emit(DID_CHANGE, { tab }),
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
props,
|
||||
slots,
|
||||
@ -68,9 +110,10 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
defineCustomElement();
|
||||
},
|
||||
render() {
|
||||
const { slots, emit, props } = this;
|
||||
const slottedContent = slots.default && slots.default();
|
||||
let routerOutlet;
|
||||
const { slots, props } = this;
|
||||
const slottedContent: VNode[] | undefined =
|
||||
slots.default && slots.default();
|
||||
let routerOutlet: VNode | undefined = undefined;
|
||||
let hasTab = false;
|
||||
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
@ -78,7 +121,7 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
* Developers must pass an ion-router-outlet
|
||||
* inside of ion-tabs if they want to use
|
||||
* the history stack or URL updates associated
|
||||
* wit the router.
|
||||
* with the router.
|
||||
*/
|
||||
routerOutlet = slottedContent.find((child: VNode) =>
|
||||
isRouterOutlet(child)
|
||||
@ -103,30 +146,6 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
const slottedTabBar = slottedContent.find((child: VNode) =>
|
||||
isTabBar(child)
|
||||
);
|
||||
|
||||
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 });
|
||||
slottedTabBar.props._hasRouterOutlet = !!routerOutlet;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTab) {
|
||||
return h(
|
||||
"ion-tabs",
|
||||
|
Reference in New Issue
Block a user