Compare commits

..

1 Commits

Author SHA1 Message Date
ShaneK
f332f62cbd fix(sheet): disable focus trap with string-based logic as well 2025-10-02 06:38:43 -07:00
106 changed files with 434 additions and 2113 deletions

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic Angular Server'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic Angular'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -9,9 +9,9 @@ runs:
using: 'composite'
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- name: Install Dependencies
run: npm ci

View File

@@ -9,9 +9,9 @@ runs:
using: 'composite'
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- name: Install Dependencies
run: npm install
working-directory: ./core

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic React Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic React'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -3,9 +3,9 @@ description: 'Builds Ionic Vue Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic Vue'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -10,7 +10,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v5
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -19,9 +19,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
# Provenance requires npm 9.5.0+
- name: Install latest npm
run: npm install -g npm@latest

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -3,9 +3,9 @@ description: 'Test Core Clean Build'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:

View File

@@ -3,9 +3,9 @@ description: 'Test Core Lint'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- name: Install Dependencies
run: npm ci
working-directory: ./core

View File

@@ -13,9 +13,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core
@@ -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@v5
uses: actions/upload-artifact@v4
with:
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- name: Install Dependencies
run: npm ci
working-directory: ./core

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core

View File

@@ -7,10 +7,10 @@ on:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24.x
- uses: actions/download-artifact@v6
node-version: 22.x
- uses: actions/download-artifact@v5
with:
path: ./artifacts
- name: Extract Archives

View File

@@ -13,7 +13,7 @@ runs:
- name: Create Archive
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
shell: bash
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.output }}

View File

@@ -15,7 +15,7 @@ jobs:
security-events: write
steps:
- uses: actions/checkout@v5
- uses: github/codeql-action/init@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/analyze@v3

View File

@@ -225,35 +225,3 @@ jobs:
- name: Check build matrix status
if: ${{ needs.test-react-e2e.result != 'success' }}
run: exit 1
send-success-messages:
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
runs-on: ubuntu-latest
if: ${{ !cancelled() && contains(needs.*.result, 'success') }}
steps:
- name: Notify success on Discord
run: |
curl -H "Content-Type:application/json" \
-d '{"embeds": [{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "color": 65280, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
- name: Notify success on Slack
run: |
curl -H "Content-Type:application/json" \
-d '{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
${{secrets.SLACK_NOTIFY_SUCCESS_WEBHOOK}}
send-failure-messages:
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
runs-on: ubuntu-latest
if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
steps:
- name: Notify failure on Discord
run: |
curl -H "Content-Type:application/json" \
-d '{"content": "Alerting <@&1347593178580254761>!", "embeds": [{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "color": 16711680, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
- name: Notify failure on Slack
run: |
curl -H "Content-Type:application/json" \
-d '{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
${{secrets.SLACK_NOTIFY_FAILURE_WEBHOOK}}

View File

@@ -3,52 +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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
### Bug Fixes
* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613)
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
### Bug Fixes
* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab))
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
### Bug Fixes
* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b))
* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326)
* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367))
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
### Bug Fixes
* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611)
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)

View File

@@ -3,52 +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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
### Bug Fixes
* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613)
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
### Bug Fixes
* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab))
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
### Bug Fixes
* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b))
* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326)
* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367))
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
### Bug Fixes
* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611)
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)

View File

@@ -1,5 +1,5 @@
# Get Playwright
FROM mcr.microsoft.com/playwright:v1.56.1
FROM mcr.microsoft.com/playwright:v1.55.1
# Set the working directory
WORKDIR /ionic

109
core/package-lock.json generated
View File

@@ -1,20 +1,20 @@
{
"name": "@ionic/core",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.0",
"@axe-core/playwright": "^4.10.2",
"@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
@@ -22,7 +22,7 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.56.1",
"@playwright/test": "^1.55.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.10.0",
@@ -57,12 +57,12 @@
"dev": true
},
"node_modules/@axe-core/playwright": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz",
"integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==",
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz",
"integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==",
"dev": true,
"dependencies": {
"axe-core": "~4.11.0"
"axe-core": "~4.10.3"
},
"peerDependencies": {
"playwright-core": ">= 1.0.0"
@@ -663,9 +663,9 @@
"dev": true
},
"node_modules/@capacitor/core": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.3.tgz",
"integrity": "sha512-wCWr8fQ9Wxn0466vPg7nMn0tivbNVjNy1yL4GvDSIZuZx7UpU2HeVGNe9QjN/quEd+YLRFeKEBLBw619VqUiNg==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
@@ -1715,12 +1715,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
"integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
"dev": true,
"dependencies": {
"playwright": "1.56.1"
"playwright": "1.55.1"
},
"bin": {
"playwright": "cli.js"
@@ -1914,9 +1914,10 @@
}
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
},
@@ -3033,9 +3034,9 @@
}
},
"node_modules/axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
"integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
"dev": true,
"engines": {
"node": ">=4"
@@ -8592,12 +8593,12 @@
}
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"dev": true,
"dependencies": {
"playwright-core": "1.56.1"
"playwright-core": "1.55.1"
},
"bin": {
"playwright": "cli.js"
@@ -8610,9 +8611,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -10652,12 +10653,12 @@
},
"dependencies": {
"@axe-core/playwright": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz",
"integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==",
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz",
"integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==",
"dev": true,
"requires": {
"axe-core": "~4.11.0"
"axe-core": "~4.10.3"
}
},
"@babel/code-frame": {
@@ -11101,9 +11102,9 @@
"dev": true
},
"@capacitor/core": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.3.tgz",
"integrity": "sha512-wCWr8fQ9Wxn0466vPg7nMn0tivbNVjNy1yL4GvDSIZuZx7UpU2HeVGNe9QjN/quEd+YLRFeKEBLBw619VqUiNg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
@@ -11862,12 +11863,12 @@
}
},
"@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
"integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
"dev": true,
"requires": {
"playwright": "1.56.1"
"playwright": "1.55.1"
}
},
"@rollup/plugin-node-resolve": {
@@ -11983,9 +11984,9 @@
"requires": {}
},
"@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -12770,9 +12771,9 @@
}
},
"axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
"integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
"dev": true
},
"babel-jest": {
@@ -16811,19 +16812,19 @@
}
},
"playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.56.1"
"playwright-core": "1.55.1"
}
},
"playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"dev": true
},
"postcss": {
@@ -18337,4 +18338,4 @@
"dev": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "8.7.9",
"version": "8.7.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,12 +31,12 @@
"loader/"
],
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.0",
"@axe-core/playwright": "^4.10.2",
"@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
@@ -44,7 +44,7 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.56.1",
"@playwright/test": "^1.55.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.10.0",

View File

@@ -38,40 +38,7 @@ const enum AccordionState {
})
export class Accordion implements ComponentInterface {
private accordionGroupEl?: HTMLIonAccordionGroupElement | null;
private accordionGroupUpdateHandler = () => {
/**
* Determine if this update will cause an actual state change.
* We only want to mark as "interacted" if the state is changing.
*/
const accordionGroup = this.accordionGroupEl;
if (accordionGroup) {
const value = accordionGroup.value;
const accordionValue = this.value;
const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
const isExpanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding;
const stateWillChange = shouldExpand !== isExpanded;
/**
* Only mark as interacted if:
* 1. This is not the first update we've received with a defined value
* 2. The state is actually changing (prevents redundant updates from enabling animations)
*/
if (this.hasReceivedFirstUpdate && stateWillChange) {
this.hasInteracted = true;
}
/**
* Only count this as the first update if the group value is defined.
* This prevents the initial undefined value from the group's componentDidLoad
* from being treated as the first real update.
*/
if (value !== undefined) {
this.hasReceivedFirstUpdate = true;
}
}
this.updateState();
};
private updateListener = () => this.updateState(false);
private contentEl: HTMLDivElement | undefined;
private contentElWrapper: HTMLDivElement | undefined;
private headerEl: HTMLDivElement | undefined;
@@ -83,25 +50,6 @@ export class Accordion implements ComponentInterface {
@State() state: AccordionState = AccordionState.Collapsed;
@State() isNext = false;
@State() isPrevious = false;
/**
* Tracks whether a user-initiated interaction has occurred.
* Animations are disabled until the first interaction happens.
* This prevents the accordion from animating when it's programmatically
* set to an expanded or collapsed state on initial load.
*/
@State() hasInteracted = false;
/**
* Tracks if this accordion has ever been expanded.
* Used to prevent the first expansion from animating.
*/
private hasEverBeenExpanded = false;
/**
* Tracks if this accordion has received its first update from the group.
* Used to distinguish initial programmatic sets from user interactions.
*/
private hasReceivedFirstUpdate = false;
/**
* The value of the accordion. Defaults to an autogenerated
@@ -140,15 +88,15 @@ export class Accordion implements ComponentInterface {
connectedCallback() {
const accordionGroupEl = (this.accordionGroupEl = this.el?.closest('ion-accordion-group'));
if (accordionGroupEl) {
this.updateState();
addEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
this.updateState(true);
addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
}
}
disconnectedCallback() {
const accordionGroupEl = this.accordionGroupEl;
if (accordionGroupEl) {
removeEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
}
}
@@ -264,16 +212,10 @@ export class Accordion implements ComponentInterface {
ionItem.appendChild(iconEl);
};
private expandAccordion = () => {
private expandAccordion = (initialUpdate = false) => {
const { contentEl, contentElWrapper } = this;
/**
* If the content elements aren't available yet, just set the state.
* This happens on initial render before the DOM is ready.
*/
if (contentEl === undefined || contentElWrapper === undefined) {
if (initialUpdate || contentEl === undefined || contentElWrapper === undefined) {
this.state = AccordionState.Expanded;
this.hasEverBeenExpanded = true;
return;
}
@@ -285,12 +227,6 @@ export class Accordion implements ComponentInterface {
cancelAnimationFrame(this.currentRaf);
}
/**
* Mark that this accordion has been expanded at least once.
* This allows subsequent expansions to animate.
*/
this.hasEverBeenExpanded = true;
if (this.shouldAnimate()) {
raf(() => {
this.state = AccordionState.Expanding;
@@ -311,14 +247,9 @@ export class Accordion implements ComponentInterface {
}
};
private collapseAccordion = () => {
private collapseAccordion = (initialUpdate = false) => {
const { contentEl } = this;
/**
* If the content element isn't available yet, just set the state.
* This happens on initial render before the DOM is ready.
*/
if (contentEl === undefined) {
if (initialUpdate || contentEl === undefined) {
this.state = AccordionState.Collapsed;
return;
}
@@ -360,19 +291,6 @@ export class Accordion implements ComponentInterface {
* of what is set in the config.
*/
private shouldAnimate = () => {
/**
* Don't animate until after the first user interaction.
* This prevents animations on initial load when accordions
* start in an expanded or collapsed state programmatically.
*
* Additionally, don't animate the very first expansion even if
* hasInteracted is true. This handles edge cases like React StrictMode
* where effects run twice and might incorrectly mark as interacted.
*/
if (!this.hasInteracted || !this.hasEverBeenExpanded) {
return false;
}
if (typeof (window as any) === 'undefined') {
return false;
}
@@ -394,7 +312,7 @@ export class Accordion implements ComponentInterface {
return true;
};
private updateState = async () => {
private updateState = async (initialUpdate = false) => {
const accordionGroup = this.accordionGroupEl;
const accordionValue = this.value;
@@ -407,10 +325,10 @@ export class Accordion implements ComponentInterface {
const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
if (shouldExpand) {
this.expandAccordion();
this.expandAccordion(initialUpdate);
this.isNext = this.isPrevious = false;
} else {
this.collapseAccordion();
this.collapseAccordion(initialUpdate);
/**
* When using popout or inset,
@@ -468,12 +386,6 @@ export class Accordion implements ComponentInterface {
if (disabled || readonly) return;
/**
* Mark that the user has interacted with the accordion.
* This enables animations for all future state changes.
*/
this.hasInteracted = true;
if (accordionGroupEl) {
/**
* Because the accordion group may or may

View File

@@ -200,87 +200,6 @@ it('should set default values if not provided', async () => {
expect(accordion.classList.contains('accordion-collapsed')).toEqual(false);
});
it('should not animate when initial value is set before load', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
});
const accordionGroup = page.doc.createElement('ion-accordion-group');
accordionGroup.innerHTML = `
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
`;
accordionGroup.value = 'first';
page.body.appendChild(accordionGroup);
await page.waitForChanges();
const firstAccordion = accordionGroup.querySelector('ion-accordion[value="first"]')!;
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
expect(firstAccordion.classList.contains('accordion-expanding')).toEqual(false);
});
it('should not animate when initial value is set after load', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
});
const accordionGroup = page.doc.createElement('ion-accordion-group');
accordionGroup.innerHTML = `
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
<ion-accordion value="second">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
`;
page.body.appendChild(accordionGroup);
await page.waitForChanges();
accordionGroup.value = 'first';
await page.waitForChanges();
const firstAccordion = accordionGroup.querySelector('ion-accordion[value="first"]')!;
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
expect(firstAccordion.classList.contains('accordion-expanding')).toEqual(false);
});
it('should not have animated class on first expansion', async () => {
const page = await newSpecPage({
components: [Item, Accordion, AccordionGroup],
html: `
<ion-accordion-group>
<ion-accordion value="first">
<ion-item slot="header">Label</ion-item>
<div slot="content">Content</div>
</ion-accordion>
</ion-accordion-group>
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const firstAccordion = page.body.querySelector('ion-accordion[value="first"]')!;
// First expansion should not have the animated class
accordionGroup.value = 'first';
await page.waitForChanges();
expect(firstAccordion.classList.contains('accordion-animated')).toEqual(false);
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
});
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/27047
it('should not have animated class when animated="false"', async () => {
const page = await newSpecPage({

View File

@@ -361,7 +361,11 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
target,
};
let fill = this.fill;
if (fill === undefined) {
/**
* We check both undefined and null to
* work around https://github.com/ionic-team/stencil/issues/3586.
*/
if (fill == null) {
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
}

View File

@@ -34,6 +34,7 @@ export class Checkbox implements ComponentInterface {
private inputLabelId = `${this.inputId}-lbl`;
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
private focusEl?: HTMLElement;
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLIonCheckboxElement;
@@ -146,7 +147,9 @@ export class Checkbox implements ComponentInterface {
/** @internal */
@Method()
async setFocus() {
this.el.focus();
if (this.focusEl) {
this.focusEl.focus();
}
}
/**
@@ -166,6 +169,7 @@ export class Checkbox implements ComponentInterface {
private toggleChecked = (ev: Event) => {
ev.preventDefault();
this.setFocus();
this.setChecked(!this.checked);
this.indeterminate = false;
};
@@ -281,9 +285,6 @@ export class Checkbox implements ComponentInterface {
aria-disabled={disabled ? 'true' : null}
tabindex={disabled ? undefined : 0}
onKeyDown={this.onKeyDown}
onFocus={this.onFocus}
onBlur={this.onBlur}
onClick={this.onClick}
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
@@ -295,6 +296,7 @@ export class Checkbox implements ComponentInterface {
[`checkbox-alignment-${alignment}`]: alignment !== undefined,
[`checkbox-label-placement-${labelPlacement}`]: true,
})}
onClick={this.onClick}
>
<label class="checkbox-wrapper" htmlFor={inputId}>
{/*
@@ -307,6 +309,9 @@ export class Checkbox implements ComponentInterface {
disabled={disabled}
id={inputId}
onChange={this.toggleChecked}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
required={required}
{...inheritedAttributes}
/>

View File

@@ -44,10 +44,7 @@ configs().forEach(({ title, screenshot, config }) => {
});
});
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('checkbox: ionChange'), () => {
test('should fire ionChange when interacting with checkbox', async ({ page }) => {
await page.setContent(
@@ -136,195 +133,4 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
expect(clickCount).toBe(1);
});
});
test.describe(title('checkbox: ionFocus'), () => {
test('should not have visual regressions', async ({ page, pageUtils }) => {
await page.setContent(
`
<style>
#container {
display: inline-block;
padding: 10px;
}
</style>
<div id="container">
<ion-checkbox>Unchecked</ion-checkbox>
</div>
`,
config
);
await pageUtils.pressKeys('Tab');
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`checkbox-focus`));
});
test('should not have visual regressions when interacting with checkbox in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item class="ion-focused">
<ion-checkbox>Unchecked</ion-checkbox>
</ion-item>
`,
config
);
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
const item = page.locator('ion-item');
await expect(item).toHaveScreenshot(screenshot(`checkbox-in-item-focus`));
});
test('should fire ionFocus when checkbox is focused', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
expect(ionFocus).toHaveReceivedEventTimes(1);
// Reset focus
const checkbox = page.locator('ion-checkbox');
const checkboxBoundingBox = (await checkbox.boundingBox())!;
await page.mouse.click(0, checkboxBoundingBox.height + 1);
// Test focus with click
await checkbox.click();
expect(ionFocus).toHaveReceivedEventTimes(2);
});
test('should fire ionFocus when interacting with checkbox in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item>
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
</ion-item>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
expect(ionFocus).toHaveReceivedEventTimes(1);
// Verify that the event target is the checkbox and not the item
const eventByKeyboard = ionFocus.events[0];
expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
// Reset focus
const checkbox = page.locator('ion-checkbox');
const checkboxBoundingBox = (await checkbox.boundingBox())!;
await page.mouse.click(0, checkboxBoundingBox.height + 1);
// Test focus with click
const item = page.locator('ion-item');
await item.click();
expect(ionFocus).toHaveReceivedEventTimes(2);
// Verify that the event target is the checkbox and not the item
const eventByClick = ionFocus.events[0];
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
});
test('should not fire when programmatically setting a value', async ({ page }) => {
await page.setContent(
`
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
const checkbox = page.locator('ion-checkbox');
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
expect(ionFocus).not.toHaveReceivedEvent();
});
});
test.describe(title('checkbox: ionBlur'), () => {
test('should fire ionBlur when checkbox is blurred', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
`,
config
);
const ionBlur = await page.spyOnEvent('ionBlur');
// Test blur with keyboard navigation
// Focus the checkbox
await pageUtils.pressKeys('Tab');
// Blur the checkbox
await pageUtils.pressKeys('Tab');
expect(ionBlur).toHaveReceivedEventTimes(1);
// Test blur with click
const checkbox = page.locator('ion-checkbox');
// Focus the checkbox
await checkbox.click();
// Blur the checkbox by clicking outside of it
const checkboxBoundingBox = (await checkbox.boundingBox())!;
await page.mouse.click(0, checkboxBoundingBox.height + 1);
expect(ionBlur).toHaveReceivedEventTimes(2);
});
test('should fire ionBlur after interacting with checkbox in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item>
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
</ion-item>
`,
config
);
const ionBlur = await page.spyOnEvent('ionBlur');
// Test blur with keyboard navigation
// Focus the checkbox
await pageUtils.pressKeys('Tab');
// Blur the checkbox
await pageUtils.pressKeys('Tab');
expect(ionBlur).toHaveReceivedEventTimes(1);
// Verify that the event target is the checkbox and not the item
const event = ionBlur.events[0];
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
// Test blur with click
const item = page.locator('ion-item');
await item.click();
// Blur the checkbox by clicking outside of it
const itemBoundingBox = (await item.boundingBox())!;
await page.mouse.click(0, itemBoundingBox.height + 1);
expect(ionBlur).toHaveReceivedEventTimes(2);
// Verify that the event target is the checkbox and not the item
const eventByClick = ionBlur.events[0];
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -50,20 +50,6 @@
<ion-checkbox checked style="width: 200px">Specified width</ion-checkbox><br />
<ion-checkbox checked style="width: 100%">Full-width</ion-checkbox><br />
</ion-content>
<script>
document.addEventListener('ionBlur', (ev) => {
console.log('ionBlur', ev);
});
document.addEventListener('ionChange', (ev) => {
console.log('ionChange', ev);
});
document.addEventListener('ionFocus', (ev) => {
console.log('ionFocus', ev);
});
</script>
</ion-app>
</body>
</html>

View File

@@ -246,20 +246,6 @@
</div>
</div>
</ion-content>
<script>
document.addEventListener('ionBlur', (ev) => {
console.log('ionBlur', ev);
});
document.addEventListener('ionChange', (ev) => {
console.log('ionChange', ev);
});
document.addEventListener('ionFocus', (ev) => {
console.log('ionFocus', ev);
});
</script>
</ion-app>
</body>
</html>

View File

@@ -39,15 +39,6 @@
--opacity-scale: inherit;
}
/**
* Override styles applied during the page transition to prevent
* header flickering.
*/
.header-collapse-fade.header-transitioning ion-toolbar {
--background: transparent;
--border-style: none;
}
// iOS Header - Collapse Condense
// --------------------------------------------------
.header-collapse-condense {
@@ -74,6 +65,8 @@
* since it needs to blend in with the header above it.
*/
.header-collapse-condense ion-toolbar {
--background: var(--ion-background-color, #fff);
z-index: 0;
}
@@ -100,28 +93,6 @@
transition: all 0.2s ease-in-out;
}
/**
* Large title toolbar should just use the content background
* since it needs to blend in with the header above it.
*/
.header-collapse-condense ion-toolbar,
/**
* Override styles applied during the page transition to prevent
* header flickering.
*/
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
--background: var(--ion-background-color, #fff);
}
/**
* Override styles applied during the page transition to prevent
* header flickering.
*/
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
--border-style: none;
--opacity-scale: 1;
}
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
opacity: 0;

View File

@@ -15,7 +15,6 @@ import {
handleToolbarIntersection,
setHeaderActive,
setToolbarBackgroundOpacity,
getRoleType,
} from './header.utils';
/**
@@ -209,10 +208,9 @@ export class Header implements ComponentInterface {
const { translucent, inheritedAttributes } = this;
const mode = getIonMode(this);
const collapse = this.collapse || 'none';
const isCondensed = collapse === 'condense';
// banner role must be at top level, so remove role if inside a menu
const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode);
const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner';
return (
<Host

View File

@@ -2,8 +2,6 @@ import { readTask, writeTask } from '@stencil/core';
import { clamp } from '@utils/helpers';
const TRANSITION = 'all 0.2s ease-in-out';
const ROLE_NONE = 'none';
const ROLE_BANNER = 'banner';
interface HeaderIndex {
el: HTMLIonHeaderElement;
@@ -173,7 +171,6 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
if (active) {
headerEl.setAttribute('role', ROLE_BANNER);
headerEl.classList.remove('header-collapse-condense-inactive');
ionTitles.forEach((ionTitle) => {
@@ -182,16 +179,6 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
}
});
} else {
/**
* There can only be one banner landmark per page.
* By default, all ion-headers have the banner role.
* This causes an accessibility issue when using a
* condensed header since there are two ion-headers
* on the page at once (active and inactive).
* To solve this, the role needs to be toggled
* based on which header is active.
*/
headerEl.setAttribute('role', ROLE_NONE);
headerEl.classList.add('header-collapse-condense-inactive');
/**
@@ -257,28 +244,3 @@ export const handleHeaderFade = (scrollEl: HTMLElement, baseEl: HTMLElement, con
});
});
};
/**
* Get the role type for the ion-header.
*
* @param isInsideMenu If ion-header is inside ion-menu.
* @param isCondensed If ion-header has collapse="condense".
* @param mode The current mode.
* @returns 'none' if inside ion-menu or if condensed in md
* mode, otherwise 'banner'.
*/
export const getRoleType = (isInsideMenu: boolean, isCondensed: boolean, mode: 'ios' | 'md') => {
// If the header is inside a menu, it should not have the banner role.
if (isInsideMenu) {
return ROLE_NONE;
}
/**
* Only apply role="none" to `md` mode condensed headers
* since the large header is never shown.
*/
if (isCondensed && mode === 'md') {
return ROLE_NONE;
}
// Default to banner role.
return ROLE_BANNER;
};

View File

@@ -40,45 +40,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
});
test('should only have the banner role on the active header', async ({ page }) => {
await page.goto('/src/components/header/test/condense', config);
const largeTitleHeader = page.locator('#largeTitleHeader');
const smallTitleHeader = page.locator('#smallTitleHeader');
const content = page.locator('ion-content');
await expect(largeTitleHeader).toHaveAttribute('role', 'banner');
await expect(smallTitleHeader).toHaveAttribute('role', 'none');
await content.evaluate(async (el: HTMLIonContentElement) => {
await el.scrollToBottom();
});
await page.locator('#largeTitleHeader.header-collapse-condense-inactive').waitFor();
await expect(largeTitleHeader).toHaveAttribute('role', 'none');
await expect(smallTitleHeader).toHaveAttribute('role', 'banner');
});
});
});
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('header: condense'), () => {
test('should only have the banner role on the small header', async ({ page }) => {
await page.goto('/src/components/header/test/condense', config);
const largeTitleHeader = page.locator('#largeTitleHeader');
const smallTitleHeader = page.locator('#smallTitleHeader');
const content = page.locator('ion-content');
await expect(smallTitleHeader).toHaveAttribute('role', 'banner');
await expect(largeTitleHeader).toHaveAttribute('role', 'none');
await content.evaluate(async (el: HTMLIonContentElement) => {
await el.scrollToBottom();
});
await page.waitForChanges();
await expect(smallTitleHeader).toHaveAttribute('role', 'banner');
await expect(largeTitleHeader).toHaveAttribute('role', 'none');
});
});
});

View File

@@ -14,7 +14,7 @@ import {
h,
} from '@stencil/core';
import type { NotchController } from '@utils/forms';
import { createNotchController, checkInvalidState } from '@utils/forms';
import { createNotchController } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
import { createSlotMutationController } from '@utils/slot-mutation-controller';
@@ -403,6 +403,16 @@ export class Input implements ComponentInterface {
};
}
/**
* Checks if the input is in an invalid state based on Ionic validation classes
*/
private checkInvalidState(): boolean {
const hasIonTouched = this.el.classList.contains('ion-touched');
const hasIonInvalid = this.el.classList.contains('ion-invalid');
return hasIonTouched && hasIonInvalid;
}
connectedCallback() {
const { el } = this;
@@ -416,7 +426,7 @@ export class Input implements ComponentInterface {
// Watch for class changes to update validation state
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
this.validationObserver = new MutationObserver(() => {
const newIsInvalid = checkInvalidState(el);
const newIsInvalid = this.checkInvalidState();
if (this.isInvalid !== newIsInvalid) {
this.isInvalid = newIsInvalid;
// Force a re-render to update aria-describedby immediately
@@ -431,7 +441,7 @@ export class Input implements ComponentInterface {
}
// Always set initial state
this.isInvalid = checkInvalidState(el);
this.isInvalid = this.checkInvalidState();
this.debounceChanged();
if (Build.isBrowser) {

View File

@@ -98,7 +98,11 @@ export const createSheetGesture = (
// Respect explicit opt-out of focus trapping/backdrop interactions
// If focusTrap is false or showBackdrop is false, do not enable the backdrop or re-enable focus trap
const el = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean };
if (el.focusTrap === false || el.showBackdrop === false) {
const focusTrapAttr = el.getAttribute?.('focus-trap');
const showBackdropAttr = el.getAttribute?.('show-backdrop');
const focusTrapDisabled = el.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = el.showBackdrop === false || showBackdropAttr === 'false';
if (focusTrapDisabled || backdropDisabled) {
return;
}
baseEl.style.setProperty('pointer-events', 'auto');
@@ -241,10 +245,12 @@ export const createSheetGesture = (
* ion-backdrop and .modal-wrapper always have pointer-events: auto
* applied, so the modal content can still be interacted with.
*/
const shouldEnableBackdrop =
currentBreakpoint > backdropBreakpoint &&
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
const modalEl = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean };
const focusTrapAttr = modalEl.getAttribute?.('focus-trap');
const showBackdropAttr = modalEl.getAttribute?.('show-backdrop');
const focusTrapDisabled = modalEl.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = modalEl.showBackdrop === false || showBackdropAttr === 'false';
const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
if (shouldEnableBackdrop) {
enableBackdrop();
} else {
@@ -591,10 +597,16 @@ export const createSheetGesture = (
* Backdrop should become enabled
* after the backdropBreakpoint value
*/
const modalEl = baseEl as HTMLIonModalElement & {
focusTrap?: boolean;
showBackdrop?: boolean;
};
const focusTrapAttr = modalEl.getAttribute?.('focus-trap');
const showBackdropAttr = modalEl.getAttribute?.('show-backdrop');
const focusTrapDisabled = modalEl.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = modalEl.showBackdrop === false || showBackdropAttr === 'false';
const shouldEnableBackdrop =
currentBreakpoint > backdropBreakpoint &&
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
if (shouldEnableBackdrop) {
enableBackdrop();
} else {

View File

@@ -1237,6 +1237,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
const isHandleCycle = handleBehavior === 'cycle';
const isSheetModalWithHandle = isSheetModal && showHandle;
const focusTrapAttr = this.el.getAttribute('focus-trap');
return (
<Host
no-router
@@ -1253,7 +1254,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
[`modal-sheet`]: isSheetModal,
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
'overlay-hidden': true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
...getClassMap(this.cssClass),
}}
onIonBackdropTap={this.onBackdropTap}

View File

@@ -28,6 +28,18 @@ describe('modal: focus trap', () => {
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should set the focus trap class when disabled via attribute string', async () => {
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal focus-trap="false"></ion-modal>
`,
});
const modal = page.body.querySelector('ion-modal')!;
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should not set the focus trap class by default', async () => {
const page = await newSpecPage({
components: [Modal],

View File

@@ -687,6 +687,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
const desktop = isPlatform('desktop');
const enableArrow = arrow && !parentPopover;
const focusTrapAttr = this.el.getAttribute('focus-trap');
return (
<Host
@@ -704,7 +705,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
'overlay-hidden': true,
'popover-desktop': desktop,
[`popover-side-${side}`]: true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
'popover-nested': !!parentPopover,
}}
onIonPopoverDidPresent={onLifecycle}

View File

@@ -29,6 +29,18 @@ describe('popover: focus trap', () => {
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should set the focus trap class when disabled via attribute string', async () => {
const page = await newSpecPage({
components: [Popover],
html: `
<ion-popover focus-trap="false"></ion-popover>
`,
});
const popover = page.body.querySelector('ion-popover')!;
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should not set the focus trap class by default', async () => {
const page = await newSpecPage({
components: [Popover],

View File

@@ -1,7 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
import type { NotchController } from '@utils/forms';
import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from '@utils/forms';
import { compareOptions, createNotchController, isOptionSelected } from '@utils/forms';
import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
@@ -64,7 +64,6 @@ export class Select implements ComponentInterface {
private inheritedAttributes: Attributes = {};
private nativeWrapperEl: HTMLElement | undefined;
private notchSpacerEl: HTMLElement | undefined;
private validationObserver?: MutationObserver;
private notchController?: NotchController;
@@ -82,13 +81,6 @@ export class Select implements ComponentInterface {
*/
@State() hasFocus = false;
/**
* Track validation state for proper aria-live announcements.
*/
@State() isInvalid = false;
@State() private hintTextID?: string;
/**
* The text to display on the cancel button.
*/
@@ -306,51 +298,10 @@ export class Select implements ComponentInterface {
*/
forceUpdate(this);
});
// Watch for class changes to update validation state.
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
this.validationObserver = new MutationObserver(() => {
const newIsInvalid = checkInvalidState(this.el);
if (this.isInvalid !== newIsInvalid) {
this.isInvalid = newIsInvalid;
/**
* Screen readers tend to announce changes
* to `aria-describedby` when the attribute
* is changed during a blur event for a
* native form control.
* However, the announcement can be spotty
* when using a non-native form control
* and `forceUpdate()`.
* This is due to `forceUpdate()` internally
* rescheduling the DOM update to a lower
* priority queue regardless if it's called
* inside a Promise or not, thus causing
* the screen reader to potentially miss the
* change.
* By using a State variable inside a Promise,
* it guarantees a re-render immediately at
* a higher priority.
*/
Promise.resolve().then(() => {
this.hintTextID = this.getHintTextID();
});
}
});
this.validationObserver.observe(el, {
attributes: true,
attributeFilter: ['class'],
});
}
// Always set initial state
this.isInvalid = checkInvalidState(this.el);
}
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.hintTextID = this.getHintTextID();
}
componentDidLoad() {
@@ -377,12 +328,6 @@ export class Select implements ComponentInterface {
this.notchController.destroy();
this.notchController = undefined;
}
// Clean up validation observer to prevent memory leaks.
if (this.validationObserver) {
this.validationObserver.disconnect();
this.validationObserver = undefined;
}
}
/**
@@ -1111,8 +1056,8 @@ export class Select implements ComponentInterface {
aria-label={this.ariaLabel}
aria-haspopup="dialog"
aria-expanded={`${isExpanded}`}
aria-describedby={this.hintTextID}
aria-invalid={this.isInvalid ? 'true' : undefined}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
aria-required={`${required}`}
onFocus={this.onFocus}
onBlur={this.onBlur}
@@ -1122,9 +1067,9 @@ export class Select implements ComponentInterface {
}
private getHintTextID(): string | undefined {
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (isInvalid && errorText) {
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
@@ -1139,14 +1084,14 @@ export class Select implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
const { helperText, errorText, helperTextId, errorTextId } = this;
return [
<div id={helperTextId} class="helper-text" part="supporting-text helper-text" aria-live="polite">
{!isInvalid ? helperText : null}
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
{helperText}
</div>,
<div id={errorTextId} class="error-text" part="supporting-text error-text" role="alert">
{isInvalid ? errorText : null}
<div id={errorTextId} class="error-text" part="supporting-text error-text">
{errorText}
</div>,
];
}

View File

@@ -1,200 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Select - Validation</title>
<meta
name="viewport"
content="viewport-fit=cover, 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>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 30px;
grid-column-gap: 30px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: var(--ion-color-step-600);
margin-top: 10px;
margin-bottom: 5px;
}
.validation-info {
margin: 20px;
padding: 10px;
background: var(--ion-color-light);
border-radius: 4px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Select - Validation Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="validation-info">
<h2>Screen Reader Testing Instructions:</h2>
<ol>
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
<li>Tab through the form fields</li>
<li>When you tab away from an empty required field, the error should be announced immediately</li>
<li>The error text should be announced BEFORE the next field is announced</li>
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
</ol>
</div>
<div class="grid">
<div>
<h2>Required Field</h2>
<ion-select
id="fruits-select"
label="Fruits"
placeholder="Select one"
interface="alert"
helper-text="You must select an option to continue"
error-text="This field is required"
required
>
<ion-select-option value="apples">Apples</ion-select-option>
<ion-select-option value="oranges">Oranges</ion-select-option>
<ion-select-option value="pears">Pears</ion-select-option>
</ion-select>
</div>
<div>
<h2>Optional Field (No Validation)</h2>
<ion-select
id="optional-select"
label="Colors"
placeholder="Select one"
interface="alert"
helper-text="You can skip this field"
>
<ion-select-option value="red">Red</ion-select-option>
<ion-select-option value="blue">Blue</ion-select-option>
<ion-select-option value="green">Green</ion-select-option>
</ion-select>
</div>
</div>
<div class="ion-padding">
<ion-button id="submit-btn" expand="block" disabled>Submit Form</ion-button>
<ion-button id="reset-btn" expand="block" fill="outline">Reset Form</ion-button>
</div>
</ion-content>
</ion-app>
<script>
// Simple validation logic
const selects = document.querySelectorAll('ion-select');
const submitBtn = document.getElementById('submit-btn');
const resetBtn = document.getElementById('reset-btn');
// Track which fields have been touched
const touchedFields = new Set();
// Validation functions
const validators = {
'fruits-select': (value) => {
return value !== '' && value !== undefined;
},
'optional-select': () => true, // Always valid
};
function validateField(select) {
const selectId = select.id;
const value = select.value;
const isValid = validators[selectId] ? validators[selectId](value) : true;
// Only show validation state if field has been touched
if (touchedFields.has(selectId)) {
if (isValid) {
select.classList.remove('ion-invalid');
select.classList.add('ion-valid');
} else {
select.classList.remove('ion-valid');
select.classList.add('ion-invalid');
}
select.classList.add('ion-touched');
}
return isValid;
}
function validateForm() {
let allValid = true;
selects.forEach((select) => {
if (select.id !== 'optional-select') {
const isValid = validateField(select);
if (!isValid) {
allValid = false;
}
}
});
submitBtn.disabled = !allValid;
return allValid;
}
// Add event listeners
selects.forEach((select) => {
// Mark as touched on blur
select.addEventListener('ionBlur', (e) => {
console.log('Blur event on:', select.id);
touchedFields.add(select.id);
validateField(select);
validateForm();
const isInvalid = select.classList.contains('ion-invalid');
if (isInvalid) {
console.log('Field marked invalid:', select.label, select.errorText);
}
});
// Validate on change
select.addEventListener('ionChange', (e) => {
console.log('Change event on:', select.id);
if (touchedFields.has(select.id)) {
validateField(select);
validateForm();
}
});
});
// Reset button
resetBtn.addEventListener('click', () => {
selects.forEach((select) => {
select.value = '';
select.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
});
touchedFields.clear();
submitBtn.disabled = true;
});
// Submit button
submitBtn.addEventListener('click', () => {
if (validateForm()) {
alert('Form submitted successfully!');
}
});
// Initial setup
validateForm();
</script>
</body>
</html>

View File

@@ -22,7 +22,6 @@ import type { TabBarChangedEventDetail } from './tab-bar-interface';
})
export class TabBar implements ComponentInterface {
private keyboardCtrl: KeyboardController | null = null;
private didLoad = false;
@Element() el!: HTMLElement;
@@ -41,12 +40,6 @@ export class TabBar implements ComponentInterface {
@Prop() selectedTab?: string;
@Watch('selectedTab')
selectedTabChanged() {
// Skip the initial watcher call that happens during component load
// We handle that in componentDidLoad to ensure children are ready
if (!this.didLoad) {
return;
}
if (this.selectedTab !== undefined) {
this.ionTabBarChanged.emit({
tab: this.selectedTab,
@@ -72,19 +65,8 @@ export class TabBar implements ComponentInterface {
*/
@Event() ionTabBarLoaded!: EventEmitter<void>;
componentDidLoad() {
this.ionTabBarLoaded.emit();
// Set the flag to indicate the component has loaded
// This allows the watcher to emit changes from this point forward
this.didLoad = true;
// Emit the initial selected tab after the component is fully loaded
// This ensures all child components (ion-tab-button) are ready
if (this.selectedTab !== undefined) {
this.ionTabBarChanged.emit({
tab: this.selectedTab,
});
}
componentWillLoad() {
this.selectedTabChanged();
}
async connectedCallback() {
@@ -108,6 +90,10 @@ export class TabBar implements ComponentInterface {
}
}
componentDidLoad() {
this.ionTabBarLoaded.emit();
}
render() {
const { color, translucent, keyboardVisible } = this;
const mode = getIonMode(this);

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -65,33 +65,32 @@ export class Tabs implements NavOutlet {
this.ionNavWillLoad.emit();
}
componentDidLoad() {
this.updateTabBar();
}
componentDidUpdate() {
this.updateTabBar();
}
private updateTabBar() {
componentWillRender() {
const tabBar = this.el.querySelector('ion-tab-bar');
if (!tabBar) {
return;
if (tabBar) {
let tab = this.selectedTab ? this.selectedTab.tab : undefined;
// Fallback: if no selectedTab is set but we're using router mode,
// determine the active tab from the current URL. This works around
// timing issues in React Router integration where setRouteId may not
// be called in time for the initial render.
// TODO(FW-6724): Remove this with React Router upgrade
if (!tab && this.useRouter && typeof window !== 'undefined') {
const currentPath = window.location.pathname;
const tabButtons = this.el.querySelectorAll('ion-tab-button');
// Look for a tab button that matches the current path pattern
for (const tabButton of tabButtons) {
const tabId = tabButton.getAttribute('tab');
if (tabId && currentPath.includes(tabId)) {
tab = tabId;
break;
}
}
}
tabBar.selectedTab = tab;
}
const tab = this.selectedTab ? this.selectedTab.tab : undefined;
// If tabs has no selected tab but tab-bar already has a selected-tab set,
// don't overwrite it. This handles cases where tab-bar is used without ion-tab elements.
if (tab === undefined) {
return;
}
if (tabBar.selectedTab === tab) {
return;
}
tabBar.selectedTab = tab;
}
/**
@@ -163,7 +162,6 @@ export class Tabs implements NavOutlet {
this.selectedTab = selectedTab;
this.ionTabsWillChange.emit({ tab: selectedTab.tab });
selectedTab.active = true;
this.updateTabBar();
return Promise.resolve();
}

View File

@@ -15,7 +15,7 @@ import {
writeTask,
} from '@stencil/core';
import type { NotchController } from '@utils/forms';
import { createNotchController, checkInvalidState } from '@utils/forms';
import { createNotchController } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
import { createSlotMutationController } from '@utils/slot-mutation-controller';
@@ -335,6 +335,16 @@ export class Textarea implements ComponentInterface {
}
}
/**
* Checks if the textarea is in an invalid state based on Ionic validation classes
*/
private checkValidationState(): boolean {
const hasIonTouched = this.el.classList.contains('ion-touched');
const hasIonInvalid = this.el.classList.contains('ion-invalid');
return hasIonTouched && hasIonInvalid;
}
connectedCallback() {
const { el } = this;
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -347,7 +357,7 @@ export class Textarea implements ComponentInterface {
// Watch for class changes to update validation state
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
this.validationObserver = new MutationObserver(() => {
const newIsInvalid = checkInvalidState(this.el);
const newIsInvalid = this.checkValidationState();
if (this.isInvalid !== newIsInvalid) {
this.isInvalid = newIsInvalid;
// Force a re-render to update aria-describedby immediately
@@ -362,7 +372,7 @@ export class Textarea implements ComponentInterface {
}
// Always set initial state
this.isInvalid = checkInvalidState(this.el);
this.isInvalid = this.checkValidationState();
this.debounceChanged();
if (Build.isBrowser) {

View File

@@ -45,20 +45,6 @@
<ion-toggle style="width: 100%"> Full-width </ion-toggle><br />
<ion-toggle> Long Label Long Label Long Label Long Label Long Label Long Label </ion-toggle><br />
</ion-content>
<script>
document.addEventListener('ionBlur', (ev) => {
console.log('ionBlur', ev);
});
document.addEventListener('ionChange', (ev) => {
console.log('ionChange', ev);
});
document.addEventListener('ionFocus', (ev) => {
console.log('ionFocus', ev);
});
</script>
</ion-app>
</body>
</html>

View File

@@ -1,65 +1,7 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('toggle: ionChange'), () => {
test('should fire ionChange when interacting with toggle', async ({ page }) => {
await page.setContent(
`
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
`,
config
);
const ionChange = await page.spyOnEvent('ionChange');
const toggle = page.locator('ion-toggle');
await toggle.click();
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true });
await toggle.click();
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false });
});
test('should fire ionChange when interacting with toggle in item', async ({ page }) => {
await page.setContent(
`
<ion-item>
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
</ion-item>
`,
config
);
const ionChange = await page.spyOnEvent('ionChange');
const item = page.locator('ion-item');
await item.click();
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true });
await item.click();
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false });
});
test('should not fire when programmatically setting a value', async ({ page }) => {
await page.setContent(
`
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
`,
config
);
const ionChange = await page.spyOnEvent('ionChange');
const toggle = page.locator('ion-toggle');
await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true));
expect(ionChange).not.toHaveReceivedEvent();
});
});
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('toggle: click'), () => {
test('should trigger onclick only once when clicking the label', async ({ page }, testInfo) => {
testInfo.annotations.push({
@@ -93,195 +35,4 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
expect(clickCount).toBe(1);
});
});
test.describe(title('toggle: ionFocus'), () => {
test('should not have visual regressions', async ({ page, pageUtils }) => {
await page.setContent(
`
<style>
#container {
display: inline-block;
padding: 10px;
}
</style>
<div id="container">
<ion-toggle>Unchecked</ion-toggle>
</div>
`,
config
);
await pageUtils.pressKeys('Tab');
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`toggle-focus`));
});
test('should not have visual regressions when interacting with toggle in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item class="ion-focused">
<ion-toggle>Unchecked</ion-toggle>
</ion-item>
`,
config
);
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
const item = page.locator('ion-item');
await expect(item).toHaveScreenshot(screenshot(`toggle-in-item-focus`));
});
test('should fire ionFocus when toggle is focused', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
expect(ionFocus).toHaveReceivedEventTimes(1);
// Reset focus
const toggle = page.locator('ion-toggle');
const toggleBoundingBox = (await toggle.boundingBox())!;
await page.mouse.click(0, toggleBoundingBox.height + 1);
// Test focus with click
await toggle.click();
expect(ionFocus).toHaveReceivedEventTimes(2);
});
test('should fire ionFocus when interacting with toggle in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item>
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
</ion-item>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
// Test focus with keyboard navigation
await pageUtils.pressKeys('Tab');
expect(ionFocus).toHaveReceivedEventTimes(1);
// Verify that the event target is the toggle and not the item
const eventByKeyboard = ionFocus.events[0];
expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
// Reset focus
const toggle = page.locator('ion-toggle');
const toggleBoundingBox = (await toggle.boundingBox())!;
await page.mouse.click(0, toggleBoundingBox.height + 1);
// Test focus with click
const item = page.locator('ion-item');
await item.click();
expect(ionFocus).toHaveReceivedEventTimes(2);
// Verify that the event target is the toggle and not the item
const eventByClick = ionFocus.events[0];
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
});
test('should not fire when programmatically setting a value', async ({ page }) => {
await page.setContent(
`
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
`,
config
);
const ionFocus = await page.spyOnEvent('ionFocus');
const toggle = page.locator('ion-toggle');
await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true));
expect(ionFocus).not.toHaveReceivedEvent();
});
});
test.describe(title('toggle: ionBlur'), () => {
test('should fire ionBlur when toggle is blurred', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
`,
config
);
const ionBlur = await page.spyOnEvent('ionBlur');
// Test blur with keyboard navigation
// Focus the toggle
await pageUtils.pressKeys('Tab');
// Blur the toggle
await pageUtils.pressKeys('Tab');
expect(ionBlur).toHaveReceivedEventTimes(1);
// Test blur with click
const toggle = page.locator('ion-toggle');
// Focus the toggle
await toggle.click();
// Blur the toggle by clicking outside of it
const toggleBoundingBox = (await toggle.boundingBox())!;
await page.mouse.click(0, toggleBoundingBox.height + 1);
expect(ionBlur).toHaveReceivedEventTimes(2);
});
test('should fire ionBlur after interacting with toggle in item', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-item>
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
</ion-item>
`,
config
);
const ionBlur = await page.spyOnEvent('ionBlur');
// Test blur with keyboard navigation
// Focus the toggle
await pageUtils.pressKeys('Tab');
// Blur the toggle
await pageUtils.pressKeys('Tab');
expect(ionBlur).toHaveReceivedEventTimes(1);
// Verify that the event target is the toggle and not the item
const event = ionBlur.events[0];
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
// Test blur with click
const item = page.locator('ion-item');
await item.click();
// Blur the toggle by clicking outside of it
const itemBoundingBox = (await item.boundingBox())!;
await page.mouse.click(0, itemBoundingBox.height + 1);
expect(ionBlur).toHaveReceivedEventTimes(2);
// Verify that the event target is the toggle and not the item
const eventByClick = ionBlur.events[0];
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -223,20 +223,6 @@
</div>
</div>
</ion-content>
<script>
document.addEventListener('ionBlur', (ev) => {
console.log('ionBlur', ev);
});
document.addEventListener('ionChange', (ev) => {
console.log('ionChange', ev);
});
document.addEventListener('ionFocus', (ev) => {
console.log('ionFocus', ev);
});
</script>
</ion-app>
</body>
</html>

View File

@@ -40,6 +40,7 @@ export class Toggle implements ComponentInterface {
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
private gesture?: Gesture;
private focusEl?: HTMLElement;
private lastDrag = 0;
private inheritedAttributes: Attributes = {};
private toggleTrack?: HTMLElement;
@@ -161,6 +162,7 @@ export class Toggle implements ComponentInterface {
const isNowChecked = !checked;
this.checked = isNowChecked;
this.setFocus();
this.ionChange.emit({
checked: isNowChecked,
value,
@@ -241,7 +243,9 @@ export class Toggle implements ComponentInterface {
}
private setFocus() {
this.el.focus();
if (this.focusEl) {
this.focusEl.focus();
}
}
private onKeyDown = (ev: KeyboardEvent) => {
@@ -413,8 +417,6 @@ export class Toggle implements ComponentInterface {
aria-disabled={disabled ? 'true' : null}
tabindex={disabled ? undefined : 0}
onKeyDown={this.onKeyDown}
onFocus={this.onFocus}
onBlur={this.onBlur}
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),
@@ -439,6 +441,9 @@ export class Toggle implements ComponentInterface {
checked={checked}
disabled={disabled}
id={inputId}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
required={required}
{...inheritedAttributes}
/>

View File

@@ -1,3 +1,2 @@
export * from './notch-controller';
export * from './compare-with-utils';
export * from './validity';

View File

@@ -1,15 +0,0 @@
type FormElement = HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSelectElement;
/**
* Checks if the form element is in an invalid state based on
* Ionic validation classes.
*
* @param el The form element to check.
* @returns `true` if the element is invalid, `false` otherwise.
*/
export const checkInvalidState = (el: FormElement): boolean => {
const hasIonTouched = el.classList.contains('ion-touched');
const hasIonInvalid = el.classList.contains('ion-invalid');
return hasIonTouched && hasIonInvalid;
};

View File

@@ -539,11 +539,18 @@ export const present = async <OverlayPresentOptions>(
* view container subtree, skip adding aria-hidden/inert there
* to avoid disabling the overlay.
*/
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false;
const overlayEl = overlay.el as HTMLIonOverlayElement & {
focusTrap?: boolean;
showBackdrop?: boolean;
};
const focusTrapAttr = overlayEl.getAttribute?.('focus-trap');
const showBackdropAttr = overlayEl.getAttribute?.('show-backdrop');
const focusTrapDisabled = overlayEl.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = overlayEl.showBackdrop === false || showBackdropAttr === 'false';
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && !focusTrapDisabled;
// Only lock out root content when backdrop is active. Developers relying on showBackdrop=false
// expect background interaction to remain enabled.
const shouldLockRoot = shouldTrapFocus && overlayEl.showBackdrop !== false;
const shouldLockRoot = shouldTrapFocus && !backdropDisabled;
overlay.presented = true;
overlay.willPresent.emit();
@@ -681,11 +688,21 @@ export const dismiss = async <OverlayDismissOptions>(
*/
const overlaysLockingRoot = presentedOverlays.filter((o) => {
const el = o as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
return el.tagName !== 'ION-TOAST' && el.focusTrap !== false && el.showBackdrop !== false;
const focusTrapAttr = el.getAttribute?.('focus-trap');
const showBackdropAttr = el.getAttribute?.('show-backdrop');
const focusTrapDisabled = el.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = el.showBackdrop === false || showBackdropAttr === 'false';
return el.tagName !== 'ION-TOAST' && !focusTrapDisabled && !backdropDisabled;
});
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
const locksRoot =
overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && overlayEl.showBackdrop !== false;
const overlayEl = overlay.el as HTMLIonOverlayElement & {
focusTrap?: boolean;
showBackdrop?: boolean;
};
const focusTrapAttr = overlayEl.getAttribute?.('focus-trap');
const showBackdropAttr = overlayEl.getAttribute?.('show-backdrop');
const focusTrapDisabled = overlayEl.focusTrap === false || focusTrapAttr === 'false';
const backdropDisabled = overlayEl.showBackdrop === false || showBackdropAttr === 'false';
const locksRoot = overlayEl.tagName !== 'ION-TOAST' && !focusTrapDisabled && !backdropDisabled;
/**
* If this is the last visible overlay that is trapping focus

View File

@@ -37,6 +37,26 @@ describe('overlays: scroll blocking', () => {
expect(body).not.toHaveClass('backdrop-no-scroll');
});
it('should not block scroll when focus-trap attribute is set to "false"', async () => {
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal focus-trap="false"></ion-modal>
`,
});
const modal = page.body.querySelector('ion-modal')!;
const body = page.doc.querySelector('body')!;
await modal.present();
expect(body).not.toHaveClass('backdrop-no-scroll');
await modal.dismiss();
expect(body).not.toHaveClass('backdrop-no-scroll');
});
it('should not block scroll when the overlay is dismissed', async () => {
const page = await newSpecPage({
components: [Modal],

View File

@@ -2,40 +2,6 @@ import type { E2EPage } from '../../playwright-declarations';
import { addE2EListener, EventSpy } from '../event-spy';
export const spyOnEvent = async (page: E2EPage, eventName: string): Promise<EventSpy> => {
/**
* Tabbing out of the page boundary can lead to unreliable `ionBlur events,
* particularly in Firefox.
*
* This occurs because Playwright may incorrectly maintain focus state on the
* last element, even after a Tab press attempts to shift focus outside the
* viewport. To reliably trigger the necessary blur event, add a visually
* hidden, focusable element at the end of the page to receive focus instead of
* the browser.
*
* Playwright issue reference:
* https://github.com/microsoft/playwright/issues/32269
*/
if (eventName === 'ionBlur') {
const hiddenInput = await page.$('#hidden-input-for-ion-blur');
if (!hiddenInput) {
await page.evaluate(() => {
const input = document.createElement('input');
input.id = 'hidden-input-for-ion-blur';
input.style.position = 'absolute';
input.style.opacity = '0';
input.style.height = '0';
input.style.width = '0';
input.style.pointerEvents = 'none';
document.body.appendChild(input);
// Clean up the element when the page is unloaded.
window.addEventListener('unload', () => {
input.remove();
});
});
}
}
const spy = new EventSpy(eventName);
const handle = await page.evaluateHandle(() => window);

View File

@@ -18,51 +18,34 @@ const focusController = createFocusController();
// TODO(FW-2832): types
/**
* Executes the main page transition.
* It also manages the lifecycle of header visibility (if any)
* to prevent visual flickering in iOS. The flickering only
* occurs for a condensed header that is placed above the content.
*
* @param opts Options for the transition.
* @returns A promise that resolves when the transition is complete.
*/
export const transition = (opts: TransitionOptions): Promise<TransitionResult> => {
return new Promise((resolve, reject) => {
writeTask(() => {
const transitioningInactiveHeader = getIosIonHeader(opts);
beforeTransition(opts, transitioningInactiveHeader);
runTransition(opts)
.then(
(result) => {
if (result.animation) {
result.animation.destroy();
}
afterTransition(opts);
resolve(result);
},
(error) => {
afterTransition(opts);
reject(error);
beforeTransition(opts);
runTransition(opts).then(
(result) => {
if (result.animation) {
result.animation.destroy();
}
)
.finally(() => {
// Ensure that the header is restored to its original state.
setHeaderTransitionClass(transitioningInactiveHeader, false);
});
afterTransition(opts);
resolve(result);
},
(error) => {
afterTransition(opts);
reject(error);
}
);
});
});
};
const beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {
const beforeTransition = (opts: TransitionOptions) => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
focusController.saveViewFocus(leavingEl);
setZIndex(enteringEl, leavingEl, opts.direction);
// Prevent flickering of the header by adding a class.
setHeaderTransitionClass(transitioningInactiveHeader, true);
if (opts.showGoBack) {
enteringEl.classList.add('can-go-back');
@@ -295,40 +278,6 @@ const setZIndex = (
}
};
/**
* Add a class to ensure that the header (if any)
* does not flicker during the transition. By adding the
* transitioning class, we ensure that the header has
* the necessary styles to prevent the following flickers:
* 1. When entering a page with a condensed header, the
* header should never be visible. However,
* it briefly renders the background color while
* the transition is occurring.
* 2. When leaving a page with a condensed header, the
* header has an opacity of 0 and the pages
* have a z-index which causes the entering page to
* briefly show it's content underneath the leaving page.
* 3. When entering a page or leaving a page with a fade
* header, the header should not have a background color.
* However, it briefly shows the background color while
* the transition is occurring.
*
* @param header The header element to modify.
* @param isTransitioning Whether the transition is occurring.
*/
const setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => {
if (!header) {
return;
}
const transitionClass = 'header-transitioning';
if (isTransitioning) {
header.classList.add(transitionClass);
} else {
header.classList.remove(transitionClass);
}
};
export const getIonPageElement = (element: HTMLElement) => {
if (element.classList.contains('ion-page')) {
return element;
@@ -342,32 +291,6 @@ export const getIonPageElement = (element: HTMLElement) => {
return element;
};
/**
* Retrieves the ion-header element from a page based on the
* direction of the transition.
*
* @param opts Options for the transition.
* @returns The ion-header element or null if not found or not in 'ios' mode.
*/
const getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
const direction = opts.direction;
const mode = opts.mode;
if (mode !== 'ios') {
return null;
}
const element = direction === 'back' ? leavingEl : enteringEl;
if (!element) {
return null;
}
return element.querySelector('ion-header');
};
export interface TransitionOptions extends NavOptions {
progressCallback?: (ani: Animation | undefined) => void;
baseEl: any;

View File

@@ -3,5 +3,5 @@
"core",
"packages/*"
],
"version": "8.7.9"
"version": "8.7.5"
}

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.9"
"@ionic/core": "^8.7.5"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^16.0.0",
@@ -1031,12 +1031,12 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -1386,9 +1386,9 @@
]
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -7306,11 +7306,11 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"requires": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -7529,9 +7529,9 @@
"optional": true
},
"@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -11286,4 +11286,4 @@
}
}
}
}
}

View File

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

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
**Note:** Version bump only for package @ionic/angular
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/angular
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
### Bug Fixes
* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367))
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/angular
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.9",
"@ionic/core": "^8.7.5",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -1398,12 +1398,12 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -2308,9 +2308,9 @@
}
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -9079,4 +9079,4 @@
}
}
}
}
}

View File

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

View File

@@ -77,31 +77,6 @@
<p>MinLength Errors: <span id="minlength-errors">{{minLengthField.errors | json}}</span></p>
</ion-label>
</ion-item>
<!-- Test ion-select with required validation -->
<ion-item>
<ion-select
label="Required Select"
[(ngModel)]="selectValue"
name="selectField"
required
#selectField="ngModel"
id="template-select-test"
errorText="This field is required"
helperText="Select an option">
<ion-select-option value="option1">Option 1</ion-select-option>
<ion-select-option value="option2">Option 2</ion-select-option>
</ion-select>
</ion-item>
<!-- Display validation state for debugging -->
<ion-item>
<ion-label>
<p>Select Touched: <span id="select-touched">{{selectField.touched}}</span></p>
<p>Select Invalid: <span id="select-invalid">{{selectField.invalid}}</span></p>
<p>Select Errors: <span id="select-errors">{{selectField.errors | json}}</span></p>
</ion-label>
</ion-item>
</ion-list>
<div class="ion-padding">

View File

@@ -9,7 +9,6 @@ export class TemplateFormComponent {
inputValue = '';
textareaValue = '';
minLengthValue = '';
selectValue = '';
// Track if form has been submitted
submitted = false;

View File

@@ -47,7 +47,6 @@ export const routes: Routes = [
children: [
{ path: 'input-validation', loadComponent: () => import('../validation/input-validation/input-validation.component').then(c => c.InputValidationComponent) },
{ path: 'textarea-validation', loadComponent: () => import('../validation/textarea-validation/textarea-validation.component').then(c => c.TextareaValidationComponent) },
{ path: 'select-validation', loadComponent: () => import('../validation/select-validation/select-validation.component').then(c => c.SelectValidationComponent) },
{ path: '**', redirectTo: 'input-validation' }
]
},

View File

@@ -131,11 +131,6 @@
Textarea Validation Test
</ion-label>
</ion-item>
<ion-item routerLink="/standalone/validation/select-validation">
<ion-label>
Select Validation Test
</ion-label>
</ion-item>
</ion-list>
<ion-list>

View File

@@ -1,63 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>Select - Validation Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="validation-info">
<h2>Screen Reader Testing Instructions:</h2>
<ol>
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
<li>Tab through the form fields</li>
<li>When you tab away from an empty required field, the error should be announced immediately</li>
<li>The error text should be announced BEFORE the next field is announced</li>
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
</ol>
</div>
<form [formGroup]="form">
<div class="grid">
<div>
<h2>Required Field</h2>
<ion-select
#fruitsSelect
id="fruits-select"
[label]="fieldMetadata.fruits.label"
placeholder="Select one"
interface="alert"
[helperText]="fieldMetadata.fruits.helperText"
[errorText]="fieldMetadata.fruits.errorText"
formControlName="fruits"
required
>
<ion-select-option value="apples">Apples</ion-select-option>
<ion-select-option value="oranges">Oranges</ion-select-option>
<ion-select-option value="pears">Pears</ion-select-option>
</ion-select>
</div>
<div>
<h2>Optional Field (No Validation)</h2>
<ion-select
#optionalSelect
id="optional-select"
[label]="fieldMetadata.optional.label"
placeholder="Select one"
interface="alert"
[helperText]="fieldMetadata.optional.helperText"
formControlName="optional"
>
<ion-select-option value="red">Red</ion-select-option>
<ion-select-option value="blue">Blue</ion-select-option>
<ion-select-option value="green">Green</ion-select-option>
</ion-select>
</div>
</div>
</form>
<div class="ion-padding">
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
</div>
</ion-content>

View File

@@ -1,36 +0,0 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: var(--ion-color-step-600);
margin-top: 10px;
margin-bottom: 5px;
}
.validation-info {
margin: 20px;
padding: 10px;
background: var(--ion-color-light);
border-radius: 4px;
}
.validation-info h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.validation-info ol {
margin: 0;
padding-left: 20px;
}
.validation-info li {
margin-bottom: 5px;
}

View File

@@ -1,63 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
FormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import {
IonButton,
IonContent,
IonHeader,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar
} from '@ionic/angular/standalone';
@Component({
selector: 'app-select-validation',
templateUrl: './select-validation.component.html',
styleUrls: ['./select-validation.component.scss'],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
IonSelect,
IonSelectOption,
IonButton,
IonHeader,
IonToolbar,
IonTitle,
IonContent
]
})
export class SelectValidationComponent {
// Field metadata for labels and error messages
fieldMetadata = {
fruits: {
label: 'Fruits',
helperText: "Select an option",
errorText: 'This field is required'
},
optional: {
label: 'Colors',
helperText: 'You can skip this field',
errorText: ''
}
};
form = this.fb.group({
fruits: ['', Validators.required],
optional: ['']
});
constructor(private fb: FormBuilder) {}
// Submit form
onSubmit(): void {
if (this.form.valid) {
alert('Form submitted successfully!');
}
}
}

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
**Note:** Version bump only for package @ionic/docs
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/docs
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
**Note:** Version bump only for package @ionic/docs
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/docs
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
**Note:** Version bump only for package @ionic/docs

View File

@@ -1,13 +1,13 @@
{
"name": "@ionic/docs",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT"
}
}
}
}

View File

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

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
**Note:** Version bump only for package @ionic/react-router
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/react-router
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
**Note:** Version bump only for package @ionic/react-router
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/react-router
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
**Note:** Version bump only for package @ionic/react-router

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react-router",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/react-router",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT",
"dependencies": {
"@ionic/react": "^8.7.9",
"@ionic/react": "^8.7.5",
"tslib": "*"
},
"devDependencies": {
@@ -238,12 +238,12 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -415,12 +415,12 @@
}
},
"node_modules/@ionic/react": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.8.tgz",
"integrity": "sha512-QRxGXcSkfmwVIFxdHI776bqiHpqT1FwwVNASBRPCD8RNCIT9NTZIvgNdJ2FokBZjHRfgk4QuYOcQntrbPmK0Hg==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz",
"integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.8",
"@ionic/core": "8.7.5",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -669,9 +669,9 @@
]
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -4175,11 +4175,11 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"requires": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -4281,11 +4281,11 @@
"requires": {}
},
"@ionic/react": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.8.tgz",
"integrity": "sha512-QRxGXcSkfmwVIFxdHI776bqiHpqT1FwwVNASBRPCD8RNCIT9NTZIvgNdJ2FokBZjHRfgk4QuYOcQntrbPmK0Hg==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz",
"integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==",
"requires": {
"@ionic/core": "8.7.8",
"@ionic/core": "8.7.5",
"ionicons": "^8.0.13",
"tslib": "*"
}
@@ -4422,9 +4422,9 @@
"optional": true
},
"@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -6844,4 +6844,4 @@
"dev": true
}
}
}
}

View File

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

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
### Bug Fixes
* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613)
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/react
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
**Note:** Version bump only for package @ionic/react
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/react
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react",
"version": "8.7.9",
"version": "8.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/react",
"version": "8.7.9",
"version": "8.7.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.9",
"@ionic/core": "^8.7.5",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -736,12 +736,12 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.8",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
}
@@ -1726,9 +1726,9 @@
}
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -11913,4 +11913,4 @@
}
}
}
}
}

View File

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

View File

@@ -37,7 +37,6 @@ import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted'
import OverlayComponents from './pages/overlay-components/OverlayComponents';
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
import ReorderGroup from './pages/ReorderGroup';
import AccordionGroup from './pages/AccordionGroup';
setupIonicReact();
@@ -70,7 +69,6 @@ const App: React.FC = () => (
<Route path="/icons" component={Icons} />
<Route path="/inputs" component={Inputs} />
<Route path="/reorder-group" component={ReorderGroup} />
<Route path="/accordion-group" component={AccordionGroup} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>

View File

@@ -1,54 +0,0 @@
import { IonHeader, IonTitle, IonToolbar, IonPage, IonContent, IonAccordionGroup, IonAccordion, IonItem, IonLabel } from '@ionic/react';
import { useEffect, useRef } from 'react';
const AccordionGroup: React.FC = () => {
const accordionGroup = useRef<null | HTMLIonAccordionGroupElement>(null);
useEffect(() => {
if (!accordionGroup.current) {
return;
}
accordionGroup.current.value = ['first', 'third'];
}, []);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Accordion Group</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonAccordionGroup ref={accordionGroup} multiple={true}>
<IonAccordion value="first">
<IonItem slot="header" color="light">
<IonLabel>First Accordion</IonLabel>
</IonItem>
<div className="ion-padding" slot="content">
First Content
</div>
</IonAccordion>
<IonAccordion value="second">
<IonItem slot="header" color="light">
<IonLabel>Second Accordion</IonLabel>
</IonItem>
<div className="ion-padding" slot="content">
Second Content
</div>
</IonAccordion>
<IonAccordion value="third">
<IonItem slot="header" color="light">
<IonLabel>Third Accordion</IonLabel>
</IonItem>
<div className="ion-padding" slot="content">
Third Content
</div>
</IonAccordion>
</IonAccordionGroup>
</IonContent>
</IonPage>
);
};
export default AccordionGroup;

View File

@@ -22,9 +22,6 @@ const Main: React.FC<MainProps> = () => {
</IonHeader>
<IonContent>
<IonList>
<IonItem routerLink="/accordion-group">
<IonLabel>Accordion Group</IonLabel>
</IonItem>
<IonItem routerLink="/overlay-hooks">
<IonLabel>Overlay Hooks</IonLabel>
</IonItem>

View File

@@ -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.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
**Note:** Version bump only for package @ionic/vue-router

Some files were not shown because too many files have changed in this diff Show More