@@ -114,6 +114,18 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
114114 // for future navigations once React has committed the mount. This avoids
115115 // duplicate entries when React StrictMode runs an extra render pre-commit.
116116 locationHistory . current . add ( routeInfo ) ;
117+
118+ // If IonTabBar already called handleSetCurrentTab during render (before this
119+ // effect), the tab was stored in currentTab.current but the history entry was
120+ // not yet seeded. Apply the pending tab to the seed entry now.
121+ if ( currentTab . current ) {
122+ const ri = { ...locationHistory . current . current ( ) } ;
123+ if ( ri . tab !== currentTab . current ) {
124+ ri . tab = currentTab . current ;
125+ locationHistory . current . update ( ri ) ;
126+ }
127+ }
128+
117129 registerHistoryListener ( handleHistoryChange ) ;
118130
119131 didMountRef . current = true ;
@@ -179,14 +191,17 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
179191 const leavingUrl = leavingLocationInfo . pathname + leavingLocationInfo . search ;
180192 if ( leavingUrl !== location . pathname + location . search ) {
181193 if ( ! incomingRouteParams . current ) {
182- // Determine if the destination is a tab route by checking if it matches
183- // the pattern of tab routes (containing /tabs/ in the path)
184- const isTabRoute = / \/ t a b s ( \/ | $ ) / . test ( location . pathname ) ;
185- const tabToUse = isTabRoute ? currentTab . current : undefined ;
186-
187- // If we're leaving tabs entirely, clear the current tab
188- if ( ! isTabRoute && currentTab . current ) {
189- currentTab . current = undefined ;
194+ // Use history-based tab detection instead of URL-pattern heuristics,
195+ // so tab routes work with any URL structure (not just paths containing "/tabs").
196+ // Fall back to currentTab.current only when the destination is within the
197+ // current tab's path hierarchy (prevents non-tab routes from inheriting a tab).
198+ let tabToUse = locationHistory . current . findTabForPathname ( location . pathname ) ;
199+ if ( ! tabToUse && currentTab . current ) {
200+ const tabFirstRoute = locationHistory . current . getFirstRouteInfoForTab ( currentTab . current ) ;
201+ const tabRootPath = tabFirstRoute ?. pathname ;
202+ if ( tabRootPath && ( location . pathname === tabRootPath || location . pathname . startsWith ( tabRootPath + '/' ) ) ) {
203+ tabToUse = currentTab . current ;
204+ }
190205 }
191206
192207 /**
@@ -256,7 +271,7 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
256271 let routeInfo : RouteInfo ;
257272
258273 // If we're navigating away from tabs to a non-tab route, clear the current tab
259- if ( ! / \/ t a b s ( \/ | $ ) / . test ( location . pathname ) && currentTab . current ) {
274+ if ( ! locationHistory . current . findTabForPathname ( location . pathname ) && currentTab . current ) {
260275 currentTab . current = undefined ;
261276 }
262277
@@ -441,7 +456,13 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
441456 */
442457 const handleSetCurrentTab = ( tab : string ) => {
443458 currentTab . current = tab ;
444- const ri = { ...locationHistory . current . current ( ) } ;
459+ const current = locationHistory . current . current ( ) ;
460+ if ( ! current ) {
461+ // locationHistory not yet seeded (e.g., called during initial render
462+ // before mount effect). The mount effect will seed the correct entry.
463+ return ;
464+ }
465+ const ri = { ...current } ;
445466 if ( ri . tab !== tab ) {
446467 ri . tab = tab ;
447468 locationHistory . current . update ( ri ) ;
@@ -553,26 +574,27 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
553574 let navigationTab = tab ;
554575
555576 // If no explicit tab is provided and we're in a tab context,
556- // check if the destination path is outside of the current tab context
577+ // check if the destination path is outside of the current tab context.
578+ // Uses history-based tab detection instead of URL pattern matching,
579+ // so it works with any tab URL structure.
557580 if ( ! tab && currentTab . current && path ) {
558- // Get the current route info to understand where we are
559- const currentRoute = locationHistory . current . current ( ) ;
560-
561- // If we're navigating from a tab route to a completely different path structure,
562- // we should clear the tab context. This is a simplified check that assumes
563- // tab routes share a common parent path.
564- if ( currentRoute && currentRoute . pathname ) {
565- // Extract the base tab path (e.g., /routing/tabs from /routing/tabs/home)
566- const tabBaseMatch = currentRoute . pathname . match ( / ^ ( .* \/ t a b s ) / ) ;
567- if ( tabBaseMatch ) {
568- const tabBasePath = tabBaseMatch [ 1 ] ;
569- // If the new path doesn't start with the tab base path, we're leaving tabs
570- if ( ! path . startsWith ( tabBasePath ) ) {
581+ // Check if destination was previously visited in a tab context
582+ const destinationTab = locationHistory . current . findTabForPathname ( path ) ;
583+ if ( destinationTab ) {
584+ // Previously visited as a tab route - use the known tab
585+ navigationTab = destinationTab ;
586+ } else {
587+ // New destination - check if it's a child of the current tab's root path
588+ const tabFirstRoute = locationHistory . current . getFirstRouteInfoForTab ( currentTab . current ) ;
589+ if ( tabFirstRoute ) {
590+ const tabRootPath = tabFirstRoute . pathname ;
591+ if ( path === tabRootPath || path . startsWith ( tabRootPath + '/' ) ) {
592+ // Still within the current tab's path hierarchy
593+ navigationTab = currentTab . current ;
594+ } else {
595+ // Destination is outside the current tab context
571596 currentTab . current = undefined ;
572597 navigationTab = undefined ;
573- } else {
574- // Still within tabs, preserve the tab context
575- navigationTab = currentTab . current ;
576598 }
577599 }
578600 }
0 commit comments