mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 01:03:03 +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:
@ -8,6 +8,8 @@ import { IonTabBarInner } from '../inner-proxies';
|
|||||||
import { createForwardRef } from '../utils';
|
import { createForwardRef } from '../utils';
|
||||||
|
|
||||||
import { IonTabButton } from './IonTabButton';
|
import { IonTabButton } from './IonTabButton';
|
||||||
|
import { IonTabsContext } from './IonTabsContext';
|
||||||
|
import type { IonTabsContextState } from './IonTabsContext';
|
||||||
|
|
||||||
type IonTabBarProps = LocalJSX.IonTabBar &
|
type IonTabBarProps = LocalJSX.IonTabBar &
|
||||||
IonicReactProps & {
|
IonicReactProps & {
|
||||||
@ -21,7 +23,7 @@ interface InternalProps extends IonTabBarProps {
|
|||||||
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
||||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||||
routeInfo: RouteInfo;
|
routeInfo: RouteInfo;
|
||||||
routerOutletRef?: React.RefObject<HTMLIonRouterOutletElement> | undefined;
|
tabsContext?: IonTabsContextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabUrls {
|
interface TabUrls {
|
||||||
@ -183,12 +185,14 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
|||||||
) {
|
) {
|
||||||
const tappedTab = this.state.tabs[e.detail.tab];
|
const tappedTab = this.state.tabs[e.detail.tab];
|
||||||
const originalHref = tappedTab.originalHref;
|
const originalHref = tappedTab.originalHref;
|
||||||
|
const hasRouterOutlet = this.props.tabsContext?.hasRouterOutlet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the router outlet is not defined, then the tabs is being used
|
* 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
|
* as a basic tab navigation without the router. In this case, we
|
||||||
* don't want to update the href else the URL will change.
|
* don't want to update the href else the URL will change.
|
||||||
*/
|
*/
|
||||||
const currentHref = this.props.routerOutletRef?.current ? e.detail.href : '';
|
const currentHref = hasRouterOutlet ? e.detail.href : '';
|
||||||
const { activeTab: prevActiveTab } = this.state;
|
const { activeTab: prevActiveTab } = this.state;
|
||||||
|
|
||||||
if (onClickFn) {
|
if (onClickFn) {
|
||||||
@ -212,7 +216,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
|||||||
if (this.props.onIonTabsDidChange) {
|
if (this.props.onIonTabsDidChange) {
|
||||||
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
||||||
}
|
}
|
||||||
if (this.props.routerOutletRef?.current) {
|
if (hasRouterOutlet) {
|
||||||
this.setActiveTabOnContext(e.detail.tab);
|
this.setActiveTabOnContext(e.detail.tab);
|
||||||
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
|
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
|
||||||
}
|
}
|
||||||
@ -262,12 +266,29 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
|||||||
|
|
||||||
const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
|
const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
|
||||||
const context = useContext(NavContext);
|
const context = useContext(NavContext);
|
||||||
|
const tabsContext = useContext(IonTabsContext);
|
||||||
|
const tabBarRef = forwardedRef || tabsContext.tabBarProps.ref;
|
||||||
|
const updatedTabBarProps = {
|
||||||
|
...tabsContext.tabBarProps,
|
||||||
|
ref: tabBarRef,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IonTabBarUnwrapped
|
<IonTabBarUnwrapped
|
||||||
ref={forwardedRef}
|
ref={tabBarRef}
|
||||||
{...(props as any)}
|
{...(props as any)}
|
||||||
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
|
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
|
||||||
onSetCurrentTab={context.setCurrentTab}
|
onSetCurrentTab={context.setCurrentTab}
|
||||||
|
/**
|
||||||
|
* Tab bar can be used as a standalone component,
|
||||||
|
* so it cannot be modified directly through
|
||||||
|
* IonTabs. Instead, props will be passed through
|
||||||
|
* the context.
|
||||||
|
*/
|
||||||
|
tabsContext={{
|
||||||
|
...tabsContext,
|
||||||
|
tabBarProps: updatedTabBarProps,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</IonTabBarUnwrapped>
|
</IonTabBarUnwrapped>
|
||||||
|
@ -8,7 +8,6 @@ import { IonRouterOutlet } from '../IonRouterOutlet';
|
|||||||
import { IonTabsInner } from '../inner-proxies';
|
import { IonTabsInner } from '../inner-proxies';
|
||||||
import { IonTab } from '../proxies';
|
import { IonTab } from '../proxies';
|
||||||
|
|
||||||
import { IonTabBar } from './IonTabBar';
|
|
||||||
import type { IonTabsContextState } from './IonTabsContext';
|
import type { IonTabsContextState } from './IonTabsContext';
|
||||||
import { IonTabsContext } from './IonTabsContext';
|
import { IonTabsContext } from './IonTabsContext';
|
||||||
|
|
||||||
@ -43,28 +42,15 @@ interface Props extends LocalJSX.IonTabs {
|
|||||||
children: ChildFunction | React.ReactNode;
|
children: ChildFunction | React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostStyles: React.CSSProperties = {
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
right: '0',
|
|
||||||
bottom: '0',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
contain: 'layout size style',
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabsInner: React.CSSProperties = {
|
|
||||||
position: 'relative',
|
|
||||||
flex: 1,
|
|
||||||
contain: 'layout size style',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IonTabs = /*@__PURE__*/ (() =>
|
export const IonTabs = /*@__PURE__*/ (() =>
|
||||||
class extends React.Component<Props> {
|
class extends React.Component<Props> {
|
||||||
context!: React.ContextType<typeof NavContext>;
|
context!: React.ContextType<typeof NavContext>;
|
||||||
|
/**
|
||||||
|
* `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`.
|
||||||
|
* Without this, `ref.current` will be `undefined` in the user's app,
|
||||||
|
* breaking their ability to access the `IonRouterOutlet` instance.
|
||||||
|
* Do not remove this ref.
|
||||||
|
*/
|
||||||
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
|
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
|
||||||
selectTabHandler?: (tag: string) => boolean;
|
selectTabHandler?: (tag: string) => boolean;
|
||||||
tabBarRef = React.createRef<any>();
|
tabBarRef = React.createRef<any>();
|
||||||
@ -72,6 +58,14 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
ionTabContextState: IonTabsContextState = {
|
ionTabContextState: IonTabsContextState = {
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
selectTab: () => false,
|
selectTab: () => false,
|
||||||
|
hasRouterOutlet: false,
|
||||||
|
/**
|
||||||
|
* Tab bar can be used as a standalone component,
|
||||||
|
* so the props can not be passed directly to the
|
||||||
|
* tab bar component. Instead, props will be
|
||||||
|
* passed through the context.
|
||||||
|
*/
|
||||||
|
tabBarProps: { ref: this.tabBarRef },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@ -90,9 +84,32 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
|
||||||
|
return (
|
||||||
|
<IonTabsInner {...this.props}>
|
||||||
|
{React.Children.map(children, (child: React.ReactNode) => {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
const isRouterOutlet =
|
||||||
|
child.type === IonRouterOutlet ||
|
||||||
|
(child.type as any).isRouterOutlet ||
|
||||||
|
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
|
||||||
|
|
||||||
|
if (isRouterOutlet) {
|
||||||
|
/**
|
||||||
|
* The modified outlet needs to be returned to include
|
||||||
|
* the ref.
|
||||||
|
*/
|
||||||
|
return outlet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
})}
|
||||||
|
</IonTabsInner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let outlet: React.ReactElement<{}> | undefined;
|
let outlet: React.ReactElement<{}> | undefined;
|
||||||
let tabBar: React.ReactElement | undefined;
|
|
||||||
// Check if IonTabs has any IonTab children
|
// Check if IonTabs has any IonTab children
|
||||||
let hasTab = false;
|
let hasTab = false;
|
||||||
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
|
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
|
||||||
@ -102,19 +119,15 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
? (this.props.children as ChildFunction)(this.ionTabContextState)
|
? (this.props.children as ChildFunction)(this.ionTabContextState)
|
||||||
: this.props.children;
|
: this.props.children;
|
||||||
|
|
||||||
const outletProps = {
|
|
||||||
ref: this.routerOutletRef,
|
|
||||||
};
|
|
||||||
|
|
||||||
React.Children.forEach(children, (child: any) => {
|
React.Children.forEach(children, (child: any) => {
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
|
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
|
||||||
outlet = React.cloneElement(child, outletProps);
|
outlet = React.cloneElement(child);
|
||||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||||
outlet = React.cloneElement(child.props.children[0], outletProps);
|
outlet = React.cloneElement(child.props.children[0]);
|
||||||
} else if (child.type === IonTab) {
|
} else if (child.type === IonTab) {
|
||||||
/**
|
/**
|
||||||
* This indicates that IonTabs will be using a basic tab-based navigation
|
* This indicates that IonTabs will be using a basic tab-based navigation
|
||||||
@ -123,9 +136,10 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
hasTab = true;
|
hasTab = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ionTabContextState.hasRouterOutlet = !!outlet;
|
||||||
|
|
||||||
let childProps: any = {
|
let childProps: any = {
|
||||||
ref: this.tabBarRef,
|
...this.ionTabContextState.tabBarProps,
|
||||||
routerOutletRef: this.routerOutletRef,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,14 +163,7 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.type === IonTabBar || child.type.isTabBar) {
|
this.ionTabContextState.tabBarProps = childProps;
|
||||||
tabBar = React.cloneElement(child, childProps);
|
|
||||||
} else if (
|
|
||||||
child.type === Fragment &&
|
|
||||||
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar)
|
|
||||||
) {
|
|
||||||
tabBar = React.cloneElement(child.props.children[1], childProps);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!outlet && !hasTab) {
|
if (!outlet && !hasTab) {
|
||||||
@ -186,46 +193,10 @@ export const IonTabs = /*@__PURE__*/ (() =>
|
|||||||
<IonTabsContext.Provider value={this.ionTabContextState}>
|
<IonTabsContext.Provider value={this.ionTabContextState}>
|
||||||
{this.context.hasIonicRouter() ? (
|
{this.context.hasIonicRouter() ? (
|
||||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||||
<IonTabsInner {...this.props}>
|
{this.renderTabsInner(children, outlet)}
|
||||||
{React.Children.map(children, (child: React.ReactNode) => {
|
|
||||||
if (React.isValidElement(child)) {
|
|
||||||
const isTabBar =
|
|
||||||
child.type === IonTabBar ||
|
|
||||||
(child.type as any).isTabBar ||
|
|
||||||
(child.type === Fragment &&
|
|
||||||
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar));
|
|
||||||
const isRouterOutlet =
|
|
||||||
child.type === IonRouterOutlet ||
|
|
||||||
(child.type as any).isRouterOutlet ||
|
|
||||||
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
|
|
||||||
|
|
||||||
if (isTabBar) {
|
|
||||||
/**
|
|
||||||
* The modified tabBar needs to be returned to include
|
|
||||||
* the context and the overridden methods.
|
|
||||||
*/
|
|
||||||
return tabBar;
|
|
||||||
}
|
|
||||||
if (isRouterOutlet) {
|
|
||||||
/**
|
|
||||||
* The modified outlet needs to be returned to include
|
|
||||||
* the ref.
|
|
||||||
*/
|
|
||||||
return outlet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
})}
|
|
||||||
</IonTabsInner>
|
|
||||||
</PageManager>
|
</PageManager>
|
||||||
) : (
|
) : (
|
||||||
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
|
this.renderTabsInner(children, outlet)
|
||||||
{tabBar?.props.slot === 'top' ? tabBar : null}
|
|
||||||
<div style={tabsInner} className="tabs-inner">
|
|
||||||
{outlet}
|
|
||||||
</div>
|
|
||||||
{tabBar?.props.slot === 'bottom' ? tabBar : null}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</IonTabsContext.Provider>
|
</IonTabsContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -3,9 +3,25 @@ import React from 'react';
|
|||||||
export interface IonTabsContextState {
|
export interface IonTabsContextState {
|
||||||
activeTab: string | undefined;
|
activeTab: string | undefined;
|
||||||
selectTab: (tab: string) => boolean;
|
selectTab: (tab: string) => boolean;
|
||||||
|
hasRouterOutlet: boolean;
|
||||||
|
tabBarProps: TabBarProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab bar can be used as a standalone component,
|
||||||
|
* so the props can not be passed directly to the
|
||||||
|
* tab bar component. Instead, props will be
|
||||||
|
* passed through the context.
|
||||||
|
*/
|
||||||
|
type TabBarProps = {
|
||||||
|
ref: React.RefObject<any>;
|
||||||
|
onIonTabsWillChange?: (e: CustomEvent) => void;
|
||||||
|
onIonTabsDidChange?: (e: CustomEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const IonTabsContext = React.createContext<IonTabsContextState>({
|
export const IonTabsContext = React.createContext<IonTabsContextState>({
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
selectTab: () => false,
|
selectTab: () => false,
|
||||||
|
hasRouterOutlet: false,
|
||||||
|
tabBarProps: { ref: React.createRef() },
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { defineCustomElement } from "@ionic/core/components/ion-tab-bar.js";
|
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";
|
import { h, defineComponent, getCurrentInstance, inject } from "vue";
|
||||||
|
|
||||||
// TODO(FW-2969): types
|
// TODO(FW-2969): types
|
||||||
@ -16,6 +16,12 @@ interface Tab {
|
|||||||
ref: VNode;
|
ref: VNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TabBarData {
|
||||||
|
hasRouterOutlet: boolean;
|
||||||
|
_tabsWillChange: Function;
|
||||||
|
_tabsDidChange: Function;
|
||||||
|
}
|
||||||
|
|
||||||
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
|
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
|
||||||
|
|
||||||
const getTabs = (nodes: VNode[]) => {
|
const getTabs = (nodes: VNode[]) => {
|
||||||
@ -34,20 +40,23 @@ const getTabs = (nodes: VNode[]) => {
|
|||||||
|
|
||||||
export const IonTabBar = defineComponent({
|
export const IonTabBar = defineComponent({
|
||||||
name: "IonTabBar",
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tabState: {
|
tabState: {
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
tabs: {},
|
tabs: {},
|
||||||
|
/**
|
||||||
|
* Passing this prop to each tab button
|
||||||
|
* lets it be aware of the presence of
|
||||||
|
* the router outlet.
|
||||||
|
*/
|
||||||
|
hasRouterOutlet: false,
|
||||||
},
|
},
|
||||||
tabVnodes: [],
|
tabVnodes: [],
|
||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
_tabsWillChange: { type: Function, default: () => {} },
|
||||||
|
_tabsDidChange: { type: Function, default: () => {} },
|
||||||
|
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
@ -55,7 +64,7 @@ export const IonTabBar = defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setupTabState(ionRouter: any) {
|
setupTabState(ionRouter: any) {
|
||||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||||
/**
|
/**
|
||||||
* For each tab, we need to keep track of its
|
* For each tab, we need to keep track of its
|
||||||
* base href as well as any child page that
|
* base href as well as any child page that
|
||||||
@ -75,13 +84,6 @@ export const IonTabBar = defineComponent({
|
|||||||
ref: child,
|
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
|
* Passing this prop to each tab button
|
||||||
* lets it be aware of the state that
|
* lets it be aware of the state that
|
||||||
@ -126,7 +128,7 @@ export const IonTabBar = defineComponent({
|
|||||||
* @param ionRouter
|
* @param ionRouter
|
||||||
*/
|
*/
|
||||||
checkActiveTab(ionRouter: any) {
|
checkActiveTab(ionRouter: any) {
|
||||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||||
const currentRoute = ionRouter?.getCurrentRouteInfo();
|
const currentRoute = ionRouter?.getCurrentRouteInfo();
|
||||||
const childNodes = this.$data.tabVnodes;
|
const childNodes = this.$data.tabVnodes;
|
||||||
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
|
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
|
||||||
@ -216,7 +218,7 @@ export const IonTabBar = defineComponent({
|
|||||||
this.tabSwitch(activeTab);
|
this.tabSwitch(activeTab);
|
||||||
},
|
},
|
||||||
tabSwitch(activeTab: string, ionRouter?: any) {
|
tabSwitch(activeTab: string, ionRouter?: any) {
|
||||||
const hasRouterOutlet = this.$props._hasRouterOutlet;
|
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
|
||||||
const childNodes = this.$data.tabVnodes;
|
const childNodes = this.$data.tabVnodes;
|
||||||
const { activeTab: prevActiveTab } = this.$data.tabState;
|
const { activeTab: prevActiveTab } = this.$data.tabState;
|
||||||
const tabState = this.$data.tabState;
|
const tabState = this.$data.tabState;
|
||||||
@ -227,7 +229,7 @@ export const IonTabBar = defineComponent({
|
|||||||
const tabDidChange = activeTab !== prevActiveTab;
|
const tabDidChange = activeTab !== prevActiveTab;
|
||||||
if (tabBar) {
|
if (tabBar) {
|
||||||
if (activeChild) {
|
if (activeChild) {
|
||||||
tabDidChange && this.$props._tabsWillChange(activeTab);
|
tabDidChange && this.$data._tabsWillChange(activeTab);
|
||||||
|
|
||||||
if (hasRouterOutlet && ionRouter !== null) {
|
if (hasRouterOutlet && ionRouter !== null) {
|
||||||
ionRouter.handleSetCurrentTab(activeTab);
|
ionRouter.handleSetCurrentTab(activeTab);
|
||||||
@ -235,7 +237,7 @@ export const IonTabBar = defineComponent({
|
|||||||
|
|
||||||
tabBar.selectedTab = tabState.activeTab = activeTab;
|
tabBar.selectedTab = tabState.activeTab = activeTab;
|
||||||
|
|
||||||
tabDidChange && this.$props._tabsDidChange(activeTab);
|
tabDidChange && this.$data._tabsDidChange(activeTab);
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* When going to a tab that does
|
* When going to a tab that does
|
||||||
@ -250,6 +252,17 @@ export const IonTabBar = defineComponent({
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const ionRouter: any = inject("navManager", null);
|
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);
|
this.setupTabState(ionRouter);
|
||||||
|
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
|
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
|
||||||
import type { VNode } from "vue";
|
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";
|
import { IonTab } from "../proxies";
|
||||||
|
|
||||||
@ -9,6 +16,12 @@ const DID_CHANGE = "ionTabsDidChange";
|
|||||||
|
|
||||||
// TODO(FW-2969): types
|
// TODO(FW-2969): types
|
||||||
|
|
||||||
|
interface TabBarData {
|
||||||
|
hasRouterOutlet: boolean;
|
||||||
|
_tabsWillChange: Function;
|
||||||
|
_tabsDidChange: Function;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vue 3.2.38 fixed an issue where Web Component
|
* Vue 3.2.38 fixed an issue where Web Component
|
||||||
* names are respected using kebab case instead of pascal case.
|
* 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 => {
|
const isTab = (node: VNode): boolean => {
|
||||||
// The `ion-tab` component was created with the `v-for` directive.
|
// The `ion-tab` component was created with the `v-for` directive.
|
||||||
if (node.type === Fragment) {
|
if (node.type === Fragment) {
|
||||||
@ -49,7 +55,43 @@ const isTab = (node: VNode): boolean => {
|
|||||||
export const IonTabs = /*@__PURE__*/ defineComponent({
|
export const IonTabs = /*@__PURE__*/ defineComponent({
|
||||||
name: "IonTabs",
|
name: "IonTabs",
|
||||||
emits: [WILL_CHANGE, DID_CHANGE],
|
emits: [WILL_CHANGE, DID_CHANGE],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasRouterOutlet: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
setup(props, { slots, emit }) {
|
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 {
|
return {
|
||||||
props,
|
props,
|
||||||
slots,
|
slots,
|
||||||
@ -68,9 +110,10 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
|||||||
defineCustomElement();
|
defineCustomElement();
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const { slots, emit, props } = this;
|
const { slots, props } = this;
|
||||||
const slottedContent = slots.default && slots.default();
|
const slottedContent: VNode[] | undefined =
|
||||||
let routerOutlet;
|
slots.default && slots.default();
|
||||||
|
let routerOutlet: VNode | undefined = undefined;
|
||||||
let hasTab = false;
|
let hasTab = false;
|
||||||
|
|
||||||
if (slottedContent && slottedContent.length > 0) {
|
if (slottedContent && slottedContent.length > 0) {
|
||||||
@ -78,7 +121,7 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
|
|||||||
* Developers must pass an ion-router-outlet
|
* Developers must pass an ion-router-outlet
|
||||||
* inside of ion-tabs if they want to use
|
* inside of ion-tabs if they want to use
|
||||||
* the history stack or URL updates associated
|
* the history stack or URL updates associated
|
||||||
* wit the router.
|
* with the router.
|
||||||
*/
|
*/
|
||||||
routerOutlet = slottedContent.find((child: VNode) =>
|
routerOutlet = slottedContent.find((child: VNode) =>
|
||||||
isRouterOutlet(child)
|
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) {
|
if (hasTab) {
|
||||||
return h(
|
return h(
|
||||||
"ion-tabs",
|
"ion-tabs",
|
||||||
|
Reference in New Issue
Block a user