feat(react, vue, angular): use tabs without router (#29794)

Issue number: resolves #25184 

---------

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
This commit is contained in:
Maria Hutt
2024-08-26 08:13:20 -07:00
committed by GitHub
parent 4580edc21f
commit 867066b4eb
38 changed files with 1946 additions and 1281 deletions

View File

@ -4,6 +4,7 @@ import { defineCustomElement as defineIonBackButton } from '@ionic/core/componen
import { defineCustomElement as defineIonRouterOutlet } from '@ionic/core/components/ion-router-outlet.js';
import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js';
import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js';
import { defineCustomElement as defineIonTabs } from '@ionic/core/components/ion-tabs.js';
import type { JSX as IoniconsJSX } from 'ionicons';
import { defineCustomElement as defineIonIcon } from 'ionicons/components/ion-icon.js';
@ -19,6 +20,12 @@ export const IonTabBarInner = /*@__PURE__*/ createReactComponent<JSX.IonTabBar,
undefined,
defineIonTabBar
);
export const IonTabsInner = /*@__PURE__*/ createReactComponent<JSX.IonTabs, HTMLIonTabsElement>(
'ion-tabs',
undefined,
undefined,
defineIonTabs
);
export const IonBackButtonInner = /*@__PURE__*/ createReactComponent<
Omit<JSX.IonBackButton, 'icon'>,
HTMLIonBackButtonElement

View File

@ -21,6 +21,7 @@ interface InternalProps extends IonTabBarProps {
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
routeInfo: RouteInfo;
routerOutletRef?: React.RefObject<HTMLIonRouterOutletElement> | undefined;
}
interface TabUrls {
@ -182,7 +183,12 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
) {
const tappedTab = this.state.tabs[e.detail.tab];
const originalHref = tappedTab.originalHref;
const currentHref = e.detail.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 = this.props.routerOutletRef?.current ? e.detail.href : '';
const { activeTab: prevActiveTab } = this.state;
if (onClickFn) {

View File

@ -5,6 +5,8 @@ import { NavContext } from '../../contexts/NavContext';
import PageManager from '../../routing/PageManager';
import { HTMLElementSSR } from '../../utils/HTMLElementSSR';
import { IonRouterOutlet } from '../IonRouterOutlet';
import { IonTabsInner } from '../inner-proxies';
import { IonTab } from '../proxies';
import { IonTabBar } from './IonTabBar';
import type { IonTabsContextState } from './IonTabsContext';
@ -91,6 +93,8 @@ export const IonTabs = /*@__PURE__*/ (() =>
render() {
let outlet: React.ReactElement<{}> | undefined;
let tabBar: React.ReactElement | undefined;
// Check if IonTabs has any IonTab children
let hasTab = false;
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
const children =
@ -98,19 +102,30 @@ export const IonTabs = /*@__PURE__*/ (() =>
? (this.props.children as ChildFunction)(this.ionTabContextState)
: this.props.children;
const outletProps = {
ref: this.routerOutletRef,
};
React.Children.forEach(children, (child: any) => {
// eslint-disable-next-line no-prototype-builtins
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
return;
}
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
outlet = React.cloneElement(child);
outlet = React.cloneElement(child, outletProps);
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
outlet = child.props.children[0];
outlet = React.cloneElement(child.props.children[0], outletProps);
} else if (child.type === IonTab) {
/**
* This indicates that IonTabs will be using a basic tab-based navigation
* without the history stack or URL updates associated with the router.
*/
hasTab = true;
}
let childProps: any = {
ref: this.tabBarRef,
routerOutletRef: this.routerOutletRef,
};
/**
@ -144,24 +159,67 @@ export const IonTabs = /*@__PURE__*/ (() =>
}
});
if (!outlet) {
throw new Error('IonTabs must contain an IonRouterOutlet');
if (!outlet && !hasTab) {
throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab');
}
if (outlet && hasTab) {
throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time');
}
if (!tabBar) {
throw new Error('IonTabs needs a IonTabBar');
}
if (hasTab) {
return <IonTabsInner {...this.props}></IonTabsInner>;
}
/**
* TODO(ROU-11051)
*
* There is no error handling for the case where there
* is no associated Route for the given IonTabButton.
*
* More investigation is needed to determine how to
* handle this to prevent any overwriting of the
* IonTabButton's onClick handler and how the routing
* is handled.
*/
return (
<IonTabsContext.Provider value={this.ionTabContextState}>
{this.context.hasIonicRouter() ? (
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
<ion-tabs className="ion-tabs" style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{outlet}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</ion-tabs>
<IonTabsInner {...this.props}>
{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>
) : (
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>