From 6b012004c9b1364e4aa9cc82362601643e5661d7 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 9 Oct 2024 10:44:59 -0700 Subject: [PATCH 1/5] fix(react): use as standalone in react --- .../src/components/navigation/IonTabBar.tsx | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx index f1a66440fcd..6f266e8267c 100644 --- a/packages/react/src/components/navigation/IonTabBar.tsx +++ b/packages/react/src/components/navigation/IonTabBar.tsx @@ -21,6 +21,17 @@ interface InternalProps extends IonTabBarProps { forwardedRef?: React.ForwardedRef; onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void; routeInfo: RouteInfo; + /** + * This prop is set by the `IonTabs` component. If + * the value is `undefined`, then the `ion-tab-bar` + * component was not found within the slotted content. + * Most likely, the tab bar was not passed as a direct + * child of `IonTabs`. + * + * A workaround will be used to determine if the tabs + * are being used as a basic tab navigation or with + * the router. + */ routerOutletRef?: React.RefObject | undefined; } @@ -40,10 +51,12 @@ interface IonTabBarState { class IonTabBarUnwrapped extends React.PureComponent { context!: React.ContextType; + private tabBarRef: React.RefObject; constructor(props: InternalProps) { super(props); const tabs: { [key: string]: TabUrls } = {}; + this.tabBarRef = React.createRef(); React.Children.forEach((props as any).children, (child: any) => { if ( child != null && @@ -183,12 +196,33 @@ class IonTabBarUnwrapped extends React.PureComponent + {React.Children.map(this.props.children as any, this.renderTabButton(activeTab))} ); From f323d6907123ec8dcb44a9188d5871efae70e4e5 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 9 Oct 2024 11:44:11 -0700 Subject: [PATCH 2/5] refactor(tabs, tab-bar): use context for router outlet --- .../src/components/navigation/IonTabBar.tsx | 30 +++++-------------- .../src/components/navigation/IonTabs.tsx | 3 ++ .../components/navigation/IonTabsContext.tsx | 2 ++ 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx index 6f266e8267c..84ac3c20d26 100644 --- a/packages/react/src/components/navigation/IonTabBar.tsx +++ b/packages/react/src/components/navigation/IonTabBar.tsx @@ -8,6 +8,7 @@ import { IonTabBarInner } from '../inner-proxies'; import { createForwardRef } from '../utils'; import { IonTabButton } from './IonTabButton'; +import { IonTabsContext } from './IonTabsContext'; type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & { @@ -32,7 +33,7 @@ interface InternalProps extends IonTabBarProps { * are being used as a basic tab navigation or with * the router. */ - routerOutletRef?: React.RefObject | undefined; + hasRouterOutlet: boolean; } interface TabUrls { @@ -196,33 +197,13 @@ class IonTabBarUnwrapped extends React.PureComponent = React.memo(({ forwardedRef, ...props }) => { const context = useContext(NavContext); + const tabsContext = useContext(IonTabsContext); + return ( {props.children} diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index e80e09ac159..79c4a2fc3f2 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -72,6 +72,7 @@ export const IonTabs = /*@__PURE__*/ (() => ionTabContextState: IonTabsContextState = { activeTab: undefined, selectTab: () => false, + hasRouterOutlet: false, }; constructor(props: Props) { @@ -123,6 +124,8 @@ export const IonTabs = /*@__PURE__*/ (() => hasTab = true; } + this.ionTabContextState.hasRouterOutlet = !!outlet; + let childProps: any = { ref: this.tabBarRef, routerOutletRef: this.routerOutletRef, diff --git a/packages/react/src/components/navigation/IonTabsContext.tsx b/packages/react/src/components/navigation/IonTabsContext.tsx index e7f9ba2b109..0b3c1a1261e 100644 --- a/packages/react/src/components/navigation/IonTabsContext.tsx +++ b/packages/react/src/components/navigation/IonTabsContext.tsx @@ -3,9 +3,11 @@ import React from 'react'; export interface IonTabsContextState { activeTab: string | undefined; selectTab: (tab: string) => boolean; + hasRouterOutlet: boolean; } export const IonTabsContext = React.createContext({ activeTab: undefined, selectTab: () => false, + hasRouterOutlet: false, }); From ef16a63d42f991b38cfed425669da201831660d5 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 9 Oct 2024 14:49:32 -0700 Subject: [PATCH 3/5] refactor(tabs, tab-bar): cleanup context --- .../src/components/navigation/IonTabBar.tsx | 41 ++++--- .../src/components/navigation/IonTabs.tsx | 113 ++++++------------ .../components/navigation/IonTabsContext.tsx | 14 +++ 3 files changed, 73 insertions(+), 95 deletions(-) diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx index 84ac3c20d26..92fde774ddd 100644 --- a/packages/react/src/components/navigation/IonTabBar.tsx +++ b/packages/react/src/components/navigation/IonTabBar.tsx @@ -9,6 +9,7 @@ import { createForwardRef } from '../utils'; import { IonTabButton } from './IonTabButton'; import { IonTabsContext } from './IonTabsContext'; +import type { IonTabsContextState } from './IonTabsContext'; type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & { @@ -22,18 +23,7 @@ interface InternalProps extends IonTabBarProps { forwardedRef?: React.ForwardedRef; onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void; routeInfo: RouteInfo; - /** - * This prop is set by the `IonTabs` component. If - * the value is `undefined`, then the `ion-tab-bar` - * component was not found within the slotted content. - * Most likely, the tab bar was not passed as a direct - * child of `IonTabs`. - * - * A workaround will be used to determine if the tabs - * are being used as a basic tab navigation or with - * the router. - */ - hasRouterOutlet: boolean; + tabsContext?: IonTabsContextState; } interface TabUrls { @@ -52,12 +42,10 @@ interface IonTabBarState { class IonTabBarUnwrapped extends React.PureComponent { context!: React.ContextType; - private tabBarRef: React.RefObject; constructor(props: InternalProps) { super(props); const tabs: { [key: string]: TabUrls } = {}; - this.tabBarRef = React.createRef(); React.Children.forEach((props as any).children, (child: any) => { if ( child != null && @@ -197,13 +185,14 @@ class IonTabBarUnwrapped extends React.PureComponent + {React.Children.map(this.props.children as any, this.renderTabButton(activeTab))} ); @@ -278,14 +267,28 @@ class IonTabBarUnwrapped extends React.PureComponent = React.memo(({ forwardedRef, ...props }) => { const context = useContext(NavContext); const tabsContext = useContext(IonTabsContext); + const tabBarRef = forwardedRef || tabsContext.tabBarProps.ref; + const updatedTabBarProps = { + ...tabsContext.tabBarProps, + ref: tabBarRef, + }; return ( {props.children} diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index 79c4a2fc3f2..d2eb34a9074 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -8,7 +8,6 @@ import { IonRouterOutlet } from '../IonRouterOutlet'; import { IonTabsInner } from '../inner-proxies'; import { IonTab } from '../proxies'; -import { IonTabBar } from './IonTabBar'; import type { IonTabsContextState } from './IonTabsContext'; import { IonTabsContext } from './IonTabsContext'; @@ -43,29 +42,9 @@ interface Props extends LocalJSX.IonTabs { 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__*/ (() => class extends React.Component { context!: React.ContextType; - routerOutletRef: React.Ref = React.createRef(); selectTabHandler?: (tag: string) => boolean; tabBarRef = React.createRef(); @@ -73,6 +52,13 @@ export const IonTabs = /*@__PURE__*/ (() => activeTab: undefined, 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) { @@ -91,9 +77,32 @@ export const IonTabs = /*@__PURE__*/ (() => } } + renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) { + return ( + + {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; + })} + + ); + } + 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; @@ -103,19 +112,15 @@ 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, outletProps); + outlet = React.cloneElement(child); } 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) { /** * This indicates that IonTabs will be using a basic tab-based navigation @@ -127,8 +132,7 @@ export const IonTabs = /*@__PURE__*/ (() => this.ionTabContextState.hasRouterOutlet = !!outlet; let childProps: any = { - ref: this.tabBarRef, - routerOutletRef: this.routerOutletRef, + ...this.ionTabContextState.tabBarProps, }; /** @@ -152,14 +156,7 @@ export const IonTabs = /*@__PURE__*/ (() => }; } - 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); - } + this.ionTabContextState.tabBarProps = childProps; }); if (!outlet && !hasTab) { @@ -189,46 +186,10 @@ export const IonTabs = /*@__PURE__*/ (() => {this.context.hasIonicRouter() ? ( - - {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; - })} - + {this.renderTabsInner(children, outlet)} ) : ( -
- {tabBar?.props.slot === 'top' ? tabBar : null} -
- {outlet} -
- {tabBar?.props.slot === 'bottom' ? tabBar : null} -
+ this.renderTabsInner(children, outlet) )}
); diff --git a/packages/react/src/components/navigation/IonTabsContext.tsx b/packages/react/src/components/navigation/IonTabsContext.tsx index 0b3c1a1261e..12686bd2ad6 100644 --- a/packages/react/src/components/navigation/IonTabsContext.tsx +++ b/packages/react/src/components/navigation/IonTabsContext.tsx @@ -4,10 +4,24 @@ export interface IonTabsContextState { activeTab: string | undefined; 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; + onIonTabsWillChange?: (e: CustomEvent) => void; + onIonTabsDidChange?: (e: CustomEvent) => void; +}; + export const IonTabsContext = React.createContext({ activeTab: undefined, selectTab: () => false, hasRouterOutlet: false, + tabBarProps: { ref: React.createRef() }, }); From ddf07ff992b8b0baf388dc0f094fd97d3034515e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Oct 2024 08:19:58 -0700 Subject: [PATCH 4/5] refactor(tabs): add routerOutletRef back --- packages/react/src/components/navigation/IonTabs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index d2eb34a9074..c76b4f38a80 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -45,6 +45,7 @@ interface Props extends LocalJSX.IonTabs { export const IonTabs = /*@__PURE__*/ (() => class extends React.Component { context!: React.ContextType; + routerOutletRef: React.Ref = React.createRef(); selectTabHandler?: (tag: string) => boolean; tabBarRef = React.createRef(); From 7739cf72423bc0192b01454cc0a2ab53f52379d1 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 10 Oct 2024 10:31:37 -0700 Subject: [PATCH 5/5] docs(tabs): add comment --- packages/react/src/components/navigation/IonTabs.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index c76b4f38a80..a7a8a250bf2 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -45,6 +45,12 @@ interface Props extends LocalJSX.IonTabs { export const IonTabs = /*@__PURE__*/ (() => class extends React.Component { context!: React.ContextType; + /** + * `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 = React.createRef(); selectTabHandler?: (tag: string) => boolean; tabBarRef = React.createRef();