Files
2023-01-18 16:49:25 -05:00

270 lines
8.6 KiB
TypeScript

import type { JSX as LocalJSX } from '@ionic/core/components';
import React, { useContext } from 'react';
import { NavContext } from '../../contexts/NavContext';
import type { RouteInfo } from '../../models';
import type { IonicReactProps } from '../IonicReactProps';
import { IonTabBarInner } from '../inner-proxies';
import { createForwardRef } from '../utils';
import { IonTabButton } from './IonTabButton';
type IonTabBarProps = LocalJSX.IonTabBar &
IonicReactProps & {
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
slot?: 'bottom' | 'top';
style?: { [key: string]: string };
};
interface InternalProps extends IonTabBarProps {
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
routeInfo: RouteInfo;
}
interface TabUrls {
originalHref: string;
currentHref: string;
originalRouteOptions?: unknown;
currentRouteOptions?: unknown;
}
interface IonTabBarState {
activeTab?: string;
tabs: { [key: string]: TabUrls };
}
// TODO(FW-2959): types
class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarState> {
context!: React.ContextType<typeof NavContext>;
constructor(props: InternalProps) {
super(props);
const tabs: { [key: string]: TabUrls } = {};
React.Children.forEach((props as any).children, (child: any) => {
if (
child != null &&
typeof child === 'object' &&
child.props &&
(child.type === IonTabButton || child.type.isTabButton)
) {
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href,
originalRouteOptions:
child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
currentRouteOptions:
child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
};
}
});
this.state = {
tabs,
};
this.onTabButtonClick = this.onTabButtonClick.bind(this);
this.renderTabButton = this.renderTabButton.bind(this);
this.setActiveTabOnContext = this.setActiveTabOnContext.bind(this);
this.selectTab = this.selectTab.bind(this);
}
componentDidMount() {
const tabs = this.state.tabs;
const tabKeys = Object.keys(tabs);
const activeTab = tabKeys.find((key) => {
const href = tabs[key].originalHref;
return this.props.routeInfo!.pathname.startsWith(href);
});
if (activeTab) {
this.setState({
activeTab,
});
}
}
componentDidUpdate() {
if (this.state.activeTab) {
this.setActiveTabOnContext(this.state.activeTab);
}
}
// eslint-disable-next-line
setActiveTabOnContext = (_tab: string) => {};
selectTab(tab: string) {
const tabUrl = this.state.tabs[tab];
if (tabUrl) {
this.onTabButtonClick(
new CustomEvent('ionTabButtonClick', {
detail: {
href: tabUrl.currentHref,
tab,
selected: tab === this.state.activeTab,
routeOptions: undefined,
},
})
);
return true;
}
return false;
}
static getDerivedStateFromProps(props: InternalProps, state: IonTabBarState) {
const tabs = { ...state.tabs };
const tabKeys = Object.keys(state.tabs);
const activeTab = tabKeys.find((key) => {
const href = state.tabs[key].originalHref;
return props.routeInfo!.pathname.startsWith(href);
});
// Check to see if the tab button href has changed, and if so, update it in the tabs state
React.Children.forEach((props as any).children, (child: any) => {
if (
child != null &&
typeof child === 'object' &&
child.props &&
(child.type === IonTabButton || child.type.isTabButton)
) {
const tab = tabs[child.props.tab];
if (!tab || tab.originalHref !== child.props.href) {
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href,
originalRouteOptions: child.props.routeOptions,
currentRouteOptions: child.props.routeOptions,
};
}
}
});
const { activeTab: prevActiveTab } = state;
if (activeTab && prevActiveTab) {
const prevHref = state.tabs[prevActiveTab].currentHref;
const prevRouteOptions = state.tabs[prevActiveTab].currentRouteOptions;
if (
activeTab !== prevActiveTab ||
prevHref !== props.routeInfo?.pathname ||
prevRouteOptions !== props.routeInfo?.routeOptions
) {
tabs[activeTab] = {
originalHref: tabs[activeTab].originalHref,
currentHref: props.routeInfo!.pathname + (props.routeInfo!.search || ''),
originalRouteOptions: tabs[activeTab].originalRouteOptions,
currentRouteOptions: props.routeInfo?.routeOptions,
};
if (props.routeInfo.routeAction === 'pop' && activeTab !== prevActiveTab) {
// If navigating back and the tabs change, set the prev tab back to its original href
tabs[prevActiveTab] = {
originalHref: tabs[prevActiveTab].originalHref,
currentHref: tabs[prevActiveTab].originalHref,
originalRouteOptions: tabs[prevActiveTab].originalRouteOptions,
currentRouteOptions: tabs[prevActiveTab].currentRouteOptions,
};
}
}
}
activeTab && props.onSetCurrentTab(activeTab, props.routeInfo);
return {
activeTab,
tabs,
};
}
private onTabButtonClick(
e: CustomEvent<{ href: string; selected: boolean; tab: string; routeOptions: unknown }>,
onClickFn?: (e: any) => void
) {
const tappedTab = this.state.tabs[e.detail.tab];
const originalHref = tappedTab.originalHref;
const currentHref = e.detail.href;
const { activeTab: prevActiveTab } = this.state;
if (onClickFn) {
/**
* If the user provides an onClick function, we call it
* with the original event.
*/
onClickFn(e);
}
// this.props.onSetCurrentTab(e.detail.tab, this.props.routeInfo);
// Clicking the current tab will bring you back to the original href
if (prevActiveTab === e.detail.tab) {
if (originalHref !== currentHref) {
this.context.resetTab(e.detail.tab, originalHref, tappedTab.originalRouteOptions);
}
} else {
if (this.props.onIonTabsWillChange) {
this.props.onIonTabsWillChange(new CustomEvent('ionTabWillChange', { detail: { tab: e.detail.tab } }));
}
if (this.props.onIonTabsDidChange) {
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
}
this.setActiveTabOnContext(e.detail.tab);
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
}
}
private renderTabButton(activeTab: string | null | undefined) {
return (
child:
| React.ReactElement<LocalJSX.IonTabButton & { onClick: (e: any) => void; routeOptions?: unknown }>
| null
| undefined
) => {
if (child != null && child.props && (child.type === IonTabButton || (child as any).type.isTabButton)) {
const href =
child.props.tab === activeTab
? this.props.routeInfo?.pathname
: this.state.tabs[child.props.tab!].currentHref;
const routeOptions =
child.props.tab === activeTab
? this.props.routeInfo?.routeOptions
: this.state.tabs[child.props.tab!].currentRouteOptions;
return React.cloneElement(child, {
href,
routeOptions,
onClick: (ev: CustomEvent) => this.onTabButtonClick(ev, child.props.onClick),
});
}
return null;
};
}
render() {
const { activeTab } = this.state;
return (
<IonTabBarInner {...this.props} selectedTab={activeTab}>
{React.Children.map(this.props.children as any, this.renderTabButton(activeTab))}
</IonTabBarInner>
);
}
static get contextType() {
return NavContext;
}
}
const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
const context = useContext(NavContext);
return (
<IonTabBarUnwrapped
ref={forwardedRef}
{...(props as any)}
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
onSetCurrentTab={context.setCurrentTab}
>
{props.children}
</IonTabBarUnwrapped>
);
});
export const IonTabBar = createForwardRef<IonTabBarProps, HTMLIonTabBarElement>(IonTabBarContainer, 'IonTabBar');