mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 17:42:15 +08:00
Merge branch 'main' into chore-update-from-main-2
This commit is contained in:
@ -7,6 +7,7 @@ import { h, defineComponent, getCurrentInstance, inject } from "vue";
|
||||
interface TabState {
|
||||
activeTab?: string;
|
||||
tabs: { [k: string]: Tab };
|
||||
hasRouterOutlet?: boolean;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
@ -37,6 +38,7 @@ export const IonTabBar = defineComponent({
|
||||
/* 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() {
|
||||
@ -53,6 +55,7 @@ export const IonTabBar = defineComponent({
|
||||
},
|
||||
methods: {
|
||||
setupTabState(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
/**
|
||||
* For each tab, we need to keep track of its
|
||||
* base href as well as any child page that
|
||||
@ -72,27 +75,76 @@ 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
|
||||
* ion-tab-bar is managing for it.
|
||||
*/
|
||||
child.component.props._getTabState = () => tabState;
|
||||
|
||||
/**
|
||||
* If the router outlet is not defined, then the tabs are being used
|
||||
* as a basic tab navigation without the router. In this case, the
|
||||
* tabs will not emit the `ionTabsDidChange` and `ionTabsWillChange`
|
||||
* events through the `checkActiveTab` method. Instead, we need to
|
||||
* handle those events through the tab buttons.
|
||||
*/
|
||||
if (!hasRouterOutlet) {
|
||||
child.component.props._onClick = (
|
||||
event: CustomEvent<{
|
||||
href: string;
|
||||
selected: boolean;
|
||||
tab: string;
|
||||
}>
|
||||
) => {
|
||||
this.handleIonTabButtonClick(event);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.checkActiveTab(ionRouter);
|
||||
},
|
||||
/**
|
||||
* This method is called upon setup and when the
|
||||
* history changes. It checks the current route
|
||||
* and updates the active tab accordingly.
|
||||
*
|
||||
* History changes only occur when the router
|
||||
* outlet is present. Due to this, the
|
||||
* `ionTabsDidChange` and `ionTabsWillChange`
|
||||
* events are only emitted when the router
|
||||
* outlet is present. A different approach must
|
||||
* be taken for tabs without a router outlet.
|
||||
*
|
||||
* @param ionRouter
|
||||
*/
|
||||
checkActiveTab(ionRouter: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
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) => {
|
||||
let activeTab = tabKeys.find((key) => {
|
||||
const href = tabs[key].originalHref;
|
||||
return currentRoute.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tabs is being used as a basic tab navigation,
|
||||
* so we need to set the first tab as active since
|
||||
* `checkActiveTab` will not be called after setup.
|
||||
*/
|
||||
if (!activeTab && !hasRouterOutlet) {
|
||||
activeTab = tabKeys[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* For each tab, check to see if the
|
||||
* base href has changed. If so, update
|
||||
@ -147,6 +199,24 @@ export const IonTabBar = defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
this.tabSwitch(activeTab, ionRouter);
|
||||
},
|
||||
handleIonTabButtonClick(
|
||||
event: CustomEvent<{
|
||||
href: string;
|
||||
selected: boolean;
|
||||
tab: string;
|
||||
}>
|
||||
) {
|
||||
const activeTab = event.detail.tab;
|
||||
|
||||
this.tabSwitch(activeTab);
|
||||
},
|
||||
tabSwitch(activeTab: string, ionRouter?: any) {
|
||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
||||
const childNodes = this.$data.tabVnodes;
|
||||
const { activeTab: prevActiveTab } = this.$data.tabState;
|
||||
const tabState = this.$data.tabState;
|
||||
const activeChild = childNodes.find(
|
||||
(child: VNode) => isTabButton(child) && child.props?.tab === activeTab
|
||||
);
|
||||
@ -156,17 +226,20 @@ export const IonTabBar = defineComponent({
|
||||
if (activeChild) {
|
||||
tabDidChange && this.$props._tabsWillChange(activeTab);
|
||||
|
||||
ionRouter.handleSetCurrentTab(activeTab);
|
||||
if (hasRouterOutlet && ionRouter) {
|
||||
ionRouter.handleSetCurrentTab(activeTab);
|
||||
}
|
||||
|
||||
tabBar.selectedTab = tabState.activeTab = activeTab;
|
||||
|
||||
tabDidChange && this.$props._tabsDidChange(activeTab);
|
||||
} else {
|
||||
/**
|
||||
* When going to a tab that does
|
||||
* not have an associated ion-tab-button
|
||||
* we need to remove the selected state from
|
||||
* the old tab.
|
||||
*/
|
||||
} else {
|
||||
tabBar.selectedTab = tabState.activeTab = "";
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,10 @@ export const IonTabButton = /*@__PURE__*/ defineComponent({
|
||||
selected: Boolean,
|
||||
tab: String,
|
||||
target: String,
|
||||
_onClick: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
defineCustomElement();
|
||||
@ -37,11 +41,29 @@ export const IonTabButton = /*@__PURE__*/ defineComponent({
|
||||
*/
|
||||
const { tab, href, _getTabState } = props;
|
||||
const tabState = _getTabState();
|
||||
const hasRouterOutlet = tabState.hasRouterOutlet;
|
||||
const tappedTab = tabState.tabs[tab] || {};
|
||||
const originalHref = tappedTab.originalHref || href;
|
||||
const currentHref = tappedTab.currentHref || href;
|
||||
/**
|
||||
* If the router outlet is not defined, then the tabs is being used
|
||||
* as a basic tab navigation without the router. In this case, we
|
||||
* don't want to update the href else the URL will change.
|
||||
*/
|
||||
const currentHref = hasRouterOutlet ? tappedTab.currentHref || href : "";
|
||||
const prevActiveTab = tabState.activeTab;
|
||||
|
||||
if (!hasRouterOutlet && props._onClick) {
|
||||
props._onClick(
|
||||
new CustomEvent("ionTabButtonClick", {
|
||||
detail: {
|
||||
href: currentHref,
|
||||
selected: tab === prevActiveTab,
|
||||
tab,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are still on the same
|
||||
* tab as before, but the base href
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
|
||||
import type { VNode } from "vue";
|
||||
import { h, defineComponent } from "vue";
|
||||
import { h, defineComponent, Fragment, isVNode } from "vue";
|
||||
|
||||
import { IonTab } from "../proxies";
|
||||
|
||||
const WILL_CHANGE = "ionTabsWillChange";
|
||||
const DID_CHANGE = "ionTabsDidChange";
|
||||
@ -28,64 +31,82 @@ const isTabBar = (node: VNode) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isTab = (node: VNode): boolean => {
|
||||
// The `ion-tab` component was created with the `v-for` directive.
|
||||
if (node.type === Fragment) {
|
||||
if (Array.isArray(node.children)) {
|
||||
return node.children.some((child) => isVNode(child) && isTab(child));
|
||||
}
|
||||
|
||||
return false; // In case the fragment has no children.
|
||||
}
|
||||
|
||||
return (
|
||||
node.type && ((node.type as any).name === "ion-tab" || node.type === IonTab)
|
||||
);
|
||||
};
|
||||
|
||||
export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
name: "IonTabs",
|
||||
emits: [WILL_CHANGE, DID_CHANGE],
|
||||
setup(props, { slots, emit }) {
|
||||
return {
|
||||
props,
|
||||
slots,
|
||||
emit,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
/**
|
||||
* `defineCustomElement` must be called in the `mounted` hook
|
||||
* to ensure that the custom element is defined after the
|
||||
* component has been fully rendered and initialized.
|
||||
* This prevents issues with undefined properties, like
|
||||
* `selectedTab` from core, which may occur if the custom
|
||||
* element is defined too early in the component's lifecycle.
|
||||
*/
|
||||
defineCustomElement();
|
||||
},
|
||||
render() {
|
||||
const { $slots: slots, $emit } = this;
|
||||
const { slots, emit, props } = this;
|
||||
const slottedContent = slots.default && slots.default();
|
||||
let routerOutlet;
|
||||
let hasTab = false;
|
||||
|
||||
/**
|
||||
* Developers must pass an ion-router-outlet
|
||||
* inside of ion-tabs.
|
||||
*/
|
||||
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
|
||||
* wit the router.
|
||||
*/
|
||||
routerOutlet = slottedContent.find((child: VNode) =>
|
||||
isRouterOutlet(child)
|
||||
);
|
||||
}
|
||||
|
||||
if (!routerOutlet) {
|
||||
throw new Error(
|
||||
"IonTabs must contain an IonRouterOutlet. See https://ionicframework.com/docs/vue/navigation#working-with-tabs for more information."
|
||||
);
|
||||
}
|
||||
|
||||
let childrenToRender = [
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
class: "tabs-inner",
|
||||
style: {
|
||||
position: "relative",
|
||||
flex: "1",
|
||||
contain: "layout size style",
|
||||
},
|
||||
},
|
||||
routerOutlet
|
||||
),
|
||||
];
|
||||
|
||||
/**
|
||||
* If ion-tab-bar has slot="top" it needs to be
|
||||
* rendered before `.tabs-inner` otherwise it will
|
||||
* not show above the tab content.
|
||||
*/
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
/**
|
||||
* Render all content except for router outlet
|
||||
* since that needs to be inside of `.tabs-inner`.
|
||||
* Developers must pass at least one ion-tab
|
||||
* inside of ion-tabs if they want to use a
|
||||
* basic tab-based navigation without the
|
||||
* history stack or URL updates associated
|
||||
* with the router.
|
||||
*/
|
||||
const filteredContent = slottedContent.filter(
|
||||
(child: VNode) => !child.type || !isRouterOutlet(child)
|
||||
);
|
||||
hasTab = slottedContent.some((child: VNode) => isTab(child));
|
||||
}
|
||||
|
||||
const slottedTabBar = filteredContent.find((child: VNode) =>
|
||||
if (!routerOutlet && !hasTab) {
|
||||
throw new Error("IonTabs must contain an IonRouterOutlet or an IonTab.");
|
||||
}
|
||||
if (routerOutlet && hasTab) {
|
||||
throw new Error(
|
||||
"IonTabs cannot contain an IonRouterOutlet and IonTab at the same time."
|
||||
);
|
||||
}
|
||||
|
||||
if (slottedContent && slottedContent.length > 0) {
|
||||
const slottedTabBar = slottedContent.find((child: VNode) =>
|
||||
isTabBar(child)
|
||||
);
|
||||
const hasTopSlotTabBar =
|
||||
slottedTabBar && slottedTabBar.props?.slot === "top";
|
||||
|
||||
if (slottedTabBar) {
|
||||
if (!slottedTabBar.props) {
|
||||
@ -99,18 +120,34 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
* so we do not have code split across two components.
|
||||
*/
|
||||
slottedTabBar.props._tabsWillChange = (tab: string) =>
|
||||
$emit(WILL_CHANGE, { tab });
|
||||
emit(WILL_CHANGE, { tab });
|
||||
slottedTabBar.props._tabsDidChange = (tab: string) =>
|
||||
$emit(DID_CHANGE, { tab });
|
||||
}
|
||||
|
||||
if (hasTopSlotTabBar) {
|
||||
childrenToRender = [...filteredContent, ...childrenToRender];
|
||||
} else {
|
||||
childrenToRender = [...childrenToRender, ...filteredContent];
|
||||
emit(DID_CHANGE, { tab });
|
||||
slottedTabBar.props._hasRouterOutlet = !!routerOutlet;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTab) {
|
||||
return h(
|
||||
"ion-tabs",
|
||||
{
|
||||
...props,
|
||||
},
|
||||
slottedContent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(ROU-11056)
|
||||
*
|
||||
* Vue handles the error case for when there is no
|
||||
* associated page matching the tab `href`.
|
||||
*
|
||||
* More investigation is needed to determine if we
|
||||
* override the error handling and provide our own
|
||||
* error message.
|
||||
*/
|
||||
|
||||
return h(
|
||||
"ion-tabs",
|
||||
{
|
||||
@ -128,7 +165,7 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||
"z-index": "0",
|
||||
},
|
||||
},
|
||||
childrenToRender
|
||||
slottedContent
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -72,6 +72,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon
|
||||
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
|
||||
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
|
||||
import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js';
|
||||
import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js';
|
||||
import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js';
|
||||
import { defineCustomElement as defineIonTextarea } from '@ionic/core/components/ion-textarea.js';
|
||||
import { defineCustomElement as defineIonThumbnail } from '@ionic/core/components/ion-thumbnail.js';
|
||||
@ -824,6 +825,14 @@ export const IonSplitPane = /*@__PURE__*/ defineContainer<JSX.IonSplitPane>('ion
|
||||
]);
|
||||
|
||||
|
||||
export const IonTab = /*@__PURE__*/ defineContainer<JSX.IonTab>('ion-tab', defineIonTab, [
|
||||
'active',
|
||||
'delegate',
|
||||
'tab',
|
||||
'component'
|
||||
]);
|
||||
|
||||
|
||||
export const IonText = /*@__PURE__*/ defineContainer<JSX.IonText>('ion-text', defineIonText, [
|
||||
'color'
|
||||
]);
|
||||
|
Reference in New Issue
Block a user