From a2e803a553dc58fc0e1599e515a56180a7ab263a Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 4 Aug 2025 15:08:04 -0700 Subject: [PATCH] fix(tabs): add fallback to select tab if router integration fails (#30599) Issue number: resolves #30552 --------- ## What is the current behavior? Something caused a timing shift in v8.6.0 that messed up the timing required for react router to set the active tab ID. Currently, when the router goes to set the tab ID, it's possibly too early and the tab may not exist yet, causing it to go unset. ## What is the new behavior? This PR is a workaround that allows tabs to check when they're rendered if a tab should be selected as a fallback for the router not setting them. I don't think the tabs, in the long run, should be responsible for this, but I think this is a good intermediate step until the upcoming react router upgrade, when we can look into a better solution for react router that may require less timing precision. This PR also adds regression tests for React to make sure this doesn't happen again without getting noticed. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information **Current dev build:** ``` 8.7.2-dev.11754338216.1a548096 ``` --- core/src/components/tabs/tabs.tsx | 22 ++++- packages/react/test/base/src/App.tsx | 2 + .../base/src/pages/TabsDirectNavigation.tsx | 94 +++++++++++++++++++ .../components/tabs-direct-navigation.cy.ts | 35 +++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 packages/react/test/base/src/pages/TabsDirectNavigation.tsx create mode 100644 packages/react/test/base/tests/e2e/specs/components/tabs-direct-navigation.cy.ts diff --git a/core/src/components/tabs/tabs.tsx b/core/src/components/tabs/tabs.tsx index e02e1aa91b..ada833446e 100644 --- a/core/src/components/tabs/tabs.tsx +++ b/core/src/components/tabs/tabs.tsx @@ -68,7 +68,27 @@ export class Tabs implements NavOutlet { componentWillRender() { const tabBar = this.el.querySelector('ion-tab-bar'); if (tabBar) { - const tab = this.selectedTab ? this.selectedTab.tab : undefined; + let tab = this.selectedTab ? this.selectedTab.tab : undefined; + + // Fallback: if no selectedTab is set but we're using router mode, + // determine the active tab from the current URL. This works around + // timing issues in React Router integration where setRouteId may not + // be called in time for the initial render. + // TODO(FW-6724): Remove this with React Router upgrade + if (!tab && this.useRouter && typeof window !== 'undefined') { + const currentPath = window.location.pathname; + const tabButtons = this.el.querySelectorAll('ion-tab-button'); + + // Look for a tab button that matches the current path pattern + for (const tabButton of tabButtons) { + const tabId = tabButton.getAttribute('tab'); + if (tabId && currentPath.includes(tabId)) { + tab = tabId; + break; + } + } + } + tabBar.selectedTab = tab; } } diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx index 39ebb5f74f..291acf7524 100644 --- a/packages/react/test/base/src/App.tsx +++ b/packages/react/test/base/src/App.tsx @@ -27,6 +27,7 @@ import Main from './pages/Main'; import Tabs from './pages/Tabs'; import TabsBasic from './pages/TabsBasic'; import NavComponent from './pages/navigation/NavComponent'; +import TabsDirectNavigation from './pages/TabsDirectNavigation'; import IonModalConditional from './pages/overlay-components/IonModalConditional'; import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling'; import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton'; @@ -63,6 +64,7 @@ const App: React.FC = () => ( + diff --git a/packages/react/test/base/src/pages/TabsDirectNavigation.tsx b/packages/react/test/base/src/pages/TabsDirectNavigation.tsx new file mode 100644 index 0000000000..0e8df5db1f --- /dev/null +++ b/packages/react/test/base/src/pages/TabsDirectNavigation.tsx @@ -0,0 +1,94 @@ +import { IonContent, IonHeader, IonIcon, IonLabel, IonPage, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, IonTitle, IonToolbar } from '@ionic/react'; +import { homeOutline, radioOutline, libraryOutline, searchOutline } from 'ionicons/icons'; +import React from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +const HomePage: React.FC = () => ( + + + + Home + + + +
Home Content
+
+
+); + +const RadioPage: React.FC = () => ( + + + + Radio + + + +
Radio Content
+
+
+); + +const LibraryPage: React.FC = () => ( + + + + Library + + + +
Library Content
+
+
+); + +const SearchPage: React.FC = () => ( + + + + Search + + + +
Search Content
+
+
+); + +const TabsDirectNavigation: React.FC = () => { + return ( + + + + } exact={true} /> + } exact={true} /> + } exact={true} /> + } exact={true} /> + + + + + + Home + + + + + Radio + + + + + Library + + + + + Search + + + + ); +}; + +export default TabsDirectNavigation; \ No newline at end of file diff --git a/packages/react/test/base/tests/e2e/specs/components/tabs-direct-navigation.cy.ts b/packages/react/test/base/tests/e2e/specs/components/tabs-direct-navigation.cy.ts new file mode 100644 index 0000000000..b1bc76cb5b --- /dev/null +++ b/packages/react/test/base/tests/e2e/specs/components/tabs-direct-navigation.cy.ts @@ -0,0 +1,35 @@ +describe('Tabs Direct Navigation', () => { + it('should select the correct tab when navigating directly to home route', () => { + cy.visit('/tabs-direct-navigation/home'); + cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected'); + cy.get('[data-testid="home-content"]').should('be.visible'); + }); + + it('should select the correct tab when navigating directly to radio route', () => { + cy.visit('/tabs-direct-navigation/radio'); + cy.get('[data-testid="radio-tab"]').should('have.class', 'tab-selected'); + cy.get('[data-testid="radio-content"]').should('be.visible'); + }); + + it('should select the correct tab when navigating directly to library route', () => { + cy.visit('/tabs-direct-navigation/library'); + cy.get('[data-testid="library-tab"]').should('have.class', 'tab-selected'); + cy.get('[data-testid="library-content"]').should('be.visible'); + }); + + it('should select the correct tab when navigating directly to search route', () => { + cy.visit('/tabs-direct-navigation/search'); + cy.get('[data-testid="search-tab"]').should('have.class', 'tab-selected'); + cy.get('[data-testid="search-content"]').should('be.visible'); + }); + + it('should update tab selection when navigating between tabs', () => { + cy.visit('/tabs-direct-navigation/home'); + cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected'); + + cy.get('[data-testid="radio-tab"]').click(); + cy.get('[data-testid="radio-tab"]').should('have.class', 'tab-selected'); + cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected'); + cy.get('[data-testid="radio-content"]').should('be.visible'); + }); +}); \ No newline at end of file