From d00fe99eb7451895af185c9624061ebe2dc48cd8 Mon Sep 17 00:00:00 2001 From: Benziza Date: Wed, 26 Aug 2026 23:41:23 +0200 Subject: [PATCH] fix(tabs): handle outlet in fragment --- .../src/components/navigation/IonTabs.tsx | 46 ++++++++++++------- .../navigation/__tests__/IonTabs.spec.tsx | 33 +++++++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 packages/react/src/components/navigation/__tests__/IonTabs.spec.tsx diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index 8bb962ff515..da837abd78b 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -40,6 +40,26 @@ declare module 'react' { type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode; +const getRouterOutlet = (child: React.ReactNode): React.ReactElement | undefined => { + if (!React.isValidElement(child)) { + return undefined; + } + + const candidate = + child.type === Fragment + ? React.Children.toArray((child.props as { children?: React.ReactNode }).children)[0] + : child; + + if ( + React.isValidElement(candidate) && + (candidate.type === IonRouterOutlet || (candidate.type as any).isRouterOutlet) + ) { + return candidate; + } + + return undefined; +}; + interface Props extends LocalJSX.IonTabs { className?: string; children: React.ReactNode; @@ -96,19 +116,12 @@ export class IonTabs extends React.Component { 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; - } + if (getRouterOutlet(child)) { + /** + * The modified outlet needs to be returned to include + * the ref. + */ + return outlet; } return child; })} @@ -132,10 +145,9 @@ export class IonTabs extends React.Component { 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 = React.cloneElement(child.props.children[0]); + const routerOutlet = getRouterOutlet(child); + if (routerOutlet) { + outlet = React.cloneElement(routerOutlet); } else if (child.type === IonTab) { /** * This indicates that IonTabs will be using a basic tab-based navigation diff --git a/packages/react/src/components/navigation/__tests__/IonTabs.spec.tsx b/packages/react/src/components/navigation/__tests__/IonTabs.spec.tsx new file mode 100644 index 00000000000..b65facad128 --- /dev/null +++ b/packages/react/src/components/navigation/__tests__/IonTabs.spec.tsx @@ -0,0 +1,33 @@ +jest.mock('../../IonRouterOutlet', () => ({ + IonRouterOutlet: () => null, +})); +jest.mock('../../components', () => ({ + IonTab: () => null, +})); +jest.mock('../../inner-proxies', () => ({ + IonTabsInner: () => null, +})); +jest.mock('../../../routing/PageManager', () => ({ + __esModule: true, + default: () => null, +})); + +import { render } from '@testing-library/react'; +import { Fragment } from 'react'; + +import { IonRouterOutlet } from '../../IonRouterOutlet'; +import { IonTabs } from '../IonTabs'; + +describe('IonTabs', () => { + it('handles a single router outlet inside a fragment', () => { + expect(() => + render( + + + + + + ) + ).not.toThrow(); + }); +});