fix(tabs): select correct tab when routes have similar prefixes (#30863)

Issue number: resolves #30448 

---------

<!-- 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?

When using ion-tabs with routes that share a common prefix (e.g.,
`/home`, `/home2`, `/home3`), navigating to `/home2` incorrectly
highlights the `/home` tab. This occurs because the tab matching logic
uses `pathname.startsWith(href)`, which causes `/home2` to match `/home`
since `/home2` starts with `/home`.

## What is the new behavior?

Tab selection now uses path segment matching instead of simple prefix
matching. A tab's href will only match if the pathname is an exact match
OR starts with the href followed by a / (for nested routes). This
ensures /home2 no longer incorrectly matches /home, while still allowing
/home/details to correctly match the /home tab.

## 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.13-dev.11765486444.14025098
```

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
Shane
2025-12-16 16:11:10 -08:00
committed by GitHub
parent 82de33b96e
commit 03fb422bfa
13 changed files with 337 additions and 3 deletions

View File

@@ -28,6 +28,7 @@ import Tabs from './pages/Tabs';
import TabsBasic from './pages/TabsBasic';
import NavComponent from './pages/navigation/NavComponent';
import TabsDirectNavigation from './pages/TabsDirectNavigation';
import TabsSimilarPrefixes from './pages/TabsSimilarPrefixes';
import IonModalConditional from './pages/overlay-components/IonModalConditional';
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
@@ -67,6 +68,7 @@ const App: React.FC = () => (
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-basic" component={TabsBasic} />
<Route path="/tabs-direct-navigation" component={TabsDirectNavigation} />
<Route path="/tabs-similar-prefixes" component={TabsSimilarPrefixes} />
<Route path="/icons" component={Icons} />
<Route path="/inputs" component={Inputs} />
<Route path="/reorder-group" component={ReorderGroup} />

View File

@@ -46,6 +46,9 @@ const Main: React.FC<MainProps> = () => {
<IonItem routerLink="/tabs-direct-navigation">
<IonLabel>Tabs with Direct Navigation</IonLabel>
</IonItem>
<IonItem routerLink="/tabs-similar-prefixes">
<IonLabel>Tabs with Similar Route Prefixes</IonLabel>
</IonItem>
<IonItem routerLink="/icons">
<IonLabel>Icons</IonLabel>
</IonItem>

View File

@@ -0,0 +1,87 @@
import {
IonContent,
IonHeader,
IonIcon,
IonLabel,
IonPage,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { homeOutline, radioOutline, libraryOutline } from 'ionicons/icons';
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const HomePage: React.FC = () => (
<IonPage data-testid="home-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home-content">Home Content</div>
</IonContent>
</IonPage>
);
const Home2Page: React.FC = () => (
<IonPage data-testid="home2-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home 2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home2-content">Home 2 Content</div>
</IonContent>
</IonPage>
);
const Home3Page: React.FC = () => (
<IonPage data-testid="home3-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home 3</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home3-content">Home 3 Content</div>
</IonContent>
</IonPage>
);
const TabsSimilarPrefixes: React.FC = () => {
return (
<IonTabs data-testid="tabs-similar-prefixes">
<IonRouterOutlet>
<Redirect exact path="/tabs-similar-prefixes" to="/tabs-similar-prefixes/home" />
<Route path="/tabs-similar-prefixes/home" render={() => <HomePage />} exact={true} />
<Route path="/tabs-similar-prefixes/home2" render={() => <Home2Page />} exact={true} />
<Route path="/tabs-similar-prefixes/home3" render={() => <Home3Page />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom" data-testid="tab-bar">
<IonTabButton tab="home" href="/tabs-similar-prefixes/home" data-testid="home-tab">
<IonIcon icon={homeOutline}></IonIcon>
<IonLabel>Home</IonLabel>
</IonTabButton>
<IonTabButton tab="home2" href="/tabs-similar-prefixes/home2" data-testid="home2-tab">
<IonIcon icon={radioOutline}></IonIcon>
<IonLabel>Home 2</IonLabel>
</IonTabButton>
<IonTabButton tab="home3" href="/tabs-similar-prefixes/home3" data-testid="home3-tab">
<IonIcon icon={libraryOutline}></IonIcon>
<IonLabel>Home 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default TabsSimilarPrefixes;

View File

@@ -1,4 +1,45 @@
describe('IonTabs', () => {
/**
* Verifies that tabs with similar route prefixes (e.g., /home, /home2, /home3)
* correctly select the matching tab instead of the first prefix match.
*
* @see https://github.com/ionic-team/ionic-framework/issues/30448
*/
describe('Similar Route Prefixes', () => {
it('should select the correct tab when routes have similar prefixes', () => {
cy.visit('/tabs-similar-prefixes/home2');
cy.get('[data-testid="home2-content"]').should('be.visible');
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when navigating via tab buttons', () => {
cy.visit('/tabs-similar-prefixes/home');
cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').click();
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home3-tab"]').click();
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when directly navigating to home3', () => {
cy.visit('/tabs-similar-prefixes/home3');
cy.get('[data-testid="home3-content"]').should('be.visible');
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
});
describe('With IonRouterOutlet', () => {
beforeEach(() => {
cy.visit('/tabs/tab1');