mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 23:58:13 +08:00
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 ```
This commit is contained in:
@ -68,7 +68,27 @@ export class Tabs implements NavOutlet {
|
|||||||
componentWillRender() {
|
componentWillRender() {
|
||||||
const tabBar = this.el.querySelector('ion-tab-bar');
|
const tabBar = this.el.querySelector('ion-tab-bar');
|
||||||
if (tabBar) {
|
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;
|
tabBar.selectedTab = tab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import Main from './pages/Main';
|
|||||||
import Tabs from './pages/Tabs';
|
import Tabs from './pages/Tabs';
|
||||||
import TabsBasic from './pages/TabsBasic';
|
import TabsBasic from './pages/TabsBasic';
|
||||||
import NavComponent from './pages/navigation/NavComponent';
|
import NavComponent from './pages/navigation/NavComponent';
|
||||||
|
import TabsDirectNavigation from './pages/TabsDirectNavigation';
|
||||||
import IonModalConditional from './pages/overlay-components/IonModalConditional';
|
import IonModalConditional from './pages/overlay-components/IonModalConditional';
|
||||||
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
|
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
|
||||||
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
|
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
|
||||||
@ -63,6 +64,7 @@ const App: React.FC = () => (
|
|||||||
<Route path="/navigation" component={NavComponent} />
|
<Route path="/navigation" component={NavComponent} />
|
||||||
<Route path="/tabs" component={Tabs} />
|
<Route path="/tabs" component={Tabs} />
|
||||||
<Route path="/tabs-basic" component={TabsBasic} />
|
<Route path="/tabs-basic" component={TabsBasic} />
|
||||||
|
<Route path="/tabs-direct-navigation" component={TabsDirectNavigation} />
|
||||||
<Route path="/icons" component={Icons} />
|
<Route path="/icons" component={Icons} />
|
||||||
<Route path="/inputs" component={Inputs} />
|
<Route path="/inputs" component={Inputs} />
|
||||||
</IonRouterOutlet>
|
</IonRouterOutlet>
|
||||||
|
|||||||
94
packages/react/test/base/src/pages/TabsDirectNavigation.tsx
Normal file
94
packages/react/test/base/src/pages/TabsDirectNavigation.tsx
Normal file
@ -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 = () => (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Home</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<div data-testid="home-content">Home Content</div>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RadioPage: React.FC = () => (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Radio</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<div data-testid="radio-content">Radio Content</div>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LibraryPage: React.FC = () => (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Library</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<div data-testid="library-content">Library Content</div>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SearchPage: React.FC = () => (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Search</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<div data-testid="search-content">Search Content</div>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TabsDirectNavigation: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<IonTabs data-testid="tabs-direct-navigation">
|
||||||
|
<IonRouterOutlet>
|
||||||
|
<Redirect exact path="/tabs-direct-navigation" to="/tabs-direct-navigation/home" />
|
||||||
|
<Route path="/tabs-direct-navigation/home" render={() => <HomePage />} exact={true} />
|
||||||
|
<Route path="/tabs-direct-navigation/radio" render={() => <RadioPage />} exact={true} />
|
||||||
|
<Route path="/tabs-direct-navigation/library" render={() => <LibraryPage />} exact={true} />
|
||||||
|
<Route path="/tabs-direct-navigation/search" render={() => <SearchPage />} exact={true} />
|
||||||
|
</IonRouterOutlet>
|
||||||
|
|
||||||
|
<IonTabBar slot="bottom" data-testid="tab-bar">
|
||||||
|
<IonTabButton tab="home" href="/tabs-direct-navigation/home" data-testid="home-tab">
|
||||||
|
<IonIcon icon={homeOutline}></IonIcon>
|
||||||
|
<IonLabel>Home</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
|
||||||
|
<IonTabButton tab="radio" href="/tabs-direct-navigation/radio" data-testid="radio-tab">
|
||||||
|
<IonIcon icon={radioOutline}></IonIcon>
|
||||||
|
<IonLabel>Radio</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
|
||||||
|
<IonTabButton tab="library" href="/tabs-direct-navigation/library" data-testid="library-tab">
|
||||||
|
<IonIcon icon={libraryOutline}></IonIcon>
|
||||||
|
<IonLabel>Library</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
|
||||||
|
<IonTabButton tab="search" href="/tabs-direct-navigation/search" data-testid="search-tab">
|
||||||
|
<IonIcon icon={searchOutline}></IonIcon>
|
||||||
|
<IonLabel>Search</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
</IonTabBar>
|
||||||
|
</IonTabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabsDirectNavigation;
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user