Merge branch 'main' into chore-update-from-main-2

This commit is contained in:
Brandy Carney
2024-09-06 10:25:27 -04:00
765 changed files with 11338 additions and 1754 deletions

View File

@ -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 = "";
}
}

View File

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

View File

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

View File

@ -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'
]);