diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 7e8dd3069a..88ec6f60d5 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -18,6 +18,7 @@ export { IonPopover } from './IonPopover'; // Custom Components export { IonPage } from './IonPage'; +export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext'; export { IonTabs } from './navigation/IonTabs'; export { IonTabBar } from './navigation/IonTabBar'; export { IonBackButton } from './navigation/IonBackButton'; diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx index c188f732b7..024c1b9cee 100644 --- a/packages/react/src/components/navigation/IonTabBar.tsx +++ b/packages/react/src/components/navigation/IonTabBar.tsx @@ -5,28 +5,33 @@ import { NavContext } from '../../contexts/NavContext'; import { IonicReactProps } from '../IonicReactProps'; import { IonTabBarInner } from '../inner-proxies'; import { IonTabButton } from '../proxies'; +import { createForwardRef } from '../utils'; -type Props = LocalJSX.IonTabBar & IonicReactProps & { - onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void; - onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void; +type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & { + onIonTabsDidChange?: (event: CustomEvent<{ tab: string; }>) => void; + onIonTabsWillChange?: (event: CustomEvent<{ tab: string; }>) => void; currentPath?: string; slot?: 'bottom' | 'top'; }; +interface InternalProps extends IonTabBarProps { + forwardedRef?: React.RefObject; +} + interface TabUrls { originalHref: string; currentHref: string; } -interface State { +interface IonTabBarState { activeTab: string | undefined; - tabs: { [key: string]: TabUrls }; + tabs: { [key: string]: TabUrls; }; } -class IonTabBarUnwrapped extends React.PureComponent { +class IonTabBarUnwrapped extends React.PureComponent { context!: React.ContextType; - constructor(props: Props) { + constructor(props: InternalProps) { super(props); const tabs: { [key: string]: TabUrls; } = {}; @@ -39,16 +44,42 @@ class IonTabBarUnwrapped extends React.PureComponent { } }); + const tabKeys = Object.keys(tabs); + const activeTab = tabKeys + .find(key => { + const href = tabs[key].originalHref; + return props.currentPath!.startsWith(href); + }) || tabKeys[0]; + this.state = { - activeTab: undefined, + activeTab, 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); } - static getDerivedStateFromProps(props: Props, state: State) { + 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 + } + })); + return true; + } + return false; + } + + static getDerivedStateFromProps(props: IonTabBarProps, state: IonTabBarState) { const tabs = { ...state.tabs }; const activeTab = Object.keys(state.tabs) @@ -101,6 +132,7 @@ class IonTabBarUnwrapped extends React.PureComponent { if (this.props.onIonTabsDidChange) { this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } })); } + this.setActiveTabOnContext(e.detail.tab); this.context.navigate(currentHref, 'none'); } } @@ -120,9 +152,7 @@ class IonTabBarUnwrapped extends React.PureComponent { } render() { - const { activeTab } = this.state; - return ( {React.Children.map(this.props.children as any, this.renderTabButton(activeTab))} @@ -135,10 +165,11 @@ class IonTabBarUnwrapped extends React.PureComponent { } } -export const IonTabBar: React.FC = React.memo(props => { +const IonTabBarContainer: React.FC = React.memo(({ forwardedRef, ...props }) => { const context = useContext(NavContext); return ( @@ -146,3 +177,5 @@ export const IonTabBar: React.FC = React.memo(props => { ); }); + +export const IonTabBar = createForwardRef(IonTabBarContainer, 'IonTabBar'); diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index 8b155b0869..d42a6f519c 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -1,13 +1,16 @@ import { JSX as LocalJSX } from '@ionic/core'; -import React from 'react'; +import React, { Fragment } from 'react'; import { NavContext } from '../../contexts/NavContext'; import { IonRouterOutlet } from '../IonRouterOutlet'; import { IonTabBar } from './IonTabBar'; +import { IonTabsContext, IonTabsContextState } from './IonTabsContext'; + +type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode; interface Props extends LocalJSX.IonTabs { - children: React.ReactNode; + children: ChildFunction | React.ReactNode; } const hostStyles: React.CSSProperties = { @@ -32,25 +35,60 @@ const tabsInner: React.CSSProperties = { export class IonTabs extends React.Component { context!: React.ContextType; routerOutletRef: React.Ref = React.createRef(); + selectTabHandler?: (tag: string) => boolean; + tabBarRef = React.createRef(); + + ionTabContextState: IonTabsContextState = { + activeTab: undefined, + selectTab: () => false + }; constructor(props: Props) { super(props); } + componentDidMount() { + if (this.tabBarRef.current) { + // Grab initial value + this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab; + // Override method + this.tabBarRef.current.setActiveTabOnContext = (tab: string) => { + this.ionTabContextState.activeTab = tab; + }; + this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab; + } + } + render() { let outlet: React.ReactElement<{}> | undefined; let tabBar: React.ReactElement | undefined; - React.Children.forEach(this.props.children, (child: any) => { + const children = typeof this.props.children === 'function' ? + (this.props.children as ChildFunction)(this.ionTabContextState) : this.props.children; + + React.Children.forEach(children, (child: any) => { if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) { return; } if (child.type === IonRouterOutlet) { outlet = child; + } else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) { + outlet = child.props.children[0]; } if (child.type === IonTabBar) { const { onIonTabsDidChange, onIonTabsWillChange } = this.props; - tabBar = React.cloneElement(child, { onIonTabsDidChange, onIonTabsWillChange }); + tabBar = React.cloneElement(child, { + onIonTabsDidChange, + onIonTabsWillChange, + ref: this.tabBarRef + }); + } else if (child.type === Fragment && child.props.children[1].type === IonTabBar) { + const { onIonTabsDidChange, onIonTabsWillChange } = this.props; + tabBar = React.cloneElement(child.props.children[1], { + onIonTabsDidChange, + onIonTabsWillChange, + ref: this.tabBarRef + }); } }); @@ -63,13 +101,17 @@ export class IonTabs extends React.Component { } return ( -
- {tabBar.props.slot === 'top' ? tabBar : null} -
- {outlet} + +
+ {tabBar.props.slot === 'top' ? tabBar : null} +
+ {outlet} +
+ {tabBar.props.slot === 'bottom' ? tabBar : null}
- {tabBar.props.slot === 'bottom' ? tabBar : null} -
+ ); } diff --git a/packages/react/src/components/navigation/IonTabsContext.tsx b/packages/react/src/components/navigation/IonTabsContext.tsx new file mode 100644 index 0000000000..5c63926d8a --- /dev/null +++ b/packages/react/src/components/navigation/IonTabsContext.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export interface IonTabsContextState { + activeTab: string | undefined; + selectTab: (tab: string) => boolean; +} + +export const IonTabsContext = React.createContext({ + activeTab: undefined, + selectTab: () => false +}); diff --git a/packages/react/tslint.json b/packages/react/tslint.json index ba6516b917..b641daebd1 100644 --- a/packages/react/tslint.json +++ b/packages/react/tslint.json @@ -27,8 +27,10 @@ "jsx-no-lambda": false, "jsx-no-multiline-js": false, "jsx-wrap-multiline": false, + "no-empty": false, "no-empty-interface": false, "no-unbound-method": false, + "no-unused-expression": false, "prefer-conditional-expression": false } }