Compare commits

...

26 Commits

Author SHA1 Message Date
rug
41929970d7 Changing the values of the min-height 2026-02-05 15:40:44 +01:00
rug
dc7caf4762 Locking to attribute value 2026-01-29 15:35:20 +01:00
rug
ba782156b1 Setting min-height of text area according to rows attr 2026-01-29 15:34:47 +01:00
rug
fe289a47f5 Enforcing respect over row parameter 2026-01-28 18:38:07 +01:00
rug
7c78838edb Removing min-height from textarea-sizes classes 2026-01-28 18:33:03 +01:00
Giuliana Silva
17b8468b04 style(segment-button): add and update color to slotted children for improved styling consistency (#30905)
Issue number: resolves internal

---------

<!-- 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. -->
- Previously in iOS, when using something different than the `ion-label`
to add a text inside the `segment-button`, the default color attributed
to it was blue.
<img width="357" height="103" alt="image"
src="https://github.com/user-attachments/assets/2196e548-f0b7-4e7f-b093-e58c88b2a6cb"
/>

- In Android, the expected color is not attributed either, but it
defaults to black instead of blue.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- Now, all the slotted elements will respect the color defined by the
`color` CSS variable.

## 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. -->
2026-01-07 09:33:25 +00:00
José Rio
abb950638b fix(checkbox, toggle): uniform helper text style for ionic theme (#30893)
Issue number: resolves #

---------

<!-- 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?
Helper and Error text style it's not uniform between elements.

## What is the new behavior?
* Changed the typography for `.checkbox-bottom` and `.toggle-bottom` to
use `globals.$ion-body-sm-medium` for a more consistent appearance with
other components.
[[1]](diffhunk://#diff-796ffc72b6171910c25d5aad085bc245095c38f104d55e253e06282702205066L116-R116)
[[2]](diffhunk://#diff-ad6128c5d08f42a970eab3a8c7081eaad590bd4ebbcd19ec4e6110a6f7977040L59-R70)
* Updated the color of `.input-bottom .helper-text` and `.input-bottom
.counter` to use `globals.$ion-text-subtlest` instead of
`globals.$ion-primitives-neutral-800`, and `.input-bottom .error-text`
to use `globals.$ion-text-danger`.
* Standardized `.textarea-bottom .error-text` to use
`globals.$ion-text-danger` and reordered the CSS for clarity, while
`.textarea-bottom .helper-text` and `.textarea-bottom .counter` use
`globals.$ion-text-subtlest`.
* Updated `.toggle-bottom .error-text` to use `globals.$ion-text-danger`
and `.toggle-bottom .helper-text` to use `globals.$ion-text-subtlest`
for improved semantic clarity and consistency.

## 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.
-->

---------

Co-authored-by: ionitron <hi@ionicframework.com>
2025-12-30 19:28:41 +00:00
João Ferreira
6c74618a07 fix(segment-button): correct color for button childs (#30891)
Issue number: internal


<!-- 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 new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- guarantee that every child of segment-button has color: primary-color
when checked;

## 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.
-->

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Sample:
https://ionic-framework-git-rou-12439-ionic1.vercel.app/src/components/segment/test/basic?ionic:theme=ionic
2025-12-26 17:46:40 +00:00
Shane
68b66947d3 release-8.7.15 (#30888)
v8.7.15
2025-12-23 12:18:50 -08:00
ShaneK
5b9a0bbc4d Merge branch 'main' into fix/merge-next-12-23 2025-12-23 12:04:38 -08:00
Shane
72826edf9a merge release-8.7.15 (#30886)
v8.7.15

---------

Co-authored-by: ionitron <hi@ionicframework.com>
2025-12-23 11:14:57 -08:00
ionitron
4360e39a58 chore(): update package lock files 2025-12-23 18:55:54 +00:00
ionitron
622d62a3f4 v8.7.15 2025-12-23 18:55:19 +00:00
Shane
12ede4b79c fix(input-password-toggle): improve screen reader announcements (#30885)
Issue number: resolves internal

---------

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

The `ion-input-password-toggle` button uses `role="switch"` with
`aria-checked`, causing screen readers like VoiceOver to announce both a
state ("On/Off") and an action ("Show/Hide password"). This results in
confusing, redundant output such as "On, Hide Password" or "Off, Show
Password".

## What is the new behavior?

The password toggle button now uses `aria-pressed` instead of
`role="switch"` with `aria-checked`. Screen readers announce the
action-based label ("Show password" or "Hide password") along with the
pressed state, and properly announce state changes when the button is
activated.

## 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. -->

[Old
Preview](https://ionic-framework-git-main-ionic1.vercel.app/src/components/input-password-toggle/test/basic)

[New
Preview](https://ionic-framework-git-fw-6920-ionic1.vercel.app/src/components/input-password-toggle/test/basic)

Current dev build:
```
8.7.15-dev.11766421552.180757ca
```
2025-12-22 17:24:32 +00:00
Israel de la Barrera
f83b000530 fix(header): show iOS condense header when app is in MD mode (#30690)
Issue number: resolves #29929

---------

## What is the current behavior?
When forcing `mode=ios` in a collapsible header,
`.header-collapse-condense` would still be applied from the
`header.md.scss` file, leaving the collapsible header always hidden.

## What is the new behavior?
When forcing `mode=ios` in a collapsible header, the
`.header-collapse-condense` styles from the `header.md.scss` file won't
be applied, and the collapsible header will be visible.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No


## Other information
Something worth mentioning is that this behavior only appears after
initial load: if the route is loaded refreshing the page, the header
will appear and work correctly, but navigating forth and back will apply
both the .ios and .md style files.

I showcase this with a modal because It'll always display the broken
hehavior.

| Before | After |
|--------|-------|
| <video
src="https://github.com/user-attachments/assets/1307ee9f-452a-4b00-877d-0b8e360d3bf7">
| <video
src="https://github.com/user-attachments/assets/f9ee3851-ce94-4a27-9947-37aa1f5433b9">
|

---------

Co-authored-by: ShaneK <shane@shanessite.net>
2025-12-22 17:21:16 +00:00
Pedro Lourenço
e227fd904e refactor(checkbox): update according to design (#30882)
Issue number: resolves #

---------

<!-- 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?
The checkbox component does not currently match our UX designs for the
ionic theme.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- Changed typography to use correct design token.


## 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. -->
2025-12-22 15:54:43 +00:00
Brandy Smith
3b60a1d68a fix(modal): dismiss top-most overlay when multiple IDs match (#30883)
Issue number: resolves #30030

---------

## What is the current behavior?
When modals are presented one after another with matching IDs and then
dismissed by ID it will dismiss the first presented modal.

## What is the new behavior?
- When modals are presented one after another with matching IDs and then
dismissed by ID it will dismiss the last (top-most) presented modal.
- Added e2e tests to verify this behavior works the same as the default
dismiss (not passing an ID).

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

[Modal: Dismiss
Behavior](https://ionic-framework-git-fw-7016-ionic1.vercel.app/src/components/modal/test/dismiss-behavior)

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-12-19 19:03:04 +00:00
Maria Hutt
5bb5cc6385 refactor(core): use Capacitor safe-area CSS variables on older WebViews (#30866)
Issue number: internal

---------

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

The safe area variables are only reliant on `env` variables that are
provided by devices.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Capacitor 8 has released [safe area variable
fallbacks](https://capacitorjs.com/docs/apis/system-bars#android-note)
to provide consistent behaviors with older Android devices:

> Due to a [bug](https://issues.chromium.org/issues/40699457) in some
older versions of Android WebView (< 140), correct safe area values are
not available via the safe-area-inset-x CSS env variables. This plugin
will inject the correct inset values into a new CSS variable(s) named
--safe-area-inset-x that you can use as a fallback in your frontend
styles.

- Updated safe area variables to use the fallbacks provided by
Capacitor.

## 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. -->

Changes to the [core
file](2ee52d77c8/core/src/css/core.scss (L253-L260))
will be done on `main`.

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Co-authored-by: ionitron <hi@ionicframework.com>
2025-12-18 13:41:03 -05:00
Maria Hutt
8573bf8083 fix(core): use Capacitor safe-area CSS variables on older WebViews (#30865)
Issue number: internal

---------

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

The safe area variables are only reliant on `env` variables that are
provided by devices.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Capacitor 8 has released [safe area variable
fallbacks](https://capacitorjs.com/docs/apis/system-bars#android-note)
to provide consistent behaviors with older Android devices:

> Due to a [bug](https://issues.chromium.org/issues/40699457) in some
older versions of Android WebView (< 140), correct safe area values are
not available via the safe-area-inset-x CSS env variables. This plugin
will inject the correct inset values into a new CSS variable(s) named
--safe-area-inset-x that you can use as a fallback in your frontend
styles.

- Updated safe area variables to use the fallbacks provided by
Capacitor.

## 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. -->

Dev build: `8.7.13-dev.11765920447.1a01ab8b`

---------

Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
2025-12-18 18:16:56 +00:00
Bernardo Cardoso
1b21e0748a fix(core): add fallback handler for hardware back button when no router is present (#30878)
Issue number: internal

---------

<!-- 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 new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

This pull request enhances the hardware back button functionality to
ensure a consistent user experience, especially when no custom handlers
are registered. The main improvement is the addition of a fallback
handler that triggers the default browser back navigation when no other
handlers are present.

**Hardware Back Button Improvements:**

* Added a fallback handler in `startHardwareBackButton` that navigates
back in browser history (`win?.history.back()`) if no custom handlers
are registered, ensuring the hardware back button always performs a
meaningful action.
* Introduced a constant `FALLBACK_BACK_BUTTON_PRIORITY` with a value of
`-1` to manage the priority of the fallback handler.

**Code Consistency:**

* Moved the import of `win` from `@utils/browser` to group it with other
imports for consistency.

## 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. -->

---------

Co-authored-by: ShaneK <shane@shanessite.net>
2025-12-18 16:21:11 +00:00
Brandy Smith
34bcf95481 chore(): update next from main (#30881) 2025-12-17 13:57:04 -05:00
Brandy Smith
18eacab8fb Merge branch 'main' into chore/update-next-from-main 2025-12-17 13:25:59 -05:00
Brandy Smith
2c6fac9060 merge release-8.7.14 (#30879)
v8.7.14

---------

Co-authored-by: ionitron <hi@ionicframework.com>
2025-12-17 13:22:41 -05:00
ionitron
b9fdfab667 chore(): update package lock files 2025-12-17 17:51:54 +00:00
ionitron
f7af5d3ca5 v8.7.14 2025-12-17 17:50:44 +00:00
Shane
03fb422bfa 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>
2025-12-17 00:11:10 +00:00
100 changed files with 1006 additions and 141 deletions

View File

@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
### Bug Fixes
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package ionic-framework

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
### Bug Fixes
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/core
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/core

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@phosphor-icons/core": "^2.1.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "8.7.13",
"version": "8.7.15",
"description": "Base components for Ionic",
"engines": {
"node": ">= 16"

View File

@@ -5,7 +5,7 @@ import type { E2EPage } from '@utils/test/playwright';
/**
* Safe area tests only check top and bottom edges. RTL checks are not required here.
*/
configs({ directions: ['ltr'] }).forEach(({ config, title, screenshot }) => {
configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, title, screenshot }) => {
test.describe(title('app: safe-area'), () => {
const testOverlay = async (page: E2EPage, trigger: string, event: string, screenshotModifier: string) => {
const presentEvent = await page.spyOnEvent(event);
@@ -18,20 +18,66 @@ configs({ directions: ['ltr'] }).forEach(({ config, title, screenshot }) => {
await expect(page).toHaveScreenshot(screenshot(`app-${screenshotModifier}-diff`));
};
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/app/test/safe-area`, config);
});
test('should not have visual regressions with action sheet', async ({ page }) => {
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
test.describe(title('Ionic safe area variables'), () => {
test.beforeEach(async ({ page }) => {
const htmlTag = page.locator('html');
const hasSafeAreaClass = await htmlTag.evaluate((el) => el.classList.contains('safe-area'));
expect(hasSafeAreaClass).toBe(true);
});
test('should not have visual regressions with action sheet', async ({ page }) => {
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
});
test('should not have visual regressions with menu', async ({ page }) => {
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
});
test('should not have visual regressions with picker', async ({ page }) => {
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
});
test('should not have visual regressions with toast', async ({ page }) => {
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
});
});
test('should not have visual regressions with menu', async ({ page }) => {
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
});
test('should not have visual regressions with picker', async ({ page }) => {
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
});
test('should not have visual regressions with toast', async ({ page }) => {
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
test.describe(title('Capacitor safe area variables'), () => {
test('should use safe-area-inset vars when safe-area class is not defined', async ({ page }) => {
await page.evaluate(() => {
const html = document.documentElement;
// Remove the safe area class
html.classList.remove('safe-area');
// Set the safe area inset variables
html.style.setProperty('--safe-area-inset-top', '10px');
html.style.setProperty('--safe-area-inset-bottom', '20px');
html.style.setProperty('--safe-area-inset-left', '30px');
html.style.setProperty('--safe-area-inset-right', '40px');
});
const top = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-top').trim()
);
const bottom = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-bottom').trim()
);
const left = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-left').trim()
);
const right = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-right').trim()
);
expect(top).toBe('10px');
expect(bottom).toBe('20px');
expect(left).toBe('30px');
expect(right).toBe('40px');
});
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -174,6 +174,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
*/
@Watch('aria-checked')
@Watch('aria-label')
@Watch('aria-pressed')
onAriaChanged(newValue: string, _oldValue: string, propName: string) {
this.inheritedAttributes = {
...this.inheritedAttributes,

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -8,7 +8,7 @@
box-shadow: $header-md-box-shadow;
}
.header-collapse-condense {
.header-md.header-collapse-condense {
display: none;
}

View File

@@ -0,0 +1,71 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This test verifies that collapsible headers with mode="ios" work correctly
* when both iOS and MD stylesheets are loaded. The bug occurred because
* `.header-collapse-condense { display: none }` in the MD stylesheet was not
* scoped to `.header-md`, causing it to hide iOS condense headers when both
* stylesheets were present.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('header: condense with iOS mode override'), () => {
test('should show iOS condense header when both MD and iOS styles are loaded', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29929',
});
// Include both an MD header and an iOS modal to force both stylesheets to load
await page.setContent(
`
<!-- MD header to force MD stylesheet to load -->
<ion-header mode="md" id="mdHeader">
<ion-toolbar>
<ion-title>MD Header</ion-title>
</ion-toolbar>
</ion-header>
<!-- Modal with iOS condense header -->
<ion-modal>
<ion-header mode="ios" id="smallTitleHeader">
<ion-toolbar>
<ion-title>Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen="true">
<ion-header collapse="condense" mode="ios" id="largeTitleHeader">
<ion-toolbar>
<ion-title size="large">Large Header</ion-title>
</ion-toolbar>
</ion-header>
<p>Content</p>
</ion-content>
</ion-modal>
`,
config
);
const modal = page.locator('ion-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await modal.evaluate((el: HTMLIonModalElement) => el.present());
await ionModalDidPresent.next();
const largeTitleHeader = modal.locator('#largeTitleHeader');
// The large title header should be visible, not hidden by MD styles
await expect(largeTitleHeader).toBeVisible();
// Verify it has the iOS mode class
await expect(largeTitleHeader).toHaveClass(/header-ios/);
// Verify it does NOT have display: none applied
// This would fail if the MD stylesheet's unscoped .header-collapse-condense rule applies
const display = await largeTitleHeader.evaluate((el) => {
return window.getComputedStyle(el).display;
});
expect(display).not.toBe('none');
});
});
});

View File

@@ -169,9 +169,8 @@ export class InputPasswordToggle implements ComponentInterface {
color={color}
fill="clear"
shape="round"
aria-checked={isPasswordVisible ? 'true' : 'false'}
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
role="switch"
aria-pressed={isPasswordVisible ? 'true' : 'false'}
type="button"
onPointerDown={(ev) => {
/**

View File

@@ -22,7 +22,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
});
test.describe(title('input password toggle: aria attributes'), () => {
test('should inherit aria attributes to inner button on load', async ({ page }) => {
test('should have correct aria attributes on load', async ({ page }) => {
await page.setContent(
`
<ion-input label="input" type="password">
@@ -35,9 +35,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
const nativeButton = page.locator('ion-input-password-toggle button');
await expect(nativeButton).toHaveAttribute('aria-label', 'Show password');
await expect(nativeButton).toHaveAttribute('aria-checked', 'false');
await expect(nativeButton).toHaveAttribute('aria-pressed', 'false');
});
test('should inherit aria attributes to inner button after toggle', async ({ page }) => {
test('should update aria attributes after toggle', async ({ page }) => {
await page.setContent(
`
<ion-input label="input" type="password">
@@ -51,7 +51,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
await nativeButton.click();
await expect(nativeButton).toHaveAttribute('aria-label', 'Hide password');
await expect(nativeButton).toHaveAttribute('aria-checked', 'true');
await expect(nativeButton).toHaveAttribute('aria-pressed', 'true');
});
});
});

View File

@@ -266,15 +266,15 @@
@include globals.typography(globals.$ion-body-sm-medium);
}
.input-bottom .helper-text,
.input-bottom .counter {
color: globals.$ion-primitives-neutral-800;
}
.input-bottom .error-text {
color: globals.$ion-text-danger;
}
.input-bottom .helper-text,
.input-bottom .counter {
color: globals.$ion-text-subtlest;
}
:host(.has-focus.ion-valid) .helper-text {
color: var(--highlight-color-valid);
}

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Modal - Dismiss Behavior</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Modal - Dismiss Behavior</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<button id="present-first-modal" onclick="presentFirstModal()">Present Modal</button>
</ion-content>
</div>
</ion-app>
<script type="module">
import { modalController } from '../../../../../dist/ionic/index.esm.js';
window.modalController = modalController;
const sharedId = 'shared-modal-id';
const maxModals = 5;
let modalCount = 0;
function createModalComponent(modalNumber) {
const element = document.createElement('div');
const canPresentNext = modalNumber < maxModals;
const presentNextButton = canPresentNext
? `<ion-button id="present-next-modal" onclick="presentNextModal(${modalNumber + 1})">Present Modal ${
modalNumber + 1
}</ion-button>`
: '';
element.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Modal ${modalNumber}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>This is modal number ${modalNumber}</p>
${presentNextButton}
<ion-button class="dismiss-by-id">Dismiss By ID</ion-button>
<ion-button class="dismiss-default">Dismiss Default</ion-button>
</ion-content>
`;
return element;
}
window.presentFirstModal = async () => {
modalCount = 0;
await presentNextModal(1);
};
window.presentNextModal = async (modalNumber) => {
if (modalNumber > maxModals) {
return;
}
modalCount = Math.max(modalCount, modalNumber);
const element = createModalComponent(modalNumber);
const modal = await modalController.create({
component: element,
htmlAttributes: {
id: sharedId,
'data-testid': `modal-${modalNumber}`,
},
});
await modal.present();
const dismissByIdButton = element.querySelector('ion-button.dismiss-by-id');
dismissByIdButton.addEventListener('click', () => {
modalController.dismiss(undefined, undefined, sharedId);
});
const dismissDefaultButton = element.querySelector('ion-button.dismiss-default');
dismissDefaultButton.addEventListener('click', () => {
modalController.dismiss();
});
};
</script>
</body>
</html>

View File

@@ -0,0 +1,58 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('modal: dismiss behavior'), () => {
test.describe(title('modal: default dismiss'), () => {
test('should dismiss the last presented modal when the default dismiss button is clicked', async ({ page }) => {
await page.goto('/src/components/modal/test/dismiss-behavior', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
await page.click('#present-first-modal');
await ionModalDidPresent.next();
const firstModal = page.locator('ion-modal[data-testid="modal-1"]');
await expect(firstModal).toBeVisible();
await page.click('#present-next-modal');
await ionModalDidPresent.next();
const secondModal = page.locator('ion-modal[data-testid="modal-2"]');
await expect(secondModal).toBeVisible();
await page.click('ion-modal[data-testid="modal-2"] ion-button.dismiss-default');
await ionModalDidDismiss.next();
await secondModal.waitFor({ state: 'detached' });
await expect(firstModal).toBeVisible();
await expect(secondModal).toBeHidden();
});
});
test.describe(title('modal: dismiss by id'), () => {
test('should dismiss the last presented modal when the dismiss by id button is clicked', async ({ page }) => {
await page.goto('/src/components/modal/test/dismiss-behavior', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
await page.click('#present-first-modal');
await ionModalDidPresent.next();
const firstModal = page.locator('ion-modal[data-testid="modal-1"]');
await expect(firstModal).toBeVisible();
await page.click('#present-next-modal');
await ionModalDidPresent.next();
const secondModal = page.locator('ion-modal[data-testid="modal-2"]');
await expect(secondModal).toBeVisible();
await page.click('ion-modal[data-testid="modal-2"] ion-button.dismiss-by-id');
await ionModalDidDismiss.next();
await secondModal.waitFor({ state: 'detached' });
await expect(firstModal).toBeVisible();
await expect(secondModal).toBeHidden();
});
});
});
});

View File

@@ -1,7 +1,7 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../modal';
import { h } from '@stencil/core';
describe('modal: id', () => {
it('modal should be assigned an incrementing id', async () => {
@@ -52,4 +52,21 @@ describe('modal: id', () => {
const alert = page.body.querySelector('ion-modal')!;
expect(alert.id).toBe(id);
});
it('should allow multiple modals with the same id', async () => {
const sharedId = 'shared-modal-id';
const page = await newSpecPage({
components: [Modal],
template: () => [
<ion-modal id={sharedId} overlayIndex={1} is-open={true}></ion-modal>,
<ion-modal id={sharedId} overlayIndex={2} is-open={true}></ion-modal>,
],
});
const modals = page.body.querySelectorAll('ion-modal');
expect(modals.length).toBe(2);
expect(modals[0].id).toBe(sharedId);
expect(modals[1].id).toBe(sharedId);
});
});

View File

@@ -10,7 +10,7 @@
*/
--background: none;
--background-checked: var(--background);
--color: #{globals.$ion-primitives-neutral-1000};
--color: #{globals.$ion-primitives-neutral-1200};
--color-checked: #{globals.$ion-semantics-primary-base};
--color-disabled: #{globals.$ion-primitives-neutral-500};
--border-width: #{globals.$ion-border-size-025};
@@ -44,13 +44,15 @@
gap: globals.$ion-space-100;
}
::slotted(*) {
color: var(--color);
}
// Segment Button Label
// --------------------------------------------------
::slotted(ion-label) {
@include globals.typography(globals.$ion-body-action-sm);
color: var(--color);
}
// Segment Button Icon
@@ -59,23 +61,21 @@
::slotted(ion-icon) {
width: globals.$ion-scale-600;
height: globals.$ion-scale-600;
color: var(--color);
}
// Segment Button: Checked
// All slot content should have the checked color even when it's not label or icon
// --------------------------------------------------
:host(.segment-button-checked) ::slotted(ion-label),
:host(.segment-button-checked) ::slotted(ion-icon) {
:host(.segment-button-checked) ::slotted(*) {
color: var(--color-checked);
}
// Segment Button: Disabled
// All slot content should have the disabled color even when it's not label or icon
// --------------------------------------------------
:host(.segment-button-disabled) ::slotted(ion-label),
:host(.segment-button-disabled) ::slotted(ion-icon) {
:host(.segment-button-disabled) ::slotted(*) {
color: var(--color-disabled);
}

View File

@@ -533,6 +533,17 @@
opacity: 1;
}
/**
* When the rows attribute is set, the textarea should not force a min-height,
* but rather respect the natural height of the textarea. Specially when
* rows is set to 1.
*/
:host(.textarea-fill-outline.sc-ion-textarea-md-h[rows]),
:host(.textarea-label-placement-stacked[rows]),
:host(.textarea-label-placement-floating[rows]) {
min-height: auto;
}
// Start / End Slots
// ----------------------------------------------------------------

View File

@@ -38,8 +38,6 @@
--padding-end: #{globals.$ion-space-300};
--padding-bottom: #{globals.$ion-space-200};
--padding-start: #{globals.$ion-space-300};
min-height: globals.$ion-scale-2800;
}
:host(.textarea-size-medium) .textarea-wrapper-inner {
@@ -47,8 +45,6 @@
--padding-end: #{globals.$ion-space-400};
--padding-bottom: #{globals.$ion-space-300};
--padding-start: #{globals.$ion-space-400};
min-height: globals.$ion-scale-3400;
}
:host(.textarea-size-large) .textarea-wrapper-inner {
@@ -56,8 +52,6 @@
--padding-end: #{globals.$ion-space-500};
--padding-bottom: #{globals.$ion-space-400};
--padding-start: #{globals.$ion-space-500};
min-height: globals.$ion-scale-3600;
}
// Ionic Textarea Shapes
@@ -90,17 +84,26 @@
// ----------------------------------------------------------------
// The height should be auto only when auto-grow is enabled.
:host([auto-grow]) .textarea-wrapper-inner {
:host([auto-grow=true]) .textarea-wrapper-inner {
height: auto;
}
// The min and max height should be inherited if auto-grow is enabled.
// This allows the textarea to grow and shrink as needed.
:host([auto-grow]) .native-wrapper {
:host([auto-grow=true]) .native-wrapper {
min-height: inherit;
max-height: inherit;
}
:host([rows]) {
--rows-number: attr(rows number);
}
:host([rows]) .textarea-wrapper-inner {
height: auto;
min-height: calc(var(--rows-number) * 1em + 0.5em);
}
// Textarea Label
// ----------------------------------------------------------------
@@ -141,15 +144,15 @@ ion-icon {
@include globals.typography(globals.$ion-body-sm-medium);
}
.textarea-bottom .error-text {
color: globals.$ion-text-danger;
}
.textarea-bottom .helper-text,
.textarea-bottom .counter {
color: globals.$ion-text-subtlest;
}
.textarea-bottom .error-text {
color: globals.$ion-text-danger;
}
:host(.has-focus.ion-valid) .helper-text {
color: var(--highlight-color-valid);
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -56,18 +56,18 @@
// ----------------------------------------------------------------
.toggle-bottom {
@include globals.typography(globals.$ion-body-md-regular);
@include globals.typography(globals.$ion-body-sm-medium);
}
// Toggle Hint Text
// ----------------------------------------------------------------
.toggle-bottom .error-text {
color: globals.$ion-semantics-danger-900;
color: globals.$ion-text-danger;
}
.toggle-bottom .helper-text {
color: globals.$ion-primitives-neutral-900;
color: globals.$ion-text-subtlest;
}
// Toggle Native Wrapper: Focused

View File

@@ -227,10 +227,12 @@ html.plt-ios.plt-pwa {
@supports (padding-top: env(safe-area-inset-top)) {
html {
--ion-safe-area-top: env(safe-area-inset-top);
--ion-safe-area-bottom: env(safe-area-inset-bottom);
--ion-safe-area-left: env(safe-area-inset-left);
--ion-safe-area-right: env(safe-area-inset-right);
// `--safe-area-inset-*` are set by Capacitor
// @see https://capacitorjs.com/docs/apis/system-bars#android-note
--ion-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top));
--ion-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--ion-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left));
--ion-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right));
}
}

View File

@@ -221,10 +221,12 @@ html.plt-ios.plt-pwa {
@supports (padding-top: env(safe-area-inset-top)) {
html {
--ion-safe-area-top: env(safe-area-inset-top);
--ion-safe-area-bottom: env(safe-area-inset-bottom);
--ion-safe-area-left: env(safe-area-inset-left);
--ion-safe-area-right: env(safe-area-inset-right);
// `--safe-area-inset-*` are set by Capacitor
// @see https://capacitorjs.com/docs/apis/system-bars#android-note
--ion-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top));
--ion-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--ion-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left));
--ion-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right));
}
}

View File

@@ -1,5 +1,5 @@
import { win } from '@utils/browser';
import type { CloseWatcher } from '@utils/browser';
import { win } from '@utils/browser';
import { printIonError } from '@utils/logging';
import { config } from '../global/config';
@@ -69,6 +69,21 @@ export const startHardwareBackButton = () => {
});
doc.dispatchEvent(ev);
/**
* If no handlers have been registered, fall back to the default
* behavior of navigating back in history. This ensures the hardware
* back button works even when no router or custom handler is present.
*/
if (handlers.length === 0) {
handlers.push({
priority: FALLBACK_BACK_BUTTON_PRIORITY,
handler: () => {
win?.history.back();
},
id: index++,
});
}
const executeAction = async (handlerRegister: HandlerRegister | undefined) => {
try {
if (handlerRegister?.handler) {
@@ -138,3 +153,4 @@ export const startHardwareBackButton = () => {
export const OVERLAY_BACK_BUTTON_PRIORITY = 100;
export const MENU_BACK_BUTTON_PRIORITY = 99; // 1 less than overlay priority since menu is displayed behind overlays
const FALLBACK_BACK_BUTTON_PRIORITY = -1; // Fallback when no other handlers are registered

View File

@@ -473,7 +473,9 @@ export const getPresentedOverlay = (
id?: string
): HTMLIonOverlayElement | undefined => {
const overlays = getPresentedOverlays(doc, overlayTag);
return id === undefined ? overlays[overlays.length - 1] : overlays.find((o) => o.id === id);
// If no id is provided, return the last presented overlay
// Otherwise, return the last overlay with the given id
return (id === undefined ? overlays : overlays.filter((o: HTMLIonOverlayElement) => o.id === id)).slice(-1)[0];
};
/**

View File

@@ -54,6 +54,47 @@ describe('Hardware Back Button', () => {
dispatchBackButtonEvent();
expect(cbSpyTwo).toHaveBeenCalled();
});
it('should fall back to history.back() when no handlers are registered', () => {
const historyBackSpy = jest.fn();
const originalBack = win?.history?.back;
if (win?.history) {
win.history.back = historyBackSpy;
}
// Don't register any ionBackButton handlers
dispatchBackButtonEvent();
expect(historyBackSpy).toHaveBeenCalled();
// Restore original
if (win?.history && originalBack) {
win.history.back = originalBack;
}
});
it('should not call history.back() when a handler is registered', () => {
const historyBackSpy = jest.fn();
const originalBack = win?.history?.back;
if (win?.history) {
win.history.back = historyBackSpy;
}
const cbSpy = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
(ev as BackButtonEvent).detail.register(0, cbSpy);
});
dispatchBackButtonEvent();
expect(cbSpy).toHaveBeenCalled();
expect(historyBackSpy).not.toHaveBeenCalled();
// Restore original
if (win?.history && originalBack) {
win.history.back = originalBack;
}
});
});
describe('Experimental Close Watcher', () => {

View File

@@ -3,5 +3,5 @@
"core",
"packages/*"
],
"version": "8.7.13"
"version": "8.7.15"
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.13"
"@ionic/core": "^8.7.15"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^16.0.0",
@@ -1031,9 +1031,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -7309,9 +7309,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "8.7.13",
"version": "8.7.15",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^8.7.13"
"@ionic/core": "^8.7.15"
}
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/angular
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/angular
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/angular

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -1398,9 +1398,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "8.7.13",
"version": "8.7.15",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -48,7 +48,7 @@
}
},
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/docs
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/docs
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/docs

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/docs",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "8.7.13",
"version": "8.7.15",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/react-router
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/react-router
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/react-router

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react-router",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/react-router",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/react": "^8.7.13",
"@ionic/react": "^8.7.15",
"tslib": "*"
},
"devDependencies": {
@@ -238,9 +238,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -418,12 +418,12 @@
}
},
"node_modules/@ionic/react": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.13.tgz",
"integrity": "sha512-PCuIpaurVYxYZ/CoUN3gP56Fwdx+bx78Qy7V5Ac61nGGW7XpVlV4vM9328Kv7OPs5fkmIvKI6LoY78BnjF0PkA==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.15.tgz",
"integrity": "sha512-3W5DAN3OMe2w32AGlW8zvKZHNY/dLCWhVLL9485VKtsYvgUc9nkG8dYDvQGc6c17PEDVrMy+IAl4H9hLgJhYnQ==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.15",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -4178,9 +4178,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",
@@ -4284,11 +4284,11 @@
"requires": {}
},
"@ionic/react": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.13.tgz",
"integrity": "sha512-PCuIpaurVYxYZ/CoUN3gP56Fwdx+bx78Qy7V5Ac61nGGW7XpVlV4vM9328Kv7OPs5fkmIvKI6LoY78BnjF0PkA==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.15.tgz",
"integrity": "sha512-3W5DAN3OMe2w32AGlW8zvKZHNY/dLCWhVLL9485VKtsYvgUc9nkG8dYDvQGc6c17PEDVrMy+IAl4H9hLgJhYnQ==",
"requires": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.15",
"ionicons": "^8.0.13",
"tslib": "*"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react-router",
"version": "8.7.13",
"version": "8.7.15",
"description": "React Router wrapper for @ionic/react",
"keywords": [
"ionic",
@@ -36,7 +36,7 @@
"dist/"
],
"dependencies": {
"@ionic/react": "^8.7.13",
"@ionic/react": "^8.7.15",
"tslib": "*"
},
"peerDependencies": {

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/react
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/react

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/react",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -736,9 +736,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react",
"version": "8.7.13",
"version": "8.7.15",
"description": "React specific wrapper for @ionic/core",
"keywords": [
"ionic",
@@ -40,7 +40,7 @@
"css/"
],
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"ionicons": "^8.0.13",
"tslib": "*"
},

View File

@@ -40,6 +40,20 @@ interface IonTabBarState {
// TODO(FW-2959): types
/**
* Checks if pathname matches the tab's href using path segment matching.
* Avoids false matches like /home2 matching /home by requiring exact match
* or a path segment boundary (/).
*/
const matchesTab = (pathname: string, href: string | undefined): boolean => {
if (href === undefined) {
return false;
}
const normalizedHref = href.endsWith('/') && href !== '/' ? href.slice(0, -1) : href;
return pathname === normalizedHref || pathname.startsWith(normalizedHref + '/');
};
class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarState> {
context!: React.ContextType<typeof NavContext>;
@@ -79,7 +93,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
const tabKeys = Object.keys(tabs);
const activeTab = tabKeys.find((key) => {
const href = tabs[key].originalHref;
return this.props.routeInfo!.pathname.startsWith(href);
return matchesTab(this.props.routeInfo!.pathname, href);
});
if (activeTab) {
@@ -121,7 +135,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
const tabKeys = Object.keys(state.tabs);
const activeTab = tabKeys.find((key) => {
const href = state.tabs[key].originalHref;
return props.routeInfo!.pathname.startsWith(href);
return matchesTab(props.routeInfo!.pathname, href);
});
// Check to see if the tab button href has changed, and if so, update it in the tabs state

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');

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/vue-router

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue-router",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue-router",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/vue": "^8.7.13"
"@ionic/vue": "^8.7.15"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",
@@ -673,9 +673,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -868,12 +868,12 @@
}
},
"node_modules/@ionic/vue": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.13.tgz",
"integrity": "sha512-hflvGaNPABYP0Qt68YgrauVaXyjKeHODOkYzJhk36kcr+VexwTWm1FGJG1/nKKgdh6fwDIsubJvlhoZaRhtVtg==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.15.tgz",
"integrity": "sha512-F94GoGBsLoFFkJLe+W401SynIfUzEL1Z+Y4YUQ2EGpulnFpY4tXKzDhRNrisPnM0k3BWQRh8AA8fwTtMqEiISw==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.15",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}
@@ -8044,9 +8044,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",
@@ -8159,11 +8159,11 @@
"requires": {}
},
"@ionic/vue": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.13.tgz",
"integrity": "sha512-hflvGaNPABYP0Qt68YgrauVaXyjKeHODOkYzJhk36kcr+VexwTWm1FGJG1/nKKgdh6fwDIsubJvlhoZaRhtVtg==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.15.tgz",
"integrity": "sha512-F94GoGBsLoFFkJLe+W401SynIfUzEL1Z+Y4YUQ2EGpulnFpY4tXKzDhRNrisPnM0k3BWQRh8AA8fwTtMqEiISw==",
"requires": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.15",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue-router",
"version": "8.7.13",
"version": "8.7.15",
"description": "Vue Router integration for @ionic/vue",
"scripts": {
"test.spec": "jest",
@@ -44,7 +44,7 @@
},
"homepage": "https://github.com/ionic-team/ionic-framework#readme",
"dependencies": {
"@ionic/vue": "^8.7.13"
"@ionic/vue": "^8.7.15"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
**Note:** Version bump only for package @ionic/vue
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/vue

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue",
"version": "8.7.13",
"version": "8.7.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue",
"version": "8.7.13",
"version": "8.7.15",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
},
@@ -222,9 +222,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue",
"version": "8.7.13",
"version": "8.7.15",
"description": "Vue specific wrapper for @ionic/core",
"scripts": {
"eslint": "eslint src",
@@ -68,7 +68,7 @@
"vue-router": "^4.0.16"
},
"dependencies": {
"@ionic/core": "^8.7.13",
"@ionic/core": "^8.7.15",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
},

View File

@@ -24,6 +24,23 @@ interface TabBarData {
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
/**
* Checks if pathname matches the tab's href using path segment matching.
* Avoids false matches like /home2 matching /home by requiring exact match
* or a path segment boundary (/).
*/
const matchesTab = (pathname: string, href: string | undefined): boolean => {
if (href === undefined) {
return false;
}
const normalizedHref =
href.endsWith("/") && href !== "/" ? href.slice(0, -1) : href;
return (
pathname === normalizedHref || pathname.startsWith(normalizedHref + "/")
);
};
const getTabs = (nodes: VNode[]) => {
let tabs: VNode[] = [];
nodes.forEach((node: VNode) => {
@@ -135,7 +152,9 @@ export const IonTabBar = defineComponent({
const tabKeys = Object.keys(tabs);
let activeTab = tabKeys.find((key) => {
const href = tabs[key].originalHref;
return currentRoute?.pathname.startsWith(href);
return (
currentRoute?.pathname && matchesTab(currentRoute.pathname, href)
);
});
/**

View File

@@ -165,6 +165,28 @@ const routes: Array<RouteRecordRaw> = [
path: '/tabs-basic',
component: () => import('@/views/TabsBasic.vue')
},
{
path: '/tabs-similar-prefixes/',
component: () => import('@/views/tabs-similar-prefixes/TabsSimilarPrefixes.vue'),
children: [
{
path: '',
redirect: '/tabs-similar-prefixes/home'
},
{
path: 'home',
component: () => import('@/views/tabs-similar-prefixes/Home.vue'),
},
{
path: 'home2',
component: () => import('@/views/tabs-similar-prefixes/Home2.vue'),
},
{
path: 'home3',
component: () => import('@/views/tabs-similar-prefixes/Home3.vue'),
}
]
},
]
const router = createRouter({

View File

@@ -50,6 +50,9 @@
<ion-item router-link="/tabs-basic" id="tab-basic">
<ion-label>Tabs with Basic Navigation</ion-label>
</ion-item>
<ion-item router-link="/tabs-similar-prefixes" id="tabs-similar-prefixes">
<ion-label>Tabs with Similar Route Prefixes</ion-label>
</ion-item>
<ion-item router-link="/lifecycle" id="lifecycle">
<ion-label>Lifecycle</ion-label>
</ion-item>

View File

@@ -0,0 +1,16 @@
<template>
<ion-page data-pageid="home">
<ion-header>
<ion-toolbar>
<ion-title>Home</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home-content">Home Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -0,0 +1,16 @@
<template>
<ion-page data-pageid="home2">
<ion-header>
<ion-toolbar>
<ion-title>Home 2</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home2-content">Home 2 Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -0,0 +1,16 @@
<template>
<ion-page data-pageid="home3">
<ion-header>
<ion-toolbar>
<ion-title>Home 3</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home3-content">Home 3 Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -0,0 +1,54 @@
<template>
<ion-page data-pageid="tabs-similar-prefixes">
<ion-content>
<ion-tabs id="tabs-similar-prefixes">
<ion-router-outlet></ion-router-outlet>
<ion-tab-bar slot="bottom" data-testid="tab-bar">
<ion-tab-button
tab="home"
href="/tabs-similar-prefixes/home"
data-testid="home-tab"
id="tab-button-home"
>
<ion-icon :icon="homeOutline" />
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button
tab="home2"
href="/tabs-similar-prefixes/home2"
data-testid="home2-tab"
id="tab-button-home2"
>
<ion-icon :icon="radioOutline" />
<ion-label>Home 2</ion-label>
</ion-tab-button>
<ion-tab-button
tab="home3"
href="/tabs-similar-prefixes/home3"
data-testid="home3-tab"
id="tab-button-home3"
>
<ion-icon :icon="libraryOutline" />
<ion-label>Home 3</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import {
IonContent,
IonIcon,
IonLabel,
IonPage,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
} from '@ionic/vue';
import { homeOutline, radioOutline, libraryOutline } from 'ionicons/icons';
</script>

View File

@@ -1,4 +1,45 @@
describe('Tabs', () => {
/**
* 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', () => {
it('should go back from child pages', () => {
cy.visit('/tabs');