Skip to content

Commit a2e803a

Browse files
authored
fix(tabs): add fallback to select tab if router integration fails (#30599)
Issue number: resolves #30552 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> 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? <!-- Please describe the behavior or changes that are being added by this PR. --> 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 <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> **Current dev build:** ``` 8.7.2-dev.11754338216.1a548096 ```
1 parent 56265e3 commit a2e803a

File tree

4 files changed

+152
-1
lines changed

4 files changed

+152
-1
lines changed

core/src/components/tabs/tabs.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,27 @@ export class Tabs implements NavOutlet {
6868
componentWillRender() {
6969
const tabBar = this.el.querySelector('ion-tab-bar');
7070
if (tabBar) {
71-
const tab = this.selectedTab ? this.selectedTab.tab : undefined;
71+
let tab = this.selectedTab ? this.selectedTab.tab : undefined;
72+
73+
// Fallback: if no selectedTab is set but we're using router mode,
74+
// determine the active tab from the current URL. This works around
75+
// timing issues in React Router integration where setRouteId may not
76+
// be called in time for the initial render.
77+
// TODO(FW-6724): Remove this with React Router upgrade
78+
if (!tab && this.useRouter && typeof window !== 'undefined') {
79+
const currentPath = window.location.pathname;
80+
const tabButtons = this.el.querySelectorAll('ion-tab-button');
81+
82+
// Look for a tab button that matches the current path pattern
83+
for (const tabButton of tabButtons) {
84+
const tabId = tabButton.getAttribute('tab');
85+
if (tabId && currentPath.includes(tabId)) {
86+
tab = tabId;
87+
break;
88+
}
89+
}
90+
}
91+
7292
tabBar.selectedTab = tab;
7393
}
7494
}

packages/react/test/base/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Main from './pages/Main';
2727
import Tabs from './pages/Tabs';
2828
import TabsBasic from './pages/TabsBasic';
2929
import NavComponent from './pages/navigation/NavComponent';
30+
import TabsDirectNavigation from './pages/TabsDirectNavigation';
3031
import IonModalConditional from './pages/overlay-components/IonModalConditional';
3132
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
3233
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
@@ -63,6 +64,7 @@ const App: React.FC = () => (
6364
<Route path="/navigation" component={NavComponent} />
6465
<Route path="/tabs" component={Tabs} />
6566
<Route path="/tabs-basic" component={TabsBasic} />
67+
<Route path="/tabs-direct-navigation" component={TabsDirectNavigation} />
6668
<Route path="/icons" component={Icons} />
6769
<Route path="/inputs" component={Inputs} />
6870
</IonRouterOutlet>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { IonContent, IonHeader, IonIcon, IonLabel, IonPage, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, IonTitle, IonToolbar } from '@ionic/react';
2+
import { homeOutline, radioOutline, libraryOutline, searchOutline } from 'ionicons/icons';
3+
import React from 'react';
4+
import { Route, Redirect } from 'react-router-dom';
5+
6+
const HomePage: React.FC = () => (
7+
<IonPage>
8+
<IonHeader>
9+
<IonToolbar>
10+
<IonTitle>Home</IonTitle>
11+
</IonToolbar>
12+
</IonHeader>
13+
<IonContent>
14+
<div data-testid="home-content">Home Content</div>
15+
</IonContent>
16+
</IonPage>
17+
);
18+
19+
const RadioPage: React.FC = () => (
20+
<IonPage>
21+
<IonHeader>
22+
<IonToolbar>
23+
<IonTitle>Radio</IonTitle>
24+
</IonToolbar>
25+
</IonHeader>
26+
<IonContent>
27+
<div data-testid="radio-content">Radio Content</div>
28+
</IonContent>
29+
</IonPage>
30+
);
31+
32+
const LibraryPage: React.FC = () => (
33+
<IonPage>
34+
<IonHeader>
35+
<IonToolbar>
36+
<IonTitle>Library</IonTitle>
37+
</IonToolbar>
38+
</IonHeader>
39+
<IonContent>
40+
<div data-testid="library-content">Library Content</div>
41+
</IonContent>
42+
</IonPage>
43+
);
44+
45+
const SearchPage: React.FC = () => (
46+
<IonPage>
47+
<IonHeader>
48+
<IonToolbar>
49+
<IonTitle>Search</IonTitle>
50+
</IonToolbar>
51+
</IonHeader>
52+
<IonContent>
53+
<div data-testid="search-content">Search Content</div>
54+
</IonContent>
55+
</IonPage>
56+
);
57+
58+
const TabsDirectNavigation: React.FC = () => {
59+
return (
60+
<IonTabs data-testid="tabs-direct-navigation">
61+
<IonRouterOutlet>
62+
<Redirect exact path="/tabs-direct-navigation" to="/tabs-direct-navigation/home" />
63+
<Route path="/tabs-direct-navigation/home" render={() => <HomePage />} exact={true} />
64+
<Route path="/tabs-direct-navigation/radio" render={() => <RadioPage />} exact={true} />
65+
<Route path="/tabs-direct-navigation/library" render={() => <LibraryPage />} exact={true} />
66+
<Route path="/tabs-direct-navigation/search" render={() => <SearchPage />} exact={true} />
67+
</IonRouterOutlet>
68+
69+
<IonTabBar slot="bottom" data-testid="tab-bar">
70+
<IonTabButton tab="home" href="/tabs-direct-navigation/home" data-testid="home-tab">
71+
<IonIcon icon={homeOutline}></IonIcon>
72+
<IonLabel>Home</IonLabel>
73+
</IonTabButton>
74+
75+
<IonTabButton tab="radio" href="/tabs-direct-navigation/radio" data-testid="radio-tab">
76+
<IonIcon icon={radioOutline}></IonIcon>
77+
<IonLabel>Radio</IonLabel>
78+
</IonTabButton>
79+
80+
<IonTabButton tab="library" href="/tabs-direct-navigation/library" data-testid="library-tab">
81+
<IonIcon icon={libraryOutline}></IonIcon>
82+
<IonLabel>Library</IonLabel>
83+
</IonTabButton>
84+
85+
<IonTabButton tab="search" href="/tabs-direct-navigation/search" data-testid="search-tab">
86+
<IonIcon icon={searchOutline}></IonIcon>
87+
<IonLabel>Search</IonLabel>
88+
</IonTabButton>
89+
</IonTabBar>
90+
</IonTabs>
91+
);
92+
};
93+
94+
export default TabsDirectNavigation;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
describe('Tabs Direct Navigation', () => {
2+
it('should select the correct tab when navigating directly to home route', () => {
3+
cy.visit('/tabs-direct-navigation/home');
4+
cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected');
5+
cy.get('[data-testid="home-content"]').should('be.visible');
6+
});
7+
8+
it('should select the correct tab when navigating directly to radio route', () => {
9+
cy.visit('/tabs-direct-navigation/radio');
10+
cy.get('[data-testid="radio-tab"]').should('have.class', 'tab-selected');
11+
cy.get('[data-testid="radio-content"]').should('be.visible');
12+
});
13+
14+
it('should select the correct tab when navigating directly to library route', () => {
15+
cy.visit('/tabs-direct-navigation/library');
16+
cy.get('[data-testid="library-tab"]').should('have.class', 'tab-selected');
17+
cy.get('[data-testid="library-content"]').should('be.visible');
18+
});
19+
20+
it('should select the correct tab when navigating directly to search route', () => {
21+
cy.visit('/tabs-direct-navigation/search');
22+
cy.get('[data-testid="search-tab"]').should('have.class', 'tab-selected');
23+
cy.get('[data-testid="search-content"]').should('be.visible');
24+
});
25+
26+
it('should update tab selection when navigating between tabs', () => {
27+
cy.visit('/tabs-direct-navigation/home');
28+
cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected');
29+
30+
cy.get('[data-testid="radio-tab"]').click();
31+
cy.get('[data-testid="radio-tab"]').should('have.class', 'tab-selected');
32+
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
33+
cy.get('[data-testid="radio-content"]').should('be.visible');
34+
});
35+
});

0 commit comments

Comments
 (0)