From 0f00d63648cf8f2fcf611571f3c53659dbbd5cb5 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 30 Jun 2023 16:12:51 -0400 Subject: [PATCH] chore: migrate IonTabs to be a functional component --- .../src/components/navigation/IonTabs.tsx | 201 ++++++++---------- .../components/navigation/IonTabsContext.tsx | 4 +- 2 files changed, 92 insertions(+), 113 deletions(-) diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index c4d2df2c35..22b97d3f93 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -1,5 +1,5 @@ import type { JSX as LocalJSX } from '@ionic/core/components'; -import React, { Fragment } from 'react'; +import React, { Fragment, cloneElement, useContext, useEffect, useRef } from 'react'; import { NavContext } from '../../contexts/NavContext'; import PageManager from '../../routing/PageManager'; @@ -60,123 +60,102 @@ const tabsInner: React.CSSProperties = { contain: 'layout size style', }; -export const IonTabs = /*@__PURE__*/ (() => - class extends React.Component { - context!: React.ContextType; - routerOutletRef: React.Ref = React.createRef(); - selectTabHandler?: (tag: string) => boolean; - tabBarRef = React.createRef(); +export const IonTabs = (props: Props) => { + const { hasIonicRouter, routeInfo } = useContext(NavContext); + const tabBarRef = useRef(null); // TODO @sean find the type for this - ionTabContextState: IonTabsContextState = { - activeTab: undefined, - selectTab: () => false, + const ionTabContextState: IonTabsContextState = { + activeTab: undefined, + selectTab: () => false, + }; + + useEffect(() => { + if (tabBarRef.current) { + ionTabContextState.activeTab = tabBarRef.current.state.activeTab; + // Overrides the web component implementation of this method + tabBarRef.current.setActiveTabOnContext = (tab: string) => { + ionTabContextState.activeTab = tab; + }; + ionTabContextState.selectTab = tabBarRef.current.selectTab; + } + }, []); + + let outlet: React.ReactElement | undefined; + let tabBar: React.ReactElement | undefined; + const { className, onIonTabsDidChange, onIonTabsWillChange, ...restProps } = props; + + const children = + typeof props.children === 'function' ? (props.children as ChildFunction)(ionTabContextState) : props.children; + + 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 = cloneElement(child); + } else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) { + outlet = child.props.children[0]; // TODO @sean why do we cloneElement everywhere but here? + } + + let childProps: any = { + ref: tabBarRef, }; - constructor(props: Props) { - super(props); + /** + * Only pass these props down from IonTabs to + * IonTabsBar if they are defined. Otherwise if you have + * a handler set on IonTabBar it will be overridden. + */ + if (onIonTabsDidChange !== undefined) { + childProps = { + ...childProps, + onIonTabsDidChange, + }; } - 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; - } + if (onIonTabsWillChange !== undefined) { + childProps = { + ...childProps, + onIonTabsWillChange, + }; } - render() { - let outlet: React.ReactElement<{}> | undefined; - let tabBar: React.ReactElement | undefined; - const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props; + if (child.type === IonTabBar || child.type.isTabBar) { + tabBar = cloneElement(child, childProps); + } else if (child.type === Fragment && child.props.children[1].type === IonTabBar) { + tabBar = cloneElement(child.props.children[1], childProps); + } + }); - const children = - typeof this.props.children === 'function' - ? (this.props.children as ChildFunction)(this.ionTabContextState) - : this.props.children; + if (!outlet) { + throw new Error('[Ionic Error]: IonTabs must contain an '); + } - 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); - } else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) { - outlet = child.props.children[0]; - } - - let childProps: any = { - ref: this.tabBarRef, - }; - - /** - * Only pass these props - * down from IonTabs to IonTabBar - * if they are defined, otherwise - * if you have a handler set on - * IonTabBar it will be overridden. - */ - if (onIonTabsDidChange !== undefined) { - childProps = { - ...childProps, - onIonTabsDidChange, - }; - } - - if (onIonTabsWillChange !== undefined) { - childProps = { - ...childProps, - onIonTabsWillChange, - }; - } - - if (child.type === IonTabBar || child.type.isTabBar) { - 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) { - throw new Error('IonTabs must contain an IonRouterOutlet'); - } - if (!tabBar) { - throw new Error('IonTabs needs a IonTabBar'); - } - - return ( - - {this.context.hasIonicRouter() ? ( - - - {tabBar.props.slot === 'top' ? tabBar : null} -
- {outlet} -
- {tabBar.props.slot === 'bottom' ? tabBar : null} -
-
- ) : ( -
- {tabBar.props.slot === 'top' ? tabBar : null} -
- {outlet} -
- {tabBar.props.slot === 'bottom' ? tabBar : null} + if (!tabBar) { + throw new Error('[Ionic Error]: IonTabs must contain an '); + } + return ( + + {hasIonicRouter() ? ( + + + {tabBar.props.slot === 'top' ? tabBar : null} +
+ {outlet}
- )} -
- ); - } - - static get contextType() { - return NavContext; - } - })(); + {tabBar.props.slot === 'bottom' ? tabBar : null} + + + ) : ( +
+ {tabBar.props.slot === 'top' ? tabBar : null} +
+ {outlet} +
+ {tabBar.props.slot === 'bottom' ? tabBar : null} +
+ )} + + ); +}; diff --git a/packages/react/src/components/navigation/IonTabsContext.tsx b/packages/react/src/components/navigation/IonTabsContext.tsx index e7f9ba2b10..e83491e883 100644 --- a/packages/react/src/components/navigation/IonTabsContext.tsx +++ b/packages/react/src/components/navigation/IonTabsContext.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import { createContext } from 'react'; export interface IonTabsContextState { activeTab: string | undefined; selectTab: (tab: string) => boolean; } -export const IonTabsContext = React.createContext({ +export const IonTabsContext = createContext({ activeTab: undefined, selectTab: () => false, });