mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
4 Commits
v8.7.16
...
fix/dateti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8772e8bd9 | ||
|
|
12ef9345d6 | ||
|
|
79d5716baf | ||
|
|
14419288c0 |
@@ -10,7 +10,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
@@ -62,7 +62,7 @@ runs:
|
||||
working-directory: ./core
|
||||
- name: 📦 Archive Updated Screenshots
|
||||
if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
|
||||
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip
|
||||
|
||||
@@ -10,7 +10,7 @@ runs:
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ./artifacts
|
||||
- name: 🔎 Extract Archives
|
||||
|
||||
@@ -13,7 +13,7 @@ runs:
|
||||
- name: 🗄️ Create Archive
|
||||
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
|
||||
shell: bash
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.output }}
|
||||
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -3,50 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** prevent card modal animation on viewport resize when modal is closed ([#30894](https://github.com/ionic-team/ionic-framework/issues/30894)) ([e5634d4](https://github.com/ionic-team/ionic-framework/commit/e5634d45ee5fd32715f6e6b75e0448f74ee1f8f2)), closes [#30679](https://github.com/ionic-team/ionic-framework/issues/30679)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
@@ -3,47 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** prevent card modal animation on viewport resize when modal is closed ([#30894](https://github.com/ionic-team/ionic-framework/issues/30894)) ([e5634d4](https://github.com/ionic-team/ionic-framework/commit/e5634d45ee5fd32715f6e6b75e0448f74ee1f8f2)), closes [#30679](https://github.com/ionic-team/ionic-framework/issues/30679)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
45
core/package-lock.json
generated
45
core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"stylelint-order": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"custom-rules": {
|
||||
@@ -94,6 +94,7 @@
|
||||
"version": "7.16.12",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.16.8",
|
||||
@@ -633,6 +634,7 @@
|
||||
"integrity": "sha512-250HTVd/W/KdMygoqaedisvNbHbpbQTN2Hy/8ZYGm1nAqE0Fx7sGss4l0nDg33STxEdDhtVRoL2fIaaiukKseA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -870,6 +872,7 @@
|
||||
"version": "4.33.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "4.33.0",
|
||||
"@typescript-eslint/types": "4.33.0",
|
||||
@@ -1807,6 +1810,7 @@
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.38.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@@ -2231,6 +2235,7 @@
|
||||
"version": "6.7.2",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.7.2",
|
||||
"@typescript-eslint/types": "6.7.2",
|
||||
@@ -2456,7 +2461,6 @@
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.25",
|
||||
@@ -2471,7 +2475,6 @@
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
@@ -2484,8 +2487,7 @@
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.25",
|
||||
@@ -2493,7 +2495,6 @@
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
@@ -2505,7 +2506,6 @@
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
@@ -2523,8 +2523,7 @@
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
@@ -2546,7 +2545,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -2562,7 +2560,6 @@
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
@@ -2574,7 +2571,6 @@
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.25"
|
||||
}
|
||||
@@ -2585,7 +2581,6 @@
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
@@ -2597,7 +2592,6 @@
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
@@ -2611,7 +2605,6 @@
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
@@ -2625,8 +2618,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zeit/schemas": {
|
||||
"version": "2.21.0",
|
||||
@@ -2649,6 +2641,7 @@
|
||||
"version": "7.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3809,8 +3802,7 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
@@ -4104,6 +4096,7 @@
|
||||
"version": "7.32.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.3",
|
||||
@@ -7299,7 +7292,6 @@
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
@@ -7621,7 +7613,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -7976,6 +7967,7 @@
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
@@ -7987,6 +7979,7 @@
|
||||
"version": "7.0.35",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.2",
|
||||
"source-map": "^0.6.1",
|
||||
@@ -8092,6 +8085,7 @@
|
||||
"version": "0.36.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"postcss": ">=5.0.0"
|
||||
}
|
||||
@@ -8140,6 +8134,7 @@
|
||||
"version": "2.6.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
@@ -8497,6 +8492,7 @@
|
||||
"version": "2.35.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -8718,7 +8714,6 @@
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9839,4 +9834,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "Base components for Ionic",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
},
|
||||
"keywords": [
|
||||
"ionic",
|
||||
|
||||
@@ -18,66 +18,20 @@ 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.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 action sheet', async ({ page }) => {
|
||||
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,6 @@ 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,
|
||||
|
||||
@@ -21,6 +21,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
|
||||
);
|
||||
|
||||
const datetimeButton = page.locator('ion-datetime-button');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale`));
|
||||
});
|
||||
@@ -40,6 +41,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
|
||||
);
|
||||
|
||||
const datetimeButton = page.locator('ion-datetime-button');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale-wrap`));
|
||||
});
|
||||
|
||||
@@ -24,6 +24,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await dateButton.click();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// Wait for datetime to be ready before taking screenshot
|
||||
await page.locator('ion-datetime.datetime-ready').waitFor();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-modal`));
|
||||
});
|
||||
|
||||
@@ -44,6 +47,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await dateButton.click();
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
// Wait for datetime to be ready before taking screenshot
|
||||
await page.locator('ion-datetime.datetime-ready').waitFor();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-popover`));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,8 +108,8 @@ export class Datetime implements ComponentInterface {
|
||||
private inputId = `ion-dt-${datetimeIds++}`;
|
||||
private calendarBodyRef?: HTMLElement;
|
||||
private popoverRef?: HTMLIonPopoverElement;
|
||||
private intersectionTrackerRef?: HTMLElement;
|
||||
private clearFocusVisible?: () => void;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private parsedMinuteValues?: number[];
|
||||
private parsedHourValues?: number[];
|
||||
private parsedMonthValues?: number[];
|
||||
@@ -118,6 +118,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
private destroyCalendarListener?: () => void;
|
||||
private destroyKeyboardMO?: () => void;
|
||||
private destroyOverlayListeners?: () => void;
|
||||
|
||||
// TODO(FW-2832): types (DatetimeParts causes some errors that need untangling)
|
||||
private minParts?: any;
|
||||
@@ -1077,6 +1078,14 @@ export class Datetime implements ComponentInterface {
|
||||
this.clearFocusVisible();
|
||||
this.clearFocusVisible = undefined;
|
||||
}
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = undefined;
|
||||
}
|
||||
if (this.destroyOverlayListeners) {
|
||||
this.destroyOverlayListeners();
|
||||
this.destroyOverlayListeners = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1102,113 +1111,100 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* Fallback to ensure the datetime becomes ready even if
|
||||
* IntersectionObserver never reports it as intersecting.
|
||||
* Sets up visibility detection for the datetime component.
|
||||
*
|
||||
* This is primarily used in environments where the observer
|
||||
* might not fire as expected, such as when running under
|
||||
* synthetic tests that stub IntersectionObserver.
|
||||
* Uses multiple strategies to reliably detect when the datetime becomes
|
||||
* visible, which is necessary for proper initialization of scrollable areas:
|
||||
* 1. ResizeObserver - detects dimension changes
|
||||
* 2. Overlay event listeners - for datetime inside modals/popovers
|
||||
* 3. Polling fallback - for browsers where observers are unreliable (WebKit)
|
||||
*/
|
||||
private ensureReadyIfVisible = () => {
|
||||
if (this.el.classList.contains('datetime-ready')) {
|
||||
return;
|
||||
private initializeVisibilityObserver() {
|
||||
const { el } = this;
|
||||
|
||||
const markReady = () => {
|
||||
if (el.classList.contains('datetime-ready')) {
|
||||
return;
|
||||
}
|
||||
this.initializeListeners();
|
||||
writeTask(() => {
|
||||
el.classList.add('datetime-ready');
|
||||
});
|
||||
};
|
||||
|
||||
const markHidden = () => {
|
||||
this.destroyInteractionListeners();
|
||||
this.showMonthAndYear = false;
|
||||
writeTask(() => {
|
||||
el.classList.remove('datetime-ready');
|
||||
});
|
||||
startVisibilityPolling();
|
||||
};
|
||||
|
||||
/**
|
||||
* FW-6931: Poll for visibility as a fallback for browsers where
|
||||
* ResizeObserver doesn't fire reliably (e.g., WebKit).
|
||||
*/
|
||||
const startVisibilityPolling = () => {
|
||||
let pollCount = 0;
|
||||
const poll = () => {
|
||||
if (el.classList.contains('datetime-ready') || pollCount++ >= 60) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
if (width > 0 && height > 0) {
|
||||
markReady();
|
||||
} else {
|
||||
raf(poll);
|
||||
}
|
||||
};
|
||||
raf(poll);
|
||||
};
|
||||
|
||||
/**
|
||||
* FW-6931: Listen for overlay present/dismiss events when datetime
|
||||
* is inside a modal or popover.
|
||||
*/
|
||||
const parentOverlay = el.closest('ion-modal, ion-popover') as HTMLIonModalElement | HTMLIonPopoverElement | null;
|
||||
if (parentOverlay) {
|
||||
const handlePresent = () => markReady();
|
||||
const handleDismiss = () => markHidden();
|
||||
|
||||
parentOverlay.addEventListener('didPresent', handlePresent);
|
||||
parentOverlay.addEventListener('didDismiss', handleDismiss);
|
||||
|
||||
this.destroyOverlayListeners = () => {
|
||||
parentOverlay.removeEventListener('didPresent', handlePresent);
|
||||
parentOverlay.removeEventListener('didDismiss', handleDismiss);
|
||||
};
|
||||
}
|
||||
|
||||
const rect = this.el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return;
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
const isVisible = width > 0 && height > 0;
|
||||
const isReady = el.classList.contains('datetime-ready');
|
||||
|
||||
if (isVisible && !isReady) {
|
||||
markReady();
|
||||
} else if (!isVisible && isReady) {
|
||||
markHidden();
|
||||
}
|
||||
});
|
||||
|
||||
// Use raf to avoid race condition with modal/popover animations
|
||||
raf(() => this.resizeObserver?.observe(el));
|
||||
startVisibilityPolling();
|
||||
} else {
|
||||
// Test environment fallback - mark ready immediately
|
||||
writeTask(() => {
|
||||
el.classList.add('datetime-ready');
|
||||
});
|
||||
}
|
||||
|
||||
this.initializeListeners();
|
||||
|
||||
writeTask(() => {
|
||||
this.el.classList.add('datetime-ready');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
const { el, intersectionTrackerRef } = this;
|
||||
|
||||
/**
|
||||
* If a scrollable element is hidden using `display: none`,
|
||||
* it will not have a scroll height meaning we cannot scroll elements
|
||||
* into view. As a result, we will need to wait for the datetime to become
|
||||
* visible if used inside of a modal or a popover otherwise the scrollable
|
||||
* areas will not have the correct values snapped into place.
|
||||
*/
|
||||
const visibleCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
if (!ev.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializeListeners();
|
||||
|
||||
/**
|
||||
* TODO FW-2793: Datetime needs a frame to ensure that it
|
||||
* can properly scroll contents into view. As a result
|
||||
* we hide the scrollable content until after that frame
|
||||
* so users do not see the content quickly shifting. The downside
|
||||
* is that the content will pop into view a frame after. Maybe there
|
||||
* is a better way to handle this?
|
||||
*/
|
||||
writeTask(() => {
|
||||
this.el.classList.add('datetime-ready');
|
||||
});
|
||||
};
|
||||
const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el });
|
||||
|
||||
/**
|
||||
* Use raf to avoid a race condition between the component loading and
|
||||
* its display animation starting (such as when shown in a modal). This
|
||||
* could cause the datetime to start at a visibility of 0, erroneously
|
||||
* triggering the `hiddenIO` observer below.
|
||||
*/
|
||||
raf(() => visibleIO?.observe(intersectionTrackerRef!));
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* Fallback: If IntersectionObserver never reports that the
|
||||
* datetime is visible but the host clearly has layout, ensure
|
||||
* we still initialize listeners and mark the component as ready.
|
||||
*
|
||||
* We schedule this after everything has had a chance to run.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
this.ensureReadyIfVisible();
|
||||
}, 100);
|
||||
|
||||
/**
|
||||
* We need to clean up listeners when the datetime is hidden
|
||||
* in a popover/modal so that we can properly scroll containers
|
||||
* back into view if they are re-presented. When the datetime is hidden
|
||||
* the scroll areas have scroll widths/heights of 0px, so any snapping
|
||||
* we did originally has been lost.
|
||||
*/
|
||||
const hiddenCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
if (ev.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyInteractionListeners();
|
||||
|
||||
/**
|
||||
* When datetime is hidden, we need to make sure that
|
||||
* the month/year picker is closed. Otherwise,
|
||||
* it will be open when the datetime re-appears
|
||||
* and the scroll area of the calendar grid will be 0.
|
||||
* As a result, the wrong month will be shown.
|
||||
*/
|
||||
this.showMonthAndYear = false;
|
||||
|
||||
writeTask(() => {
|
||||
this.el.classList.remove('datetime-ready');
|
||||
});
|
||||
};
|
||||
const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0, root: el });
|
||||
raf(() => hiddenIO?.observe(intersectionTrackerRef!));
|
||||
this.initializeVisibilityObserver();
|
||||
|
||||
/**
|
||||
* Datetime uses Ionic components that emit
|
||||
@@ -2693,20 +2689,6 @@ export class Datetime implements ComponentInterface {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
WebKit has a quirk where IntersectionObserver callbacks are delayed until after
|
||||
an accelerated animation finishes if the "root" specified in the config is the
|
||||
browser viewport (the default behavior if "root" is not specified). This means
|
||||
that when presenting a datetime in a modal on iOS the calendar body appears
|
||||
blank until the modal animation finishes.
|
||||
|
||||
We can work around this by observing .intersection-tracker and using the host
|
||||
(ion-datetime) as the "root". This allows the IO callback to fire the moment
|
||||
the datetime is visible. The .intersection-tracker element should not have
|
||||
dimensions or additional styles, and it should not be positioned absolutely
|
||||
otherwise the IO callback may fire at unexpected times.
|
||||
*/}
|
||||
<div class="intersection-tracker" ref={(el) => (this.intersectionTrackerRef = el)}></div>
|
||||
{this.renderDatetime(mode)}
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -395,40 +395,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
|
||||
/**
|
||||
* Synthetic IntersectionObserver fallback behavior.
|
||||
*
|
||||
* This test stubs IntersectionObserver so that the callback
|
||||
* never reports an intersecting entry. The datetime should
|
||||
* still become ready via its internal fallback logic.
|
||||
* Verify that datetime becomes ready via ResizeObserver.
|
||||
* This tests that the datetime properly initializes when it has
|
||||
* dimensions, using ResizeObserver to detect visibility.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: IO fallback'), () => {
|
||||
test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => {
|
||||
test.describe(title('datetime: visibility detection'), () => {
|
||||
test('should become ready when rendered with dimensions', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
|
||||
});
|
||||
|
||||
await page.addInitScript(() => {
|
||||
const OriginalIO = window.IntersectionObserver;
|
||||
(window as any).IntersectionObserver = function (callback: any, options: any) {
|
||||
const instance = new OriginalIO(() => {}, options);
|
||||
const originalObserve = instance.observe.bind(instance);
|
||||
|
||||
instance.observe = (target: Element) => {
|
||||
originalObserve(target);
|
||||
callback([
|
||||
{
|
||||
isIntersecting: false,
|
||||
target,
|
||||
} as IntersectionObserverEntry,
|
||||
]);
|
||||
};
|
||||
|
||||
return instance;
|
||||
} as any;
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-05-03"></ion-datetime>
|
||||
@@ -438,8 +416,8 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
// Give the fallback a short amount of time to run
|
||||
await page.waitForTimeout(100);
|
||||
// Wait for the datetime to become ready via ResizeObserver
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
await expect(datetime).toHaveClass(/datetime-ready/);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('datetime: custom focus'), () => {
|
||||
test('should focus the selected day and then the day after', async ({ page }) => {
|
||||
await page.goto(`/src/components/datetime/test/custom`, config);
|
||||
await page.locator('.datetime-ready').last().waitFor();
|
||||
|
||||
const datetime = page.locator('#custom-calendar-days');
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
await page.goto('/src/components/datetime/test/first-day-of-week', config);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-day-of-week`));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
`,
|
||||
config
|
||||
);
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
});
|
||||
|
||||
test('should render highlights correctly when using an array', async ({ page }) => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
box-shadow: $header-md-box-shadow;
|
||||
}
|
||||
|
||||
.header-md.header-collapse-condense {
|
||||
.header-collapse-condense {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,8 +126,9 @@ export class InputPasswordToggle implements ComponentInterface {
|
||||
color={color}
|
||||
fill="clear"
|
||||
shape="round"
|
||||
aria-checked={isPasswordVisible ? 'true' : 'false'}
|
||||
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
|
||||
aria-pressed={isPasswordVisible ? 'true' : 'false'}
|
||||
role="switch"
|
||||
type="button"
|
||||
onPointerDown={(ev) => {
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
});
|
||||
|
||||
test.describe(title('input password toggle: aria attributes'), () => {
|
||||
test('should have correct aria attributes on load', async ({ page }) => {
|
||||
test('should inherit aria attributes to inner button 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-pressed', 'false');
|
||||
await expect(nativeButton).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
test('should update aria attributes after toggle', async ({ page }) => {
|
||||
test('should inherit aria attributes to inner button 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-pressed', 'true');
|
||||
await expect(nativeButton).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1116,11 +1116,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
private handleViewTransition() {
|
||||
// Only run view transitions when the modal is presented
|
||||
if (!this.presented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPortrait = window.innerWidth < 768;
|
||||
|
||||
// Only transition if view state actually changed
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('card modal: viewport resize'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Start in portrait mode (mobile)
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-app>
|
||||
<div class="ion-page" id="main-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card Viewport Resize Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<p>This page tests that viewport resize does not trigger card modal animation when modal is closed.</p>
|
||||
<ion-button id="open-modal">Open Card Modal</ion-button>
|
||||
<ion-modal id="card-modal">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card Modal</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button id="close-modal">Close</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<p>Modal content</p>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const modal = document.querySelector('#card-modal');
|
||||
const mainPage = document.querySelector('#main-page');
|
||||
modal.presentingElement = mainPage;
|
||||
|
||||
document.querySelector('#open-modal').addEventListener('click', () => {
|
||||
modal.present();
|
||||
});
|
||||
|
||||
document.querySelector('#close-modal').addEventListener('click', () => {
|
||||
modal.dismiss();
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
});
|
||||
|
||||
test('should not animate presenting element when viewport resizes and modal is closed', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30679',
|
||||
});
|
||||
|
||||
const mainPage = page.locator('#main-page');
|
||||
|
||||
// Verify the presenting element has no transform initially
|
||||
const initialTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(initialTransform).toBe('none');
|
||||
|
||||
// Resize from portrait to landscape (crossing the 768px threshold)
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
|
||||
// Wait for the debounced resize handler (50ms) plus some buffer
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// The presenting element should still have no transform
|
||||
// If the bug exists, it would have scale(0.93) or similar applied
|
||||
const afterResizeTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(afterResizeTransform).toBe('none');
|
||||
});
|
||||
|
||||
test('should not animate presenting element when resizing multiple times with modal closed', async ({ page }) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
|
||||
// Multiple resize cycles should not trigger the animation
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Portrait to landscape
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
let transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
|
||||
// Landscape to portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
}
|
||||
});
|
||||
|
||||
test('should still animate presenting element correctly when modal is open and viewport resizes', async ({
|
||||
page,
|
||||
}) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
// Open the modal
|
||||
await page.click('#open-modal');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// When modal is open in portrait, presenting element should be transformed
|
||||
let transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
// The presenting element should have a scale transform when modal is open
|
||||
expect(transform).not.toBe('none');
|
||||
|
||||
// Resize to landscape while modal is open
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// The modal transitions correctly - in landscape mode the presenting element
|
||||
// should have different (or no) transform than portrait
|
||||
transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
|
||||
// Note: The exact transform depends on the landscape handling
|
||||
// The main point is that when modal IS open, the transition should work
|
||||
// This test just ensures we don't break existing functionality
|
||||
});
|
||||
|
||||
test('presenting element should return to normal after modal is dismissed', async ({ page }) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
// Open the modal
|
||||
await page.click('#open-modal');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// Close the modal
|
||||
await page.click('#close-modal');
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
// Wait for animations to complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// The presenting element should be back to normal
|
||||
const transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
|
||||
// Now resize the viewport - should not trigger animation
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
const afterResizeTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(afterResizeTransform).toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,58 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,21 +52,4 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,12 +252,10 @@ html.plt-ios.plt-hybrid, html.plt-ios.plt-pwa {
|
||||
|
||||
@supports (padding-top: env(safe-area-inset-top)) {
|
||||
html {
|
||||
// `--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));
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -473,9 +473,7 @@ export const getPresentedOverlay = (
|
||||
id?: string
|
||||
): HTMLIonOverlayElement | undefined => {
|
||||
const overlays = getPresentedOverlays(doc, overlayTag);
|
||||
// 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];
|
||||
return id === undefined ? overlays[overlays.length - 1] : overlays.find((o) => o.id === id);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"core",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "8.7.16"
|
||||
"version": "8.7.12"
|
||||
}
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
22
packages/angular-server/package-lock.json
generated
22
packages/angular-server/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16"
|
||||
"@ionic/core": "^8.7.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/eslint-plugin": "^16.0.0",
|
||||
@@ -1031,9 +1031,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -1041,7 +1041,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -7309,9 +7309,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"requires": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
@@ -11289,4 +11289,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "Angular SSR Module for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -62,6 +62,6 @@
|
||||
},
|
||||
"prettier": "@ionic/prettier-config",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16"
|
||||
"@ionic/core": "^8.7.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
16
packages/angular/package-lock.json
generated
16
packages/angular/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"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.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -1408,7 +1408,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -9095,4 +9095,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "Angular specific wrappers for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"ionicons": "^8.0.13",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
6
packages/docs/package-lock.json
generated
6
packages/docs/package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@ionic/docs",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/docs",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/docs",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "Pre-packaged API documentation for the Ionic docs.",
|
||||
"main": "core.json",
|
||||
"types": "core.d.ts",
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/react-router
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/react-router
|
||||
|
||||
38
packages/react-router/package-lock.json
generated
38
packages/react-router/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/react-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/react-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/react": "^8.7.16",
|
||||
"@ionic/react": "^8.7.12",
|
||||
"tslib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -238,9 +238,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -248,7 +248,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -418,12 +418,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/react": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.15.tgz",
|
||||
"integrity": "sha512-3W5DAN3OMe2w32AGlW8zvKZHNY/dLCWhVLL9485VKtsYvgUc9nkG8dYDvQGc6c17PEDVrMy+IAl4H9hLgJhYnQ==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.12.tgz",
|
||||
"integrity": "sha512-gNm5L++aiwkwJrUFKhcHUUgjqnj9n03gK7UcoL7Oz+271arzmwF/FNd47G85b6PovwiYQXY2CUBvNJ7Nh4qE/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"@ionic/core": "8.7.12",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "*"
|
||||
},
|
||||
@@ -4178,9 +4178,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"requires": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
@@ -4284,11 +4284,11 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@ionic/react": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.15.tgz",
|
||||
"integrity": "sha512-3W5DAN3OMe2w32AGlW8zvKZHNY/dLCWhVLL9485VKtsYvgUc9nkG8dYDvQGc6c17PEDVrMy+IAl4H9hLgJhYnQ==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.12.tgz",
|
||||
"integrity": "sha512-gNm5L++aiwkwJrUFKhcHUUgjqnj9n03gK7UcoL7Oz+271arzmwF/FNd47G85b6PovwiYQXY2CUBvNJ7Nh4qE/A==",
|
||||
"requires": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"@ionic/core": "8.7.12",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "*"
|
||||
}
|
||||
@@ -6847,4 +6847,4 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/react-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "React Router wrapper for @ionic/react",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -36,7 +36,7 @@
|
||||
"dist/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ionic/react": "^8.7.16",
|
||||
"@ionic/react": "^8.7.12",
|
||||
"tslib": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -3,41 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/react
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
16
packages/react/package-lock.json
generated
16
packages/react/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/react",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/react",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "*"
|
||||
},
|
||||
@@ -736,9 +736,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -746,7 +746,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -11916,4 +11916,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/react",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"description": "React specific wrapper for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -40,7 +40,7 @@
|
||||
"css/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "*"
|
||||
},
|
||||
|
||||
@@ -40,20 +40,6 @@ 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>;
|
||||
|
||||
@@ -93,7 +79,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
const tabKeys = Object.keys(tabs);
|
||||
const activeTab = tabKeys.find((key) => {
|
||||
const href = tabs[key].originalHref;
|
||||
return matchesTab(this.props.routeInfo!.pathname, href);
|
||||
return this.props.routeInfo!.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
if (activeTab) {
|
||||
@@ -135,7 +121,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 matchesTab(props.routeInfo!.pathname, href);
|
||||
return props.routeInfo!.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
// Check to see if the tab button href has changed, and if so, update it in the tabs state
|
||||
|
||||
@@ -28,7 +28,6 @@ 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';
|
||||
@@ -68,7 +67,6 @@ 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} />
|
||||
|
||||
@@ -46,9 +46,6 @@ 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>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
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;
|
||||
@@ -1,45 +1,4 @@
|
||||
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');
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/vue-router
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/vue-router
|
||||
|
||||
38
packages/vue-router/package-lock.json
generated
38
packages/vue-router/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/vue-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/vue-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/vue": "^8.7.16"
|
||||
"@ionic/vue": "^8.7.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
@@ -673,9 +673,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -683,7 +683,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -868,12 +868,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/vue": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.15.tgz",
|
||||
"integrity": "sha512-F94GoGBsLoFFkJLe+W401SynIfUzEL1Z+Y4YUQ2EGpulnFpY4tXKzDhRNrisPnM0k3BWQRh8AA8fwTtMqEiISw==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.12.tgz",
|
||||
"integrity": "sha512-fH/acQ7dYYd1XY1HFqKf0Th6klbfNNzhlnYUf9kNQpkHqUpMamSzA8TWDOV5f4PNKYq6X4oKAPPeIQA4DOayUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"@ionic/core": "8.7.12",
|
||||
"@stencil/vue-output-target": "0.10.7",
|
||||
"ionicons": "^8.0.13"
|
||||
}
|
||||
@@ -8044,9 +8044,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"requires": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
@@ -8159,11 +8159,11 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@ionic/vue": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.15.tgz",
|
||||
"integrity": "sha512-F94GoGBsLoFFkJLe+W401SynIfUzEL1Z+Y4YUQ2EGpulnFpY4tXKzDhRNrisPnM0k3BWQRh8AA8fwTtMqEiISw==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.12.tgz",
|
||||
"integrity": "sha512-fH/acQ7dYYd1XY1HFqKf0Th6klbfNNzhlnYUf9kNQpkHqUpMamSzA8TWDOV5f4PNKYq6X4oKAPPeIQA4DOayUQ==",
|
||||
"requires": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"@ionic/core": "8.7.12",
|
||||
"@stencil/vue-output-target": "0.10.7",
|
||||
"ionicons": "^8.0.13"
|
||||
}
|
||||
@@ -12994,4 +12994,4 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/vue-router",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"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.16"
|
||||
"@ionic/vue": "^8.7.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
|
||||
@@ -3,41 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/vue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/vue
|
||||
|
||||
16
packages/vue/package-lock.json
generated
16
packages/vue/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/vue",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/vue",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"@stencil/vue-output-target": "0.10.7",
|
||||
"ionicons": "^8.0.13"
|
||||
},
|
||||
@@ -222,9 +222,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"version": "8.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
|
||||
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
@@ -232,7 +232,7 @@
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core/node_modules/tslib": {
|
||||
@@ -4022,4 +4022,4 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/vue",
|
||||
"version": "8.7.16",
|
||||
"version": "8.7.12",
|
||||
"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.16",
|
||||
"@ionic/core": "^8.7.12",
|
||||
"@stencil/vue-output-target": "0.10.7",
|
||||
"ionicons": "^8.0.13"
|
||||
},
|
||||
|
||||
@@ -24,23 +24,6 @@ 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) => {
|
||||
@@ -152,9 +135,7 @@ export const IonTabBar = defineComponent({
|
||||
const tabKeys = Object.keys(tabs);
|
||||
let activeTab = tabKeys.find((key) => {
|
||||
const href = tabs[key].originalHref;
|
||||
return (
|
||||
currentRoute?.pathname && matchesTab(currentRoute.pathname, href)
|
||||
);
|
||||
return currentRoute?.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -165,28 +165,6 @@ 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({
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
<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>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,54 +0,0 @@
|
||||
<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>
|
||||
@@ -1,45 +1,4 @@
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user