Compare commits
72 Commits
fix/focus-
...
chip-tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e9ee2314 | ||
|
|
25a7adf7cc | ||
|
|
442e3e9831 | ||
|
|
62d880d620 | ||
|
|
4eca8d39d8 | ||
|
|
040bdf78c5 | ||
|
|
1bccf76d35 | ||
|
|
dd1c1e8fa3 | ||
|
|
d7b4d0690b | ||
|
|
95b87020d6 | ||
|
|
ab733b71dd | ||
|
|
f99d0007a8 | ||
|
|
3b3318da51 | ||
|
|
07b46d745a | ||
|
|
37f87b39c4 | ||
|
|
f71f4bf454 | ||
|
|
36f4b4d600 | ||
|
|
e5634d45ee | ||
|
|
7d6430738e | ||
|
|
72826edf9a | ||
|
|
4360e39a58 | ||
|
|
622d62a3f4 | ||
|
|
12ede4b79c | ||
|
|
f83b000530 | ||
|
|
3b60a1d68a | ||
|
|
8573bf8083 | ||
|
|
2c6fac9060 | ||
|
|
b9fdfab667 | ||
|
|
f7af5d3ca5 | ||
|
|
03fb422bfa | ||
|
|
82de33b96e | ||
|
|
76b715874a | ||
|
|
6205338620 | ||
|
|
f775815a13 | ||
|
|
cf3caa287e | ||
|
|
2ee52d77c8 | ||
|
|
0e110de5e3 | ||
|
|
f50994a6ef | ||
|
|
5bf6f6e825 | ||
|
|
afa15d23d2 | ||
|
|
b1645168a7 | ||
|
|
b9e3cf0f5a | ||
|
|
99dcf3810a | ||
|
|
6643f6a115 | ||
|
|
1c89cf06ac | ||
|
|
3129565e4e | ||
|
|
39a0be848c | ||
|
|
57687623aa | ||
|
|
3709bba41e | ||
|
|
76e4901189 | ||
|
|
22288319ed | ||
|
|
c65b76e727 | ||
|
|
b4e540decc | ||
|
|
87e50ad9da | ||
|
|
595643fd14 | ||
|
|
3249e1dce8 | ||
|
|
e9bd3f819d | ||
|
|
bf0f1e36e4 | ||
|
|
9d781db662 | ||
|
|
9ae41efddb | ||
|
|
7379d34f38 | ||
|
|
53f3bea244 | ||
|
|
5c86b87fe3 | ||
|
|
d8e6756ac3 | ||
|
|
627416b9d7 | ||
|
|
eeb15c3c5c | ||
|
|
2bebbd7a3e | ||
|
|
b3826febe0 | ||
|
|
1ff26b796b | ||
|
|
99bfdee4cd | ||
|
|
d03e88179b | ||
|
|
ecc291138e |
@@ -22,10 +22,11 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 🟢 Configure Node for Publish
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@ionic'
|
||||
# Provenance requires npm 9.5.0+
|
||||
- name: 📦 Install latest npm
|
||||
run: npm install -g npm@latest
|
||||
@@ -56,3 +57,4 @@ runs:
|
||||
run: npm publish ${{ inputs.folder }} --tag ${{ inputs.tag }} --provenance
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
3
.github/ionic-issue-bot.yml
vendored
@@ -40,7 +40,7 @@ comment:
|
||||
|
||||
|
||||
If the requested feature is something you would find useful for your applications, please react to the original post with 👍 (`+1`). If you would like to provide an additional use case for the feature, please post a comment.
|
||||
|
||||
|
||||
|
||||
The team will review this feedback and make a final decision. Any decision will be posted on this thread, but please note that we may ultimately decide not to pursue this feature.
|
||||
|
||||
@@ -83,6 +83,7 @@ stale:
|
||||
exemptLabels:
|
||||
- "good first issue"
|
||||
- "triage"
|
||||
- "bug: external"
|
||||
- "type: bug"
|
||||
- "type: feature request"
|
||||
- "needs: investigation"
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Build Ionic Angular Server'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -8,8 +8,8 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Build Ionic React Router'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Build Ionic React'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Builds Ionic Vue Router'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Build Ionic Vue'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -10,7 +10,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Test Core Clean Build'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: 'Test Core Lint'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
|
||||
@@ -13,7 +13,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
@@ -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@v6
|
||||
with:
|
||||
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
|
||||
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
|
||||
@@ -7,10 +7,10 @@ on:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: ./artifacts
|
||||
- name: 🔎 Extract Archives
|
||||
|
||||
@@ -13,7 +13,7 @@ runs:
|
||||
- name: 🗄️ Create Archive
|
||||
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
|
||||
shell: bash
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.output }}
|
||||
|
||||
30
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-core
|
||||
with:
|
||||
ionicons-version: ${{ inputs.ionicons_npm_release_tag }}
|
||||
@@ -31,21 +31,21 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-clean-build
|
||||
|
||||
test-core-lint:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-lint
|
||||
|
||||
test-core-spec:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-spec
|
||||
|
||||
test-core-screenshot:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-screenshot
|
||||
with:
|
||||
shard: ${{ matrix.shard }}
|
||||
@@ -90,14 +90,14 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-vue
|
||||
|
||||
build-vue-router:
|
||||
needs: [build-vue]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-vue-router
|
||||
|
||||
test-vue-e2e:
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
needs: [build-vue, build-vue-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-vue-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -126,14 +126,14 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-angular
|
||||
|
||||
build-angular-server:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-angular-server
|
||||
|
||||
test-angular-e2e:
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
needs: [build-angular, build-angular-server]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-angular-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -162,14 +162,14 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-react
|
||||
|
||||
build-react-router:
|
||||
needs: [build-react]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-react-router
|
||||
|
||||
test-react-router-e2e:
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-router-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
9
.github/workflows/dev-build.yml
vendored
@@ -1,7 +1,11 @@
|
||||
name: 'Ionic Dev Build'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
create-dev-hash:
|
||||
@@ -9,7 +13,7 @@ jobs:
|
||||
outputs:
|
||||
dev-hash: ${{ steps.create-dev-hash.outputs.DEV_HASH }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# A 1 is required before the timestamp
|
||||
# as lerna will fail when there is a leading 0
|
||||
# See https://github.com/lerna/lerna/issues/2840
|
||||
@@ -25,6 +29,7 @@ jobs:
|
||||
release-ionic:
|
||||
needs: [create-dev-hash]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/release-ionic.yml
|
||||
with:
|
||||
|
||||
13
.github/workflows/nightly.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: 'Ionic Nightly Build'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Monday-Friday
|
||||
# at 6:00 UTC (6:00 am UTC)
|
||||
- cron: '00 06 * * 1-5'
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
create-nightly-hash:
|
||||
@@ -12,7 +13,7 @@ jobs:
|
||||
outputs:
|
||||
nightly-hash: ${{ steps.create-nightly-hash.outputs.NIGHTLY_HASH }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# A 1 is required before the timestamp
|
||||
# as lerna will fail when there is a leading 0
|
||||
# See https://github.com/lerna/lerna/issues/2840
|
||||
@@ -30,8 +31,10 @@ jobs:
|
||||
release-ionic:
|
||||
needs: [create-nightly-hash]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/release-ionic.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tag: nightly
|
||||
version: ${{ needs.create-nightly-hash.outputs.nightly-hash }}
|
||||
|
||||
32
.github/workflows/release-ionic.yml
vendored
@@ -23,8 +23,8 @@ jobs:
|
||||
release-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/core'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -48,14 +48,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/docs built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-docs
|
||||
path: ./packages/docs
|
||||
filename: DocsBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/docs'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -67,14 +67,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/angular'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -93,14 +93,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/react'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -118,14 +118,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/vue'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -143,14 +143,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/angular-server'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
needs: [release-react]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
name: ionic-react
|
||||
path: ./packages/react
|
||||
filename: ReactBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/react-router'
|
||||
tag: ${{ inputs.tag }}
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
needs: [release-vue]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
name: ionic-vue
|
||||
path: ./packages/vue
|
||||
filename: VueBuild.zip
|
||||
- uses: ./.github/workflows/actions/publish-npm
|
||||
- uses: ./.github/actions/publish-npm
|
||||
with:
|
||||
scope: '@ionic/vue-router'
|
||||
tag: ${{ inputs.tag }}
|
||||
|
||||
81
.github/workflows/release-orchestrator.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: 'Release - Ionic Framework'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Monday-Friday
|
||||
# at 6:00 UTC (6:00 am UTC)
|
||||
- cron: '00 06 * * 1-5'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-type:
|
||||
description: 'Which Ionic release workflow should run?'
|
||||
required: true
|
||||
type: choice
|
||||
default: dev
|
||||
options:
|
||||
- dev
|
||||
- production
|
||||
version:
|
||||
description: 'Which version should be published? (Only for production releases)'
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- prepatch
|
||||
- preminor
|
||||
- premajor
|
||||
- prerelease
|
||||
tag:
|
||||
description: 'Which npm tag should this be published to? (Only for production releases)'
|
||||
required: false
|
||||
type: choice
|
||||
default: latest
|
||||
options:
|
||||
- latest
|
||||
- next
|
||||
preid:
|
||||
description: 'Which prerelease identifier should be used? (Only for production releases)'
|
||||
required: false
|
||||
type: choice
|
||||
default: ''
|
||||
options:
|
||||
- ''
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- next
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
run-nightly:
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/nightly.yml
|
||||
secrets: inherit
|
||||
|
||||
run-dev:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'dev' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/dev-build.yml
|
||||
secrets: inherit
|
||||
|
||||
run-production:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'production' }}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
uses: ./.github/workflows/release.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
tag: ${{ inputs.tag }}
|
||||
preid: ${{ inputs.preid }}
|
||||
67
.github/workflows/release.yml
vendored
@@ -1,40 +1,47 @@
|
||||
name: 'Ionic Production Release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which version should be published?'
|
||||
required: true
|
||||
type: choice
|
||||
description: Which version should be published?
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- prepatch
|
||||
- preminor
|
||||
- premajor
|
||||
- prerelease
|
||||
type: string
|
||||
tag:
|
||||
description: 'Which npm tag should this be published to?'
|
||||
required: true
|
||||
type: choice
|
||||
description: Which npm tag should this be published to?
|
||||
options:
|
||||
- latest
|
||||
- next
|
||||
type: string
|
||||
preid:
|
||||
type: choice
|
||||
description: Which prerelease identifier should be used? This is only needed when version is "prepatch", "preminor", "premajor", or "prerelease".
|
||||
default: ''
|
||||
options:
|
||||
- ''
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- next
|
||||
description: 'Which prerelease identifier should be used? This is only needed when version is "prepatch", "preminor", "premajor", or "prerelease".'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
validate_version:
|
||||
name: ✅ Validate Version Input
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🔎 Ensure version is allowed
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
case "$VERSION" in
|
||||
patch|minor|major|prepatch|preminor|premajor|prerelease)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "::error::Invalid version input: '$VERSION'. Allowed values: patch, minor, major, prepatch, preminor, premajor, prerelease."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shell: bash
|
||||
|
||||
release-ionic:
|
||||
needs: [validate_version]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -47,8 +54,11 @@ jobs:
|
||||
finalize-release:
|
||||
needs: [release-ionic]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.IONITRON_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -75,8 +85,11 @@ jobs:
|
||||
# possible for them to push at the same time.
|
||||
needs: [finalize-release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# Pull the latest version of the reference
|
||||
# branch instead of the revision that triggered
|
||||
# the workflow otherwise we won't get the commit
|
||||
|
||||
32
.github/workflows/stencil-nightly.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
build-core-with-stencil-nightly:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-core-stencil-prerelease
|
||||
with:
|
||||
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
|
||||
@@ -35,21 +35,21 @@ jobs:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-clean-build
|
||||
|
||||
test-core-lint:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-lint
|
||||
|
||||
test-core-spec:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-spec
|
||||
with:
|
||||
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-screenshot
|
||||
with:
|
||||
shard: ${{ matrix.shard }}
|
||||
@@ -100,14 +100,14 @@ jobs:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-vue
|
||||
|
||||
build-vue-router:
|
||||
needs: [build-vue]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-vue-router
|
||||
|
||||
test-vue-e2e:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
needs: [build-vue, build-vue-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-vue-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -136,14 +136,14 @@ jobs:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-angular
|
||||
|
||||
build-angular-server:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-angular-server
|
||||
|
||||
test-angular-e2e:
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
needs: [build-angular, build-angular-server]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-angular-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -172,14 +172,14 @@ jobs:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-react
|
||||
|
||||
build-react-router:
|
||||
needs: [build-react]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-react-router
|
||||
|
||||
test-react-router-e2e:
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-router-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
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') }}
|
||||
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
|
||||
steps:
|
||||
- name: Notify success on Discord
|
||||
run: |
|
||||
|
||||
6
.github/workflows/update-screenshots.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/build-core
|
||||
|
||||
test-core-screenshot:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
needs: [build-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-core-screenshot
|
||||
with:
|
||||
shard: ${{ matrix.shard }}
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-core-screenshot]
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# Normally, we could just push with the
|
||||
# default GITHUB_TOKEN, but that will
|
||||
# not cause the build workflow
|
||||
|
||||
93
CHANGELOG.md
@@ -3,6 +3,99 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.17](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.17) (2026-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input:** prevent Android TalkBack from focusing label separately ([#30895](https://github.com/ionic-team/ionic-framework/issues/30895)) ([ab733b7](https://github.com/ionic-team/ionic-framework/commit/ab733b71dd355d9486757f219fe09acaefeeefcc))
|
||||
* **input:** prevent placeholder from overlapping start slot during scroll assist ([#30896](https://github.com/ionic-team/ionic-framework/issues/30896)) ([3b3318d](https://github.com/ionic-team/ionic-framework/commit/3b3318da513b199128f3822bd8226797cd118b0f))
|
||||
* **tab-bar:** prevent keyboard controller memory leak on rapid mount/unmount ([#30906](https://github.com/ionic-team/ionic-framework/issues/30906)) ([f99d000](https://github.com/ionic-team/ionic-framework/commit/f99d0007a8ffc9c7d3d2636e912c37c12112b21d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** prevent card modal animation on viewport resize when modal is closed ([#30894](https://github.com/ionic-team/ionic-framework/issues/30894)) ([e5634d4](https://github.com/ionic-team/ionic-framework/commit/e5634d45ee5fd32715f6e6b75e0448f74ee1f8f2)), closes [#30679](https://github.com/ionic-team/ionic-framework/issues/30679)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
|
||||
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
|
||||
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
|
||||
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
|
||||
|
||||
**Note:** Version bump only for package ionic-framework
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** allow interaction with parent content through sheet modals in child routes ([#30839](https://github.com/ionic-team/ionic-framework/issues/30839)) ([b9e3cf0](https://github.com/ionic-team/ionic-framework/commit/b9e3cf0f5aae79a1f27a07b102c77e51f24825f4)), closes [#30700](https://github.com/ionic-team/ionic-framework/issues/30700)
|
||||
* **modal:** prevent browser hang when using ModalController in Angular ([#30845](https://github.com/ionic-team/ionic-framework/issues/30845)) ([b164516](https://github.com/ionic-team/ionic-framework/commit/b1645168a7fb9378dc39a081c207b2de0e180089))
|
||||
* **popover:** recalculate the content dimensions after the header has fully loaded ([#30853](https://github.com/ionic-team/ionic-framework/issues/30853)) ([99dcf38](https://github.com/ionic-team/ionic-framework/commit/99dcf3810a0c32416996d1e992ddf63359965cfc))
|
||||
* **select, action-sheet:** use radio role for options ([#30769](https://github.com/ionic-team/ionic-framework/issues/30769)) ([1c89cf0](https://github.com/ionic-team/ionic-framework/commit/1c89cf06ac959f9c9a35a66f811227c244d3198b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.11](https://github.com/ionic-team/ionic-framework/compare/v8.7.10...v8.7.11) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime:** ensure datetime is shown when intersection observer fails to report visibility ([#30793](https://github.com/ionic-team/ionic-framework/issues/30793)) ([9d781db](https://github.com/ionic-team/ionic-framework/commit/9d781db662d213090d0b7198d0cdc5abb16fed1b)), closes [#30706](https://github.com/ionic-team/ionic-framework/issues/30706)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,96 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.17](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.17) (2026-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input:** prevent Android TalkBack from focusing label separately ([#30895](https://github.com/ionic-team/ionic-framework/issues/30895)) ([ab733b7](https://github.com/ionic-team/ionic-framework/commit/ab733b71dd355d9486757f219fe09acaefeeefcc))
|
||||
* **input:** prevent placeholder from overlapping start slot during scroll assist ([#30896](https://github.com/ionic-team/ionic-framework/issues/30896)) ([3b3318d](https://github.com/ionic-team/ionic-framework/commit/3b3318da513b199128f3822bd8226797cd118b0f))
|
||||
* **tab-bar:** prevent keyboard controller memory leak on rapid mount/unmount ([#30906](https://github.com/ionic-team/ionic-framework/issues/30906)) ([f99d000](https://github.com/ionic-team/ionic-framework/commit/f99d0007a8ffc9c7d3d2636e912c37c12112b21d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** prevent card modal animation on viewport resize when modal is closed ([#30894](https://github.com/ionic-team/ionic-framework/issues/30894)) ([e5634d4](https://github.com/ionic-team/ionic-framework/commit/e5634d45ee5fd32715f6e6b75e0448f74ee1f8f2)), closes [#30679](https://github.com/ionic-team/ionic-framework/issues/30679)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
|
||||
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
|
||||
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
|
||||
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
|
||||
|
||||
**Note:** Version bump only for package @ionic/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
|
||||
|
||||
**Note:** Version bump only for package @ionic/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** allow interaction with parent content through sheet modals in child routes ([#30839](https://github.com/ionic-team/ionic-framework/issues/30839)) ([b9e3cf0](https://github.com/ionic-team/ionic-framework/commit/b9e3cf0f5aae79a1f27a07b102c77e51f24825f4)), closes [#30700](https://github.com/ionic-team/ionic-framework/issues/30700)
|
||||
* **modal:** prevent browser hang when using ModalController in Angular ([#30845](https://github.com/ionic-team/ionic-framework/issues/30845)) ([b164516](https://github.com/ionic-team/ionic-framework/commit/b1645168a7fb9378dc39a081c207b2de0e180089))
|
||||
* **popover:** recalculate the content dimensions after the header has fully loaded ([#30853](https://github.com/ionic-team/ionic-framework/issues/30853)) ([99dcf38](https://github.com/ionic-team/ionic-framework/commit/99dcf3810a0c32416996d1e992ddf63359965cfc))
|
||||
* **select, action-sheet:** use radio role for options ([#30769](https://github.com/ionic-team/ionic-framework/issues/30769)) ([1c89cf0](https://github.com/ionic-team/ionic-framework/commit/1c89cf06ac959f9c9a35a66f811227c244d3198b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.11](https://github.com/ionic-team/ionic-framework/compare/v8.7.10...v8.7.11) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime:** ensure datetime is shown when intersection observer fails to report visibility ([#30793](https://github.com/ionic-team/ionic-framework/issues/30793)) ([9d781db](https://github.com/ionic-team/ionic-framework/commit/9d781db662d213090d0b7198d0cdc5abb16fed1b)), closes [#30706](https://github.com/ionic-team/ionic-framework/issues/30706)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05)
|
||||
|
||||
|
||||
|
||||
10679
core/package-lock.json
generated
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"description": "Base components for Ionic",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"keywords": [
|
||||
"ionic",
|
||||
"framework",
|
||||
@@ -37,10 +40,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@capacitor/core": "^8.0.0",
|
||||
"@capacitor/haptics": "^8.0.0",
|
||||
"@capacitor/keyboard": "^8.0.0",
|
||||
"@capacitor/status-bar": "^8.0.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
@@ -52,7 +55,7 @@
|
||||
"@stencil/sass": "^3.0.9",
|
||||
"@stencil/vue-output-target": "0.10.8",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/node": "^14.6.0",
|
||||
"@types/node": "^16.18.126",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"chalk": "^5.3.0",
|
||||
@@ -65,6 +68,7 @@
|
||||
"fs-extra": "^9.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-cli": "^29.7.0",
|
||||
"playwright-core": "^1.56.1",
|
||||
"prettier": "^2.6.1",
|
||||
"rollup": "^2.26.4",
|
||||
"sass": "^1.33.0",
|
||||
|
||||
@@ -1,30 +1,81 @@
|
||||
/**
|
||||
* This script is loaded in testing environments to set up the
|
||||
* document based on URL parameters.
|
||||
*
|
||||
* Test pages (e.g., `chip/test/basic/index.html`) are set to use
|
||||
* URL query parameters.
|
||||
*
|
||||
* Playwright test environments (e.g., `chip/test/basic/chip.e2e.ts`)
|
||||
* are set based on whether `setContent` or `goto` has been used:
|
||||
* - `setContent` uses URL hash parameters. Tests will break if
|
||||
* query parameters are used.
|
||||
* - `goto` uses URL query parameters.
|
||||
*
|
||||
* The following URL parameters are supported:
|
||||
* - `rtl`: Set to `true` to enable right-to-left directionality.
|
||||
* - `ionic:_testing`: Set to `true` to identify testing environments.
|
||||
* - `ionic:mode`: Set to `ios` or `md` to load a specific mode.
|
||||
* Defaults to `md`.
|
||||
* - `palette`: Set to `light`, `dark`, `high-contrast`, or
|
||||
* `high-contrast-dark` to load a specific palette. Defaults to `light`.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
if (window.location.search.indexOf('rtl=true') > -1) {
|
||||
/**
|
||||
* The `rtl` param is used to set the directionality of the
|
||||
* document. This can be `true` or `false`.
|
||||
*/
|
||||
const isRTL = window.location.search.indexOf('rtl=true') > -1 || window.location.hash.indexOf('rtl=true') > -1;
|
||||
|
||||
if (isRTL) {
|
||||
document.documentElement.setAttribute('dir', 'rtl');
|
||||
}
|
||||
|
||||
if (window.location.search.indexOf('ionic:_testing=true') > -1) {
|
||||
/**
|
||||
* The `ionic:_testing` param is used to identify testing
|
||||
* environments.
|
||||
*/
|
||||
const isTestEnv = window.location.search.indexOf('ionic:_testing=true') > -1 || window.location.hash.indexOf('ionic:_testing=true') > -1;
|
||||
|
||||
if (isTestEnv) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
* {
|
||||
caret-color: transparent !important;
|
||||
}`;
|
||||
* {
|
||||
caret-color: transparent !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* The term `palette` is used to as a param to match the
|
||||
* Ionic docs, plus here is already a `ionic:theme` query being
|
||||
* used for `md`, `ios`, and `ionic` themes.
|
||||
* The `palette` param is used to load a specific palette
|
||||
* for the theme.
|
||||
* The dark class will load the dark palette automatically
|
||||
* if no palette is specified through the URL.
|
||||
*
|
||||
* Values can be `light`, `dark`, `high-contrast`,
|
||||
* or `high-contrast-dark`. Default to `light` for tests.
|
||||
*/
|
||||
const palette = window.location.search.match(/palette=([a-z]+)/);
|
||||
if (palette && palette[1] !== 'light') {
|
||||
const validPalettes = ['light', 'dark', 'high-contrast', 'high-contrast-dark'];
|
||||
const paletteQuery = window.location.search.match(/palette=([a-z-]+)/);
|
||||
const paletteHash = window.location.hash.match(/palette=([a-z-]+)/);
|
||||
const darkClass = document.body?.classList.contains('ion-palette-dark') ? 'dark' : null;
|
||||
const highContrastClass = document.body?.classList.contains('ion-palette-high-contrast') ? 'high-contrast' : null;
|
||||
const highContrastDarkClass = darkClass && highContrastClass ? 'high-contrast-dark' : null;
|
||||
|
||||
let paletteName = paletteQuery?.[1] || paletteHash?.[1] || highContrastDarkClass || darkClass || highContrastClass || 'light';
|
||||
|
||||
if (!validPalettes.includes(paletteName)) {
|
||||
console.warn(`Invalid palette name: '${paletteName}'. Falling back to 'light' palette.`);
|
||||
paletteName = 'light';
|
||||
}
|
||||
|
||||
if (paletteName !== 'light') {
|
||||
const linkTag = document.createElement('link');
|
||||
linkTag.setAttribute('rel', 'stylesheet');
|
||||
linkTag.setAttribute('type', 'text/css');
|
||||
linkTag.setAttribute('href', `/css/palettes/${palette[1]}.always.css`);
|
||||
linkTag.setAttribute('href', `/css/palettes/${paletteName}.always.css`);
|
||||
document.head.appendChild(linkTag);
|
||||
}
|
||||
|
||||
|
||||
4
core/src/components.d.ts
vendored
@@ -868,6 +868,10 @@ export namespace Components {
|
||||
* Get the element where the actual scrolling takes place. This element can be used to subscribe to `scroll` events or manually modify `scrollTop`. However, it's recommended to use the API provided by `ion-content`: i.e. Using `ionScroll`, `ionScrollStart`, `ionScrollEnd` for scrolling events and `scrollToPoint()` to scroll the content into a certain point.
|
||||
*/
|
||||
"getScrollElement": () => Promise<HTMLElement>;
|
||||
/**
|
||||
* Recalculate content dimensions. Called by overlays (e.g., popover) when sibling elements like headers or footers have finished rendering and their heights are available, ensuring accurate offset-top calculations.
|
||||
*/
|
||||
"recalculateDimensions": () => Promise<void>;
|
||||
/**
|
||||
* Scroll by a specified X/Y distance in the component.
|
||||
* @param x The amount to scroll by on the horizontal axis.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Watch, Component, Element, Event, Host, Method, Prop, h, readTask } from '@stencil/core';
|
||||
import { Watch, Component, Element, Event, Host, Listen, Method, Prop, State, h, readTask } from '@stencil/core';
|
||||
import type { Gesture } from '@utils/gesture';
|
||||
import { createButtonActiveGesture } from '@utils/gesture/button-active';
|
||||
import { raf } from '@utils/helpers';
|
||||
@@ -46,11 +46,18 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
private wrapperEl?: HTMLElement;
|
||||
private groupEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
private hasRadioButtons = false;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: any;
|
||||
|
||||
/**
|
||||
* The ID of the currently active/selected radio button.
|
||||
* Used for keyboard navigation and ARIA attributes.
|
||||
*/
|
||||
@State() activeRadioId?: string;
|
||||
|
||||
@Element() el!: HTMLIonActionSheetElement;
|
||||
|
||||
/** @internal */
|
||||
@@ -81,6 +88,22 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
* An array of buttons for the action sheet.
|
||||
*/
|
||||
@Prop() buttons: (ActionSheetButton | string)[] = [];
|
||||
@Watch('buttons')
|
||||
buttonsChanged() {
|
||||
const radioButtons = this.getRadioButtons();
|
||||
this.hasRadioButtons = radioButtons.length > 0;
|
||||
|
||||
// Initialize activeRadioId when buttons change
|
||||
if (this.hasRadioButtons) {
|
||||
const checkedButton = radioButtons.find((b) => b.htmlAttributes?.['aria-checked'] === 'true');
|
||||
|
||||
if (checkedButton) {
|
||||
const allButtons = this.getButtons();
|
||||
const checkedIndex = allButtons.indexOf(checkedButton);
|
||||
this.activeRadioId = this.getButtonId(checkedButton, checkedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are
|
||||
@@ -277,12 +300,53 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all buttons regardless of role.
|
||||
*/
|
||||
private getButtons(): ActionSheetButton[] {
|
||||
return this.buttons.map((b) => {
|
||||
return typeof b === 'string' ? { text: b } : b;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all radio buttons (buttons with role="radio").
|
||||
*/
|
||||
private getRadioButtons(): ActionSheetButton[] {
|
||||
return this.getButtons().filter((b) => {
|
||||
const role = b.htmlAttributes?.role;
|
||||
return role === 'radio' && !isCancel(role);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle radio button selection and update aria-checked state.
|
||||
*
|
||||
* @param button The radio button that was selected.
|
||||
*/
|
||||
private selectRadioButton(button: ActionSheetButton) {
|
||||
const buttonId = this.getButtonId(button);
|
||||
|
||||
// Set the active radio ID (this will trigger a re-render and update aria-checked)
|
||||
this.activeRadioId = buttonId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate an ID for a button.
|
||||
*
|
||||
* @param button The button for which to get the ID.
|
||||
* @param index Optional index of the button in the buttons array.
|
||||
* @returns The ID of the button.
|
||||
*/
|
||||
private getButtonId(button: ActionSheetButton, index?: number): string {
|
||||
if (button.id) {
|
||||
return button.id;
|
||||
}
|
||||
const allButtons = this.getButtons();
|
||||
const buttonIndex = index !== undefined ? index : allButtons.indexOf(button);
|
||||
return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`;
|
||||
}
|
||||
|
||||
private onBackdropTap = () => {
|
||||
this.dismiss(undefined, BACKDROP);
|
||||
};
|
||||
@@ -295,6 +359,96 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the action sheet has radio buttons, we want to follow the
|
||||
* keyboard navigation pattern for radio groups:
|
||||
* - Arrow Down/Right: Move to the next radio button (wrap to first if at end)
|
||||
* - Arrow Up/Left: Move to the previous radio button (wrap to last if at start)
|
||||
* - Space/Enter: Select the focused radio button and trigger its handler
|
||||
*/
|
||||
@Listen('keydown')
|
||||
onKeydown(ev: KeyboardEvent) {
|
||||
// Only handle keyboard navigation if we have radio buttons
|
||||
if (!this.hasRadioButtons || !this.presented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = ev.target as HTMLElement;
|
||||
|
||||
// Ignore if the target element is not within the action sheet or not a radio button
|
||||
if (
|
||||
!this.el.contains(target) ||
|
||||
!target.classList.contains('action-sheet-button') ||
|
||||
target.getAttribute('role') !== 'radio'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all radio button elements and filter out disabled ones
|
||||
const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter(
|
||||
(el) => !(el as HTMLButtonElement).disabled
|
||||
) as HTMLButtonElement[];
|
||||
const currentIndex = radios.findIndex((radio) => radio.id === target.id);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allButtons = this.getButtons();
|
||||
const radioButtons = this.getRadioButtons();
|
||||
/**
|
||||
* Build a map of button element IDs to their ActionSheetButton
|
||||
* config objects.
|
||||
* This allows us to quickly look up which button config corresponds
|
||||
* to a DOM element when handling keyboard navigation
|
||||
* (e.g., whenuser presses Space/Enter or arrow keys).
|
||||
* The key is the ID that was set on the DOM element during render,
|
||||
* and the value is the ActionSheetButton config that contains the
|
||||
* handler and other properties.
|
||||
*/
|
||||
const buttonIdMap = new Map<string, ActionSheetButton>();
|
||||
|
||||
radioButtons.forEach((b) => {
|
||||
const allIndex = allButtons.indexOf(b);
|
||||
const buttonId = this.getButtonId(b, allIndex);
|
||||
buttonIdMap.set(buttonId, b);
|
||||
});
|
||||
|
||||
let nextEl: HTMLButtonElement | undefined;
|
||||
|
||||
if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1];
|
||||
} else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1];
|
||||
} else if (ev.key === ' ' || ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const button = buttonIdMap.get(target.id);
|
||||
if (button) {
|
||||
this.selectRadioButton(button);
|
||||
this.buttonClick(button);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the next radio button
|
||||
if (nextEl) {
|
||||
const button = buttonIdMap.get(nextEl.id);
|
||||
if (button) {
|
||||
this.selectRadioButton(button);
|
||||
nextEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
this.triggerChanged();
|
||||
@@ -312,6 +466,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
if (!this.htmlAttributes?.id) {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
// Initialize activeRadioId for radio buttons
|
||||
this.buttonsChanged();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
@@ -355,8 +511,82 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
this.triggerChanged();
|
||||
}
|
||||
|
||||
private renderActionSheetButtons(filteredButtons: ActionSheetButton[]) {
|
||||
const mode = getIonMode(this);
|
||||
const { activeRadioId } = this;
|
||||
|
||||
return filteredButtons.map((b, index) => {
|
||||
const isRadio = b.htmlAttributes?.role === 'radio';
|
||||
const buttonId = this.getButtonId(b, index);
|
||||
const radioButtons = this.getRadioButtons();
|
||||
const isActiveRadio = isRadio && buttonId === activeRadioId;
|
||||
const isFirstRadio = isRadio && b === radioButtons[0];
|
||||
|
||||
// For radio buttons, set tabindex: 0 for the active one, -1 for others
|
||||
// For non-radio buttons, use default tabindex (undefined, which means 0)
|
||||
|
||||
/**
|
||||
* For radio buttons, set tabindex based on activeRadioId
|
||||
* - If the button is the active radio, tabindex is 0
|
||||
* - If no radio is active, the first radio button should have tabindex 0
|
||||
* - All other radio buttons have tabindex -1
|
||||
* For non-radio buttons, use default tabindex (undefined, which means 0)
|
||||
*/
|
||||
let tabIndex: number | undefined;
|
||||
|
||||
if (isRadio) {
|
||||
// Focus on the active radio button
|
||||
if (isActiveRadio) {
|
||||
tabIndex = 0;
|
||||
} else if (!activeRadioId && isFirstRadio) {
|
||||
// No active radio, first radio gets focus
|
||||
tabIndex = 0;
|
||||
} else {
|
||||
// All other radios are not focusable
|
||||
tabIndex = -1;
|
||||
}
|
||||
} else {
|
||||
tabIndex = undefined;
|
||||
}
|
||||
|
||||
// For radio buttons, set aria-checked based on activeRadioId
|
||||
// Otherwise, use the value from htmlAttributes if provided
|
||||
const htmlAttrs = { ...b.htmlAttributes };
|
||||
if (isRadio) {
|
||||
htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
{...htmlAttrs}
|
||||
role={isRadio ? 'radio' : undefined}
|
||||
type="button"
|
||||
id={buttonId}
|
||||
class={{
|
||||
...buttonClass(b),
|
||||
'action-sheet-selected': isActiveRadio,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isRadio) {
|
||||
this.selectRadioButton(b);
|
||||
}
|
||||
this.buttonClick(b);
|
||||
}}
|
||||
disabled={b.disabled}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<span class="action-sheet-button-inner">
|
||||
{b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />}
|
||||
{b.text}
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { header, htmlAttributes, overlayIndex } = this;
|
||||
const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this;
|
||||
const mode = getIonMode(this);
|
||||
const allButtons = this.getButtons();
|
||||
const cancelButton = allButtons.find((b) => b.role === 'cancel');
|
||||
@@ -388,7 +618,11 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
|
||||
<div class="action-sheet-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<div class="action-sheet-container">
|
||||
<div class="action-sheet-group" ref={(el) => (this.groupEl = el)}>
|
||||
<div
|
||||
class="action-sheet-group"
|
||||
ref={(el) => (this.groupEl = el)}
|
||||
role={hasRadioButtons ? 'radiogroup' : undefined}
|
||||
>
|
||||
{header !== undefined && (
|
||||
<div
|
||||
id={headerID}
|
||||
@@ -401,22 +635,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
{this.subHeader && <div class="action-sheet-sub-title">{this.subHeader}</div>}
|
||||
</div>
|
||||
)}
|
||||
{buttons.map((b) => (
|
||||
<button
|
||||
{...b.htmlAttributes}
|
||||
type="button"
|
||||
id={b.id}
|
||||
class={buttonClass(b)}
|
||||
onClick={() => this.buttonClick(b)}
|
||||
disabled={b.disabled}
|
||||
>
|
||||
<span class="action-sheet-button-inner">
|
||||
{b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />}
|
||||
{b.text}
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
))}
|
||||
{this.renderActionSheetButtons(buttons)}
|
||||
</div>
|
||||
|
||||
{cancelButton && (
|
||||
|
||||
@@ -134,3 +134,58 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('action-sheet: radio buttons'), () => {
|
||||
test('should render action sheet with radio buttons correctly', async ({ page }) => {
|
||||
await page.goto(`/src/components/action-sheet/test/a11y`, config);
|
||||
|
||||
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
|
||||
const button = page.locator('#radioButtons');
|
||||
|
||||
await button.click();
|
||||
await ionActionSheetDidPresent.next();
|
||||
|
||||
const actionSheet = page.locator('ion-action-sheet');
|
||||
|
||||
const radioButtons = actionSheet.locator('.action-sheet-button[role="radio"]');
|
||||
await expect(radioButtons).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should navigate radio buttons with keyboard', async ({ page, pageUtils }) => {
|
||||
await page.goto(`/src/components/action-sheet/test/a11y`, config);
|
||||
|
||||
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
|
||||
const button = page.locator('#radioButtons');
|
||||
|
||||
await button.click();
|
||||
await ionActionSheetDidPresent.next();
|
||||
|
||||
// Focus on the radios
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
// Verify the first focusable radio button is focused
|
||||
let focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim());
|
||||
expect(focusedElement).toBe('Option 2');
|
||||
|
||||
// Navigate to the next radio button
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
// Verify the first radio button is focused again (wrap around)
|
||||
focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim());
|
||||
expect(focusedElement).toBe('Option 1');
|
||||
|
||||
// Navigate to the next radio button
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
// Navigate to the cancel button
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim());
|
||||
expect(focusedElement).toBe('Cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<button class="expand" id="ariaLabelCancelButton" onclick="presentAriaLabelCancelButton()">
|
||||
Aria Label Cancel Button
|
||||
</button>
|
||||
<button class="expand" id="radioButtons" onclick="presentRadioButtons()">Radio Buttons</button>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -100,6 +101,32 @@
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function presentRadioButtons() {
|
||||
openActionSheet({
|
||||
header: 'Select an option',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Option 1',
|
||||
htmlAttributes: {
|
||||
role: 'radio',
|
||||
'aria-checked': 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Option 2',
|
||||
htmlAttributes: {
|
||||
role: 'radio',
|
||||
'aria-checked': 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,20 +18,66 @@ configs({ directions: ['ltr'] }).forEach(({ config, title, screenshot }) => {
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`app-${screenshotModifier}-diff`));
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/app/test/safe-area`, config);
|
||||
});
|
||||
test('should not have visual regressions with action sheet', async ({ page }) => {
|
||||
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
|
||||
|
||||
test.describe(title('Ionic safe area variables'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const htmlTag = page.locator('html');
|
||||
const hasSafeAreaClass = await htmlTag.evaluate((el) => el.classList.contains('safe-area'));
|
||||
|
||||
expect(hasSafeAreaClass).toBe(true);
|
||||
});
|
||||
|
||||
test('should not have visual regressions with action sheet', async ({ page }) => {
|
||||
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
|
||||
});
|
||||
test('should not have visual regressions with menu', async ({ page }) => {
|
||||
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
|
||||
});
|
||||
test('should not have visual regressions with picker', async ({ page }) => {
|
||||
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
|
||||
});
|
||||
test('should not have visual regressions with toast', async ({ page }) => {
|
||||
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
|
||||
});
|
||||
});
|
||||
test('should not have visual regressions with menu', async ({ page }) => {
|
||||
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
|
||||
});
|
||||
test('should not have visual regressions with picker', async ({ page }) => {
|
||||
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
|
||||
});
|
||||
test('should not have visual regressions with toast', async ({ page }) => {
|
||||
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
|
||||
|
||||
test.describe(title('Capacitor safe area variables'), () => {
|
||||
test('should use safe-area-inset vars when safe-area class is not defined', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
const html = document.documentElement;
|
||||
|
||||
// Remove the safe area class
|
||||
html.classList.remove('safe-area');
|
||||
|
||||
// Set the safe area inset variables
|
||||
html.style.setProperty('--safe-area-inset-top', '10px');
|
||||
html.style.setProperty('--safe-area-inset-bottom', '20px');
|
||||
html.style.setProperty('--safe-area-inset-left', '30px');
|
||||
html.style.setProperty('--safe-area-inset-right', '40px');
|
||||
});
|
||||
|
||||
const top = await page.evaluate(() =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-top').trim()
|
||||
);
|
||||
const bottom = await page.evaluate(() =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-bottom').trim()
|
||||
);
|
||||
const left = await page.evaluate(() =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-left').trim()
|
||||
);
|
||||
const right = await page.evaluate(() =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-right').trim()
|
||||
);
|
||||
|
||||
expect(top).toBe('10px');
|
||||
expect(bottom).toBe('20px');
|
||||
expect(left).toBe('30px');
|
||||
expect(right).toBe('40px');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,6 +170,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
*/
|
||||
@Watch('aria-checked')
|
||||
@Watch('aria-label')
|
||||
@Watch('aria-pressed')
|
||||
onAriaChanged(newValue: string, _oldValue: string, propName: string) {
|
||||
this.inheritedAttributes = {
|
||||
...this.inheritedAttributes,
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
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 }) => {
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
test.describe(title('chip: states'), () => {
|
||||
test('should render disabled state', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-chip disabled="true">
|
||||
<ion-label>Disabled</ion-label>
|
||||
</ion-chip>`,
|
||||
config
|
||||
);
|
||||
await page.goto(`/src/components/chip/test/states`, config);
|
||||
|
||||
const chip = page.locator('ion-chip');
|
||||
const container = page.locator('#disabled');
|
||||
|
||||
await expect(chip).toHaveScreenshot(screenshot(`chip-disabled`));
|
||||
await expect(container).toHaveScreenshot(screenshot(`chip-disabled`));
|
||||
});
|
||||
|
||||
test('should render activated state', async ({ page }) => {
|
||||
await page.goto(`/src/components/chip/test/states`, config);
|
||||
|
||||
const container = page.locator('#activated');
|
||||
|
||||
await expect(container).toHaveScreenshot(screenshot(`chip-activated`));
|
||||
});
|
||||
|
||||
test('should custom chip', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 27 KiB |
@@ -19,211 +19,285 @@
|
||||
<ion-content>
|
||||
<h3>Default</h3>
|
||||
|
||||
<p>
|
||||
<ion-chip>
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip style="border-radius: 4px">
|
||||
<ion-label>Border Radius</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip>
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>With Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip>
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>With Avatar</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip>
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>With Icon and Avatar</ion-label>
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
<p>
|
||||
<ion-chip class="ion-focused">
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip style="border-radius: 4px" class="ion-focused">
|
||||
<ion-label>Border Radius</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="ion-focused">
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>With Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="ion-focused">
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>With Avatar</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="ion-focused">
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>With Icon and Avatar</ion-label>
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
<div id="default">
|
||||
<p>
|
||||
<ion-chip>
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="primary">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<h3>Colors</h3>
|
||||
<p>
|
||||
<ion-chip color="primary">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary">
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>Tertiary with Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success">
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>Success with Avatar</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning">
|
||||
<ion-label>Warning with Icon</ion-label>
|
||||
<ion-icon name="calendar"></ion-icon>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
<p>
|
||||
<ion-chip color="primary" class="ion-focused">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" class="ion-focused">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary" class="ion-focused">
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>Tertiary with Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success" class="ion-focused">
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
<ion-label>Success with Avatar</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning" class="ion-focused">
|
||||
<ion-label>Warning with Icon</ion-label>
|
||||
<ion-icon name="calendar"></ion-icon>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger" class="ion-focused">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light" class="ion-focused">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium" class="ion-focused">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark" class="ion-focused">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<h3>Outline</h3>
|
||||
|
||||
<p>
|
||||
<ion-chip outline>
|
||||
<ion-label>Outline</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger">
|
||||
<ion-label>Danger Outline</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary">
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>Secondary Outline with Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary">
|
||||
<ion-label>Primary Outline with Avatar</ion-label>
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
</ion-chip>
|
||||
<ion-chip outline>
|
||||
<ion-icon name="git-pull-request"></ion-icon>
|
||||
<ion-label>Outline with Icon and Avatar</ion-label>
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
<p>
|
||||
<ion-chip outline class="ion-focused">
|
||||
<ion-label>Outline</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger" class="ion-focused">
|
||||
<ion-label>Danger Outline</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary" class="ion-focused">
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>Secondary Outline with Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary" class="ion-focused">
|
||||
<ion-label>Primary Outline with Avatar</ion-label>
|
||||
<ion-avatar>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjYzVkYmZmIiBkPSJNMCAwaDUxMnY1MTJIMHoiLz48cGF0aCBkPSJNMjU2IDMwNGM2MS42IDAgMTEyLTUwLjQgMTEyLTExMlMzMTcuNiA4MCAyNTYgODBzLTExMiA1MC40LTExMiAxMTIgNTAuNCAxMTIgMTEyIDExMnptMCA0MGMtNzQuMiAwLTIyNCAzNy44LTIyNCAxMTJ2NTZoNDQ4di01NmMwLTc0LjItMTQ5LjgtMTEyLTIyNC0xMTJ6IiBmaWxsPSIjODJhZWZmIi8+PC9zdmc+"
|
||||
/>
|
||||
</ion-avatar>
|
||||
</ion-chip>
|
||||
<ion-chip outline class="ion-focused">
|
||||
<ion-icon name="git-pull-request"></ion-icon>
|
||||
<ion-label>Outline with Icon and Avatar</ion-label>
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
<p>
|
||||
<ion-chip outline>
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="tertiary">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="success">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="warning">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="light">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="medium">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="dark">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Disabled</h3>
|
||||
<div id="disabled">
|
||||
<p>
|
||||
<ion-chip disabled>
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="primary" disabled>
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" disabled>
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary" disabled>
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success" disabled>
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning" disabled>
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger" disabled>
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light" disabled>
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium" disabled>
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark" disabled>
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<ion-chip outline disabled>
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary" disabled>
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary" disabled>
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="tertiary" disabled>
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="success" disabled>
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="warning" disabled>
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger" disabled>
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="light" disabled>
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="medium" disabled>
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="dark" disabled>
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Activated</h3>
|
||||
<div id="activated">
|
||||
<p>
|
||||
<ion-chip class="ion-activated">
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="primary" class="ion-activated">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" class="ion-activated">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary" class="ion-activated">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success" class="ion-activated">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning" class="ion-activated">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger" class="ion-activated">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light" class="ion-activated">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium" class="ion-activated">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark" class="ion-activated">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<ion-chip outline class="ion-activated">
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary" class="ion-activated">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary" class="ion-activated">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="tertiary" class="ion-activated">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="success" class="ion-activated">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="warning" class="ion-activated">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger" class="ion-activated">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="light" class="ion-activated">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="medium" class="ion-activated">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="dark" class="ion-activated">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Focused</h3>
|
||||
<p>
|
||||
<ion-chip disabled>
|
||||
<ion-label>Disabled</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger" class="ion-focused" disabled>
|
||||
<ion-label>Disabled Outline</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" class="ion-focused" disabled>
|
||||
<ion-icon name="checkmark-circle"></ion-icon>
|
||||
<ion-label>Disabled Secondary with Icon</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline class="ion-focused" disabled>
|
||||
<ion-icon name="git-pull-request"></ion-icon>
|
||||
<ion-label>Disabled Outline with Icon and Avatar</ion-label>
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</ion-chip>
|
||||
`ion-chip` is not focusable by default, meaning it doesn't use `ion-focusable`. But it does have focus styles
|
||||
that target the `:focus` pseudo-class. In order to see the focus styles, you need to add tabindex="0" and tab
|
||||
into the chip.
|
||||
</p>
|
||||
<div id="focused">
|
||||
<p>
|
||||
<ion-chip tabindex="0">
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="primary" tabindex="0">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" tabindex="0">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="tertiary" tabindex="0">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="success" tabindex="0">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="warning" tabindex="0">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="danger" tabindex="0">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="light" tabindex="0">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="medium" tabindex="0">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip color="dark" tabindex="0">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<ion-chip outline tabindex="0">
|
||||
<ion-label>Default</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="primary" tabindex="0">
|
||||
<ion-label>Primary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="secondary" tabindex="0">
|
||||
<ion-label>Secondary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="tertiary" tabindex="0">
|
||||
<ion-label>Tertiary</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="success" tabindex="0">
|
||||
<ion-label>Success</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="warning" tabindex="0">
|
||||
<ion-label>Warning</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="danger" tabindex="0">
|
||||
<ion-label>Danger</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="light" tabindex="0">
|
||||
<ion-label>Light</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="medium" tabindex="0">
|
||||
<ion-label>Medium</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip outline color="dark" tabindex="0">
|
||||
<ion-label>Dark</ion-label>
|
||||
</ion-chip>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Custom</h3>
|
||||
|
||||
@@ -232,9 +306,6 @@
|
||||
<ion-chip class="wide" text="wide">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-chip>
|
||||
<ion-chip class="wide ion-focused" text="wide">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
|
||||
<!-- Custom Colors -->
|
||||
@@ -245,25 +316,19 @@
|
||||
<ion-chip color="secondary" class="custom">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-chip>
|
||||
<ion-chip class="custom ion-focused">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-chip>
|
||||
<ion-chip color="secondary" class="custom ion-focused">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-chip>
|
||||
</p>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
var buttons = document.querySelectorAll('ion-chip');
|
||||
var chips = document.querySelectorAll('ion-chip');
|
||||
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].addEventListener('click', (event) => onClick(event));
|
||||
for (var i = 0; i < chips.length; i++) {
|
||||
chips[i].addEventListener('click', (event) => onClick(event));
|
||||
}
|
||||
|
||||
function onClick(ev) {
|
||||
console.log('clicked the button', ev.target.innerText);
|
||||
console.log('clicked the chip', ev.target.innerText);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -254,6 +254,17 @@ export class Content implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate content dimensions. Called by overlays (e.g., popover) when
|
||||
* sibling elements like headers or footers have finished rendering and their
|
||||
* heights are available, ensuring accurate offset-top calculations.
|
||||
* @internal
|
||||
*/
|
||||
@Method()
|
||||
async recalculateDimensions(): Promise<void> {
|
||||
readTask(() => this.readDimensions());
|
||||
}
|
||||
|
||||
private readDimensions() {
|
||||
const page = getPageElement(this.el);
|
||||
const top = Math.max(this.el.offsetTop, 0);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { EventSpy } from '@utils/test/playwright';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
@@ -176,5 +176,34 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
await ionModalDidPresent.next();
|
||||
await expect(datetime).toBeVisible();
|
||||
});
|
||||
test('should set datetime ready state and keep calendar interactive when reopening modal', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
|
||||
});
|
||||
|
||||
const openAndInteract = async () => {
|
||||
await page.click('#date-button');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.locator('ion-datetime.datetime-ready').waitFor();
|
||||
|
||||
const calendarBody = datetime.locator('.calendar-body');
|
||||
await expect(calendarBody).toBeVisible();
|
||||
};
|
||||
|
||||
await openAndInteract();
|
||||
|
||||
const firstEnabledDay = datetime.locator('.calendar-day:not([disabled])').first();
|
||||
await firstEnabledDay.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await modal.evaluate((el: HTMLIonModalElement) => el.dismiss());
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await openAndInteract();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1101,6 +1101,32 @@ export class Datetime implements ComponentInterface {
|
||||
this.initializeKeyboardListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* Fallback to ensure the datetime becomes ready even if
|
||||
* IntersectionObserver never reports it as intersecting.
|
||||
*
|
||||
* This is primarily used in environments where the observer
|
||||
* might not fire as expected, such as when running under
|
||||
* synthetic tests that stub IntersectionObserver.
|
||||
*/
|
||||
private ensureReadyIfVisible = () => {
|
||||
if (this.el.classList.contains('datetime-ready')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = this.el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializeListeners();
|
||||
|
||||
writeTask(() => {
|
||||
this.el.classList.add('datetime-ready');
|
||||
});
|
||||
};
|
||||
|
||||
componentDidLoad() {
|
||||
const { el, intersectionTrackerRef } = this;
|
||||
|
||||
@@ -1141,6 +1167,18 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
raf(() => visibleIO?.observe(intersectionTrackerRef!));
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* Fallback: If IntersectionObserver never reports that the
|
||||
* datetime is visible but the host clearly has layout, ensure
|
||||
* we still initialize listeners and mark the component as ready.
|
||||
*
|
||||
* We schedule this after everything has had a chance to run.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
this.ensureReadyIfVisible();
|
||||
}, 100);
|
||||
|
||||
/**
|
||||
* We need to clean up listeners when the datetime is hidden
|
||||
* in a popover/modal so that we can properly scroll containers
|
||||
|
||||
@@ -394,6 +394,61 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Synthetic IntersectionObserver fallback behavior.
|
||||
*
|
||||
* This test stubs IntersectionObserver so that the callback
|
||||
* never reports an intersecting entry. The datetime should
|
||||
* still become ready via its internal fallback logic.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: IO fallback'), () => {
|
||||
test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
|
||||
});
|
||||
|
||||
await page.addInitScript(() => {
|
||||
const OriginalIO = window.IntersectionObserver;
|
||||
(window as any).IntersectionObserver = function (callback: any, options: any) {
|
||||
const instance = new OriginalIO(() => {}, options);
|
||||
const originalObserve = instance.observe.bind(instance);
|
||||
|
||||
instance.observe = (target: Element) => {
|
||||
originalObserve(target);
|
||||
callback([
|
||||
{
|
||||
isIntersecting: false,
|
||||
target,
|
||||
} as IntersectionObserverEntry,
|
||||
]);
|
||||
};
|
||||
|
||||
return instance;
|
||||
} as any;
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-05-03"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
// Give the fallback a short amount of time to run
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await expect(datetime).toHaveClass(/datetime-ready/);
|
||||
|
||||
const calendarBody = datetime.locator('.calendar-body');
|
||||
await expect(calendarBody).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* We are setting RTL on the component
|
||||
* instead, so we don't need to test
|
||||
|
||||
@@ -22,6 +22,7 @@ export class Footer implements ComponentInterface {
|
||||
private scrollEl?: HTMLElement;
|
||||
private contentScrollCallback?: () => void;
|
||||
private keyboardCtrl: KeyboardController | null = null;
|
||||
private keyboardCtrlPromise: Promise<KeyboardController> | null = null;
|
||||
|
||||
@State() private keyboardVisible = false;
|
||||
|
||||
@@ -52,7 +53,7 @@ export class Footer implements ComponentInterface {
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
|
||||
const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
|
||||
/**
|
||||
* If the keyboard is hiding, then we need to wait
|
||||
* for the webview to resize. Otherwise, the footer
|
||||
@@ -64,11 +65,32 @@ export class Footer implements ComponentInterface {
|
||||
|
||||
this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
|
||||
});
|
||||
this.keyboardCtrlPromise = promise;
|
||||
|
||||
const keyboardCtrl = await promise;
|
||||
|
||||
/**
|
||||
* Only assign if this is still the current promise.
|
||||
* Otherwise, a new connectedCallback has started or
|
||||
* disconnectedCallback was called, so destroy this instance.
|
||||
*/
|
||||
if (this.keyboardCtrlPromise === promise) {
|
||||
this.keyboardCtrl = keyboardCtrl;
|
||||
this.keyboardCtrlPromise = null;
|
||||
} else {
|
||||
keyboardCtrl.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.keyboardCtrlPromise) {
|
||||
this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
|
||||
this.keyboardCtrlPromise = null;
|
||||
}
|
||||
|
||||
if (this.keyboardCtrl) {
|
||||
this.keyboardCtrl.destroy();
|
||||
this.keyboardCtrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
box-shadow: $header-md-box-shadow;
|
||||
}
|
||||
|
||||
.header-collapse-condense {
|
||||
.header-md.header-collapse-condense {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
71
core/src/components/header/test/condense-modal/header.e2e.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This test verifies that collapsible headers with mode="ios" work correctly
|
||||
* when both iOS and MD stylesheets are loaded. The bug occurred because
|
||||
* `.header-collapse-condense { display: none }` in the MD stylesheet was not
|
||||
* scoped to `.header-md`, causing it to hide iOS condense headers when both
|
||||
* stylesheets were present.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('header: condense with iOS mode override'), () => {
|
||||
test('should show iOS condense header when both MD and iOS styles are loaded', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/29929',
|
||||
});
|
||||
|
||||
// Include both an MD header and an iOS modal to force both stylesheets to load
|
||||
await page.setContent(
|
||||
`
|
||||
<!-- MD header to force MD stylesheet to load -->
|
||||
<ion-header mode="md" id="mdHeader">
|
||||
<ion-toolbar>
|
||||
<ion-title>MD Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<!-- Modal with iOS condense header -->
|
||||
<ion-modal>
|
||||
<ion-header mode="ios" id="smallTitleHeader">
|
||||
<ion-toolbar>
|
||||
<ion-title>Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content fullscreen="true">
|
||||
<ion-header collapse="condense" mode="ios" id="largeTitleHeader">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Large Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<p>Content</p>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await modal.evaluate((el: HTMLIonModalElement) => el.present());
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const largeTitleHeader = modal.locator('#largeTitleHeader');
|
||||
|
||||
// The large title header should be visible, not hidden by MD styles
|
||||
await expect(largeTitleHeader).toBeVisible();
|
||||
|
||||
// Verify it has the iOS mode class
|
||||
await expect(largeTitleHeader).toHaveClass(/header-ios/);
|
||||
|
||||
// Verify it does NOT have display: none applied
|
||||
// This would fail if the MD stylesheet's unscoped .header-collapse-condense rule applies
|
||||
const display = await largeTitleHeader.evaluate((el) => {
|
||||
return window.getComputedStyle(el).display;
|
||||
});
|
||||
expect(display).not.toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,9 +126,8 @@ export class InputPasswordToggle implements ComponentInterface {
|
||||
color={color}
|
||||
fill="clear"
|
||||
shape="round"
|
||||
aria-checked={isPasswordVisible ? 'true' : 'false'}
|
||||
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
|
||||
role="switch"
|
||||
aria-pressed={isPasswordVisible ? 'true' : 'false'}
|
||||
type="button"
|
||||
onPointerDown={(ev) => {
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
});
|
||||
|
||||
test.describe(title('input password toggle: aria attributes'), () => {
|
||||
test('should inherit aria attributes to inner button on load', async ({ page }) => {
|
||||
test('should have correct aria attributes on load', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input label="input" type="password">
|
||||
@@ -35,9 +35,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
const nativeButton = page.locator('ion-input-password-toggle button');
|
||||
|
||||
await expect(nativeButton).toHaveAttribute('aria-label', 'Show password');
|
||||
await expect(nativeButton).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(nativeButton).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
test('should inherit aria attributes to inner button after toggle', async ({ page }) => {
|
||||
test('should update aria attributes after toggle', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input label="input" type="password">
|
||||
@@ -51,7 +51,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
await nativeButton.click();
|
||||
|
||||
await expect(nativeButton).toHaveAttribute('aria-label', 'Hide password');
|
||||
await expect(nativeButton).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(nativeButton).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,9 +165,13 @@
|
||||
// otherwise the .input-cover will not be rendered at all
|
||||
// The input cover is not clickable when the input is disabled
|
||||
.cloned-input {
|
||||
@include position(0, null, 0, 0);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
// Reset height since absolute positioning with top/bottom handles sizing
|
||||
height: auto;
|
||||
max-height: none;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export class Input implements ComponentInterface {
|
||||
private inputId = `ion-input-${inputIds++}`;
|
||||
private helperTextId = `${this.inputId}-helper-text`;
|
||||
private errorTextId = `${this.inputId}-error-text`;
|
||||
private labelTextId = `${this.inputId}-label`;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private isComposing = false;
|
||||
private slotMutationController?: SlotMutationController;
|
||||
@@ -406,7 +407,12 @@ export class Input implements ComponentInterface {
|
||||
connectedCallback() {
|
||||
const { el } = this;
|
||||
|
||||
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
||||
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => {
|
||||
this.setSlottedLabelId();
|
||||
forceUpdate(this);
|
||||
});
|
||||
|
||||
this.setSlottedLabelId();
|
||||
this.notchController = createNotchController(
|
||||
el,
|
||||
() => this.notchSpacerEl,
|
||||
@@ -721,7 +727,7 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderLabel() {
|
||||
const { label } = this;
|
||||
const { label, labelTextId } = this;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -729,8 +735,17 @@ export class Input implements ComponentInterface {
|
||||
'label-text-wrapper': true,
|
||||
'label-text-wrapper-hidden': !this.hasLabel,
|
||||
}}
|
||||
// Prevents Android TalkBack from focusing the label separately.
|
||||
// The input remains labelled via aria-labelledby.
|
||||
aria-hidden={this.hasLabel ? 'true' : null}
|
||||
>
|
||||
{label === undefined ? <slot name="label"></slot> : <div class="label-text">{label}</div>}
|
||||
{label === undefined ? (
|
||||
<slot name="label"></slot>
|
||||
) : (
|
||||
<div class="label-text" id={labelTextId}>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -743,6 +758,33 @@ export class Input implements ComponentInterface {
|
||||
return this.el.querySelector('[slot="label"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the slotted label element has an ID for aria-labelledby.
|
||||
* If no ID exists, we assign one using our generated labelTextId.
|
||||
*/
|
||||
private setSlottedLabelId() {
|
||||
const slottedLabel = this.labelSlot;
|
||||
if (slottedLabel && !slottedLabel.id) {
|
||||
slottedLabel.id = this.labelTextId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID to use for aria-labelledby on the native input,
|
||||
* or undefined if aria-label is explicitly set (to avoid conflicts).
|
||||
*/
|
||||
private getLabelledById(): string | undefined {
|
||||
if (this.inheritedAttributes['aria-label']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.label !== undefined) {
|
||||
return this.labelTextId;
|
||||
}
|
||||
|
||||
return this.labelSlot?.id || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if label content is provided
|
||||
* either by a prop or a content. If you want
|
||||
@@ -898,6 +940,7 @@ export class Input implements ComponentInterface {
|
||||
onCompositionend={this.onCompositionEnd}
|
||||
aria-describedby={this.getHintTextID()}
|
||||
aria-invalid={this.isInvalid ? 'true' : undefined}
|
||||
aria-labelledby={this.getLabelledById()}
|
||||
{...this.inheritedAttributes}
|
||||
/>
|
||||
{this.clearInput && !readonly && !disabled && (
|
||||
|
||||
@@ -57,6 +57,104 @@ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title,
|
||||
});
|
||||
});
|
||||
|
||||
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('input: label a11y for Android TalkBack'), () => {
|
||||
/**
|
||||
* Android TalkBack treats visible text elements as separate focusable items.
|
||||
* These tests verify that the label is hidden from a11y tree (aria-hidden)
|
||||
* while remaining associated with the input via aria-labelledby.
|
||||
*/
|
||||
test('label text wrapper should be hidden from accessibility tree when using label prop', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input label="Email" value="test@example.com"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const labelTextWrapper = page.locator('ion-input .label-text-wrapper');
|
||||
await expect(labelTextWrapper).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
test('label text wrapper should be hidden from accessibility tree when using label slot', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input value="test@example.com">
|
||||
<div slot="label">Email</div>
|
||||
</ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const labelTextWrapper = page.locator('ion-input .label-text-wrapper');
|
||||
await expect(labelTextWrapper).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
test('native input should have aria-labelledby pointing to label text when using label prop', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input label="Email" value="test@example.com"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const nativeInput = page.locator('ion-input input');
|
||||
const labelText = page.locator('ion-input .label-text');
|
||||
|
||||
const labelTextId = await labelText.getAttribute('id');
|
||||
expect(labelTextId).not.toBeNull();
|
||||
await expect(nativeInput).toHaveAttribute('aria-labelledby', labelTextId!);
|
||||
});
|
||||
|
||||
test('native input should have aria-labelledby pointing to slotted label when using label slot', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input value="test@example.com">
|
||||
<div slot="label">Email</div>
|
||||
</ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const nativeInput = page.locator('ion-input input');
|
||||
const slottedLabel = page.locator('ion-input [slot="label"]');
|
||||
|
||||
const slottedLabelId = await slottedLabel.getAttribute('id');
|
||||
expect(slottedLabelId).not.toBeNull();
|
||||
await expect(nativeInput).toHaveAttribute('aria-labelledby', slottedLabelId!);
|
||||
});
|
||||
|
||||
test('should not add aria-labelledby when aria-label is provided on host', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input aria-label="Custom Label" value="test@example.com"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const nativeInput = page.locator('ion-input input');
|
||||
|
||||
await expect(nativeInput).toHaveAttribute('aria-label', 'Custom Label');
|
||||
await expect(nativeInput).not.toHaveAttribute('aria-labelledby');
|
||||
});
|
||||
|
||||
test('should not add aria-hidden to label wrapper when no label is present', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input aria-label="Hidden Label" value="test@example.com"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const labelTextWrapper = page.locator('ion-input .label-text-wrapper');
|
||||
|
||||
await expect(labelTextWrapper).not.toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('input: font scaling'), () => {
|
||||
test('should scale text on larger font sizes', async ({ page }) => {
|
||||
|
||||
@@ -71,7 +71,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private gesture?: Gesture;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private sheetTransition?: Promise<any>;
|
||||
private isSheetModal = false;
|
||||
@State() private isSheetModal = false;
|
||||
private currentBreakpoint?: number;
|
||||
private wrapperEl?: HTMLElement;
|
||||
private backdropEl?: HTMLIonBackdropElement;
|
||||
@@ -100,6 +100,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private parentRemovalObserver?: MutationObserver;
|
||||
// Cached original parent from before modal is moved to body during presentation
|
||||
private cachedOriginalParent?: HTMLElement;
|
||||
// Cached ion-page ancestor for child route passthrough
|
||||
private cachedPageParent?: HTMLElement | null;
|
||||
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
@@ -644,7 +646,14 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
}
|
||||
|
||||
if (this.isSheetModal) {
|
||||
/**
|
||||
* Recalculate isSheetModal because framework bindings (e.g., Angular)
|
||||
* may not have been applied when componentWillLoad ran.
|
||||
*/
|
||||
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
||||
this.isSheetModal = isSheetModal;
|
||||
|
||||
if (isSheetModal) {
|
||||
this.initSheetGesture();
|
||||
} else if (hasCardModal) {
|
||||
this.initSwipeToClose();
|
||||
@@ -753,6 +762,91 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
|
||||
|
||||
this.gesture.enable(true);
|
||||
|
||||
/**
|
||||
* When backdrop interaction is allowed, nested router outlets from child routes
|
||||
* may block pointer events to parent content. Apply passthrough styles only when
|
||||
* the modal was the sole content of a child route page.
|
||||
* See https://github.com/ionic-team/ionic-framework/issues/30700
|
||||
*/
|
||||
const backdropNotBlocking = this.showBackdrop === false || this.focusTrap === false || backdropBreakpoint > 0;
|
||||
if (backdropNotBlocking) {
|
||||
this.setupChildRoutePassthrough();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For sheet modals that allow background interaction, sets up pointer-events
|
||||
* passthrough on child route page wrappers and nested router outlets.
|
||||
*/
|
||||
private setupChildRoutePassthrough() {
|
||||
// Cache the page parent for cleanup
|
||||
this.cachedPageParent = this.getOriginalPageParent();
|
||||
const pageParent = this.cachedPageParent;
|
||||
|
||||
// Skip ion-app (controller modals) and pages with visible sibling content next to the modal
|
||||
if (!pageParent || pageParent.tagName === 'ION-APP') {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasVisibleContent = Array.from(pageParent.children).some(
|
||||
(child) =>
|
||||
child !== this.el &&
|
||||
!(child instanceof HTMLElement && window.getComputedStyle(child).display === 'none') &&
|
||||
child.tagName !== 'TEMPLATE' &&
|
||||
child.tagName !== 'SLOT' &&
|
||||
!(child.nodeType === Node.TEXT_NODE && !child.textContent?.trim())
|
||||
);
|
||||
|
||||
if (hasVisibleContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Child route case: page only contained the modal
|
||||
pageParent.classList.add('ion-page-overlay-passthrough');
|
||||
|
||||
// Also make nested router outlets passthrough
|
||||
const routerOutlet = pageParent.parentElement;
|
||||
if (routerOutlet?.tagName === 'ION-ROUTER-OUTLET' && routerOutlet.parentElement?.tagName !== 'ION-APP') {
|
||||
routerOutlet.style.setProperty('pointer-events', 'none');
|
||||
routerOutlet.setAttribute('data-overlay-passthrough', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ion-page ancestor of the modal's original parent location.
|
||||
*/
|
||||
private getOriginalPageParent(): HTMLElement | null {
|
||||
if (!this.cachedOriginalParent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pageParent: HTMLElement | null = this.cachedOriginalParent;
|
||||
while (pageParent && !pageParent.classList.contains('ion-page')) {
|
||||
pageParent = pageParent.parentElement;
|
||||
}
|
||||
return pageParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes passthrough styles added by setupChildRoutePassthrough.
|
||||
*/
|
||||
private cleanupChildRoutePassthrough() {
|
||||
const pageParent = this.cachedPageParent;
|
||||
if (!pageParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageParent.classList.remove('ion-page-overlay-passthrough');
|
||||
|
||||
const routerOutlet = pageParent.parentElement;
|
||||
if (routerOutlet?.hasAttribute('data-overlay-passthrough')) {
|
||||
routerOutlet.style.removeProperty('pointer-events');
|
||||
routerOutlet.removeAttribute('data-overlay-passthrough');
|
||||
}
|
||||
|
||||
// Clear the cached reference
|
||||
this.cachedPageParent = undefined;
|
||||
}
|
||||
|
||||
private sheetOnDismiss() {
|
||||
@@ -862,6 +956,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
this.cleanupViewTransitionListener();
|
||||
this.cleanupParentRemovalObserver();
|
||||
|
||||
this.cleanupChildRoutePassthrough();
|
||||
}
|
||||
this.currentBreakpoint = undefined;
|
||||
this.animation = undefined;
|
||||
@@ -1020,6 +1116,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
private handleViewTransition() {
|
||||
// Only run view transitions when the modal is presented
|
||||
if (!this.presented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPortrait = window.innerWidth < 768;
|
||||
|
||||
// Only transition if view state actually changed
|
||||
@@ -1183,6 +1284,20 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't observe for controller-based modals or when the parent is the
|
||||
* app root (document.body or ion-app). These parents won't be removed,
|
||||
* and observing document.body with subtree: true causes performance
|
||||
* issues with frameworks like Angular during change detection.
|
||||
*/
|
||||
if (
|
||||
this.hasController ||
|
||||
this.cachedOriginalParent === document.body ||
|
||||
this.cachedOriginalParent.tagName === 'ION-APP'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.parentRemovalObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test, Viewports } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
import { configs, test, Viewports } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('modal: focus trapping'), () => {
|
||||
@@ -104,6 +104,28 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('modal: parent removal observer'), () => {
|
||||
test('should not set up parentRemovalObserver for controller-created modals', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'FW-6766',
|
||||
});
|
||||
|
||||
await page.goto('/src/components/modal/test/basic', config);
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#basic-modal');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
const hasObserver = await modal.evaluate((el: any) => {
|
||||
return el.parentRemovalObserver !== undefined;
|
||||
});
|
||||
|
||||
expect(hasObserver).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('modal: backdrop'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/basic', config);
|
||||
|
||||
176
core/src/components/modal/test/card-viewport-resize/modal.e2e.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('card modal: viewport resize'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Start in portrait mode (mobile)
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-app>
|
||||
<div class="ion-page" id="main-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card Viewport Resize Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<p>This page tests that viewport resize does not trigger card modal animation when modal is closed.</p>
|
||||
<ion-button id="open-modal">Open Card Modal</ion-button>
|
||||
<ion-modal id="card-modal">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card Modal</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button id="close-modal">Close</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<p>Modal content</p>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const modal = document.querySelector('#card-modal');
|
||||
const mainPage = document.querySelector('#main-page');
|
||||
modal.presentingElement = mainPage;
|
||||
|
||||
document.querySelector('#open-modal').addEventListener('click', () => {
|
||||
modal.present();
|
||||
});
|
||||
|
||||
document.querySelector('#close-modal').addEventListener('click', () => {
|
||||
modal.dismiss();
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
});
|
||||
|
||||
test('should not animate presenting element when viewport resizes and modal is closed', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30679',
|
||||
});
|
||||
|
||||
const mainPage = page.locator('#main-page');
|
||||
|
||||
// Verify the presenting element has no transform initially
|
||||
const initialTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(initialTransform).toBe('none');
|
||||
|
||||
// Resize from portrait to landscape (crossing the 768px threshold)
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
|
||||
// Wait for the debounced resize handler (50ms) plus some buffer
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// The presenting element should still have no transform
|
||||
// If the bug exists, it would have scale(0.93) or similar applied
|
||||
const afterResizeTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(afterResizeTransform).toBe('none');
|
||||
});
|
||||
|
||||
test('should not animate presenting element when resizing multiple times with modal closed', async ({ page }) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
|
||||
// Multiple resize cycles should not trigger the animation
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Portrait to landscape
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
let transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
|
||||
// Landscape to portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
}
|
||||
});
|
||||
|
||||
test('should still animate presenting element correctly when modal is open and viewport resizes', async ({
|
||||
page,
|
||||
}) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
// Open the modal
|
||||
await page.click('#open-modal');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// When modal is open in portrait, presenting element should be transformed
|
||||
let transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
// The presenting element should have a scale transform when modal is open
|
||||
expect(transform).not.toBe('none');
|
||||
|
||||
// Resize to landscape while modal is open
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// The modal transitions correctly - in landscape mode the presenting element
|
||||
// should have different (or no) transform than portrait
|
||||
transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
|
||||
// Note: The exact transform depends on the landscape handling
|
||||
// The main point is that when modal IS open, the transition should work
|
||||
// This test just ensures we don't break existing functionality
|
||||
});
|
||||
|
||||
test('presenting element should return to normal after modal is dismissed', async ({ page }) => {
|
||||
const mainPage = page.locator('#main-page');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
// Open the modal
|
||||
await page.click('#open-modal');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// Close the modal
|
||||
await page.click('#close-modal');
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
// Wait for animations to complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// The presenting element should be back to normal
|
||||
const transform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(transform).toBe('none');
|
||||
|
||||
// Now resize the viewport - should not trigger animation
|
||||
await page.setViewportSize({ width: 900, height: 375 });
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
const afterResizeTransform = await mainPage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).transform;
|
||||
});
|
||||
expect(afterResizeTransform).toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
97
core/src/components/modal/test/dismiss-behavior/index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Modal - Dismiss Behavior</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<div class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal - Dismiss Behavior</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<button id="present-first-modal" onclick="presentFirstModal()">Present Modal</button>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script type="module">
|
||||
import { modalController } from '../../../../../dist/ionic/index.esm.js';
|
||||
|
||||
window.modalController = modalController;
|
||||
|
||||
const sharedId = 'shared-modal-id';
|
||||
const maxModals = 5;
|
||||
let modalCount = 0;
|
||||
|
||||
function createModalComponent(modalNumber) {
|
||||
const element = document.createElement('div');
|
||||
const canPresentNext = modalNumber < maxModals;
|
||||
const presentNextButton = canPresentNext
|
||||
? `<ion-button id="present-next-modal" onclick="presentNextModal(${modalNumber + 1})">Present Modal ${
|
||||
modalNumber + 1
|
||||
}</ion-button>`
|
||||
: '';
|
||||
element.innerHTML = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal ${modalNumber}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<p>This is modal number ${modalNumber}</p>
|
||||
${presentNextButton}
|
||||
<ion-button class="dismiss-by-id">Dismiss By ID</ion-button>
|
||||
<ion-button class="dismiss-default">Dismiss Default</ion-button>
|
||||
</ion-content>
|
||||
`;
|
||||
return element;
|
||||
}
|
||||
|
||||
window.presentFirstModal = async () => {
|
||||
modalCount = 0;
|
||||
await presentNextModal(1);
|
||||
};
|
||||
|
||||
window.presentNextModal = async (modalNumber) => {
|
||||
if (modalNumber > maxModals) {
|
||||
return;
|
||||
}
|
||||
modalCount = Math.max(modalCount, modalNumber);
|
||||
const element = createModalComponent(modalNumber);
|
||||
const modal = await modalController.create({
|
||||
component: element,
|
||||
htmlAttributes: {
|
||||
id: sharedId,
|
||||
'data-testid': `modal-${modalNumber}`,
|
||||
},
|
||||
});
|
||||
await modal.present();
|
||||
|
||||
const dismissByIdButton = element.querySelector('ion-button.dismiss-by-id');
|
||||
dismissByIdButton.addEventListener('click', () => {
|
||||
modalController.dismiss(undefined, undefined, sharedId);
|
||||
});
|
||||
|
||||
const dismissDefaultButton = element.querySelector('ion-button.dismiss-default');
|
||||
dismissDefaultButton.addEventListener('click', () => {
|
||||
modalController.dismiss();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
58
core/src/components/modal/test/dismiss-behavior/modal.e2e.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('modal: dismiss behavior'), () => {
|
||||
test.describe(title('modal: default dismiss'), () => {
|
||||
test('should dismiss the last presented modal when the default dismiss button is clicked', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/dismiss-behavior', config);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#present-first-modal');
|
||||
await ionModalDidPresent.next();
|
||||
const firstModal = page.locator('ion-modal[data-testid="modal-1"]');
|
||||
await expect(firstModal).toBeVisible();
|
||||
|
||||
await page.click('#present-next-modal');
|
||||
await ionModalDidPresent.next();
|
||||
const secondModal = page.locator('ion-modal[data-testid="modal-2"]');
|
||||
await expect(secondModal).toBeVisible();
|
||||
|
||||
await page.click('ion-modal[data-testid="modal-2"] ion-button.dismiss-default');
|
||||
await ionModalDidDismiss.next();
|
||||
await secondModal.waitFor({ state: 'detached' });
|
||||
|
||||
await expect(firstModal).toBeVisible();
|
||||
await expect(secondModal).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('modal: dismiss by id'), () => {
|
||||
test('should dismiss the last presented modal when the dismiss by id button is clicked', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/dismiss-behavior', config);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#present-first-modal');
|
||||
await ionModalDidPresent.next();
|
||||
const firstModal = page.locator('ion-modal[data-testid="modal-1"]');
|
||||
await expect(firstModal).toBeVisible();
|
||||
|
||||
await page.click('#present-next-modal');
|
||||
await ionModalDidPresent.next();
|
||||
const secondModal = page.locator('ion-modal[data-testid="modal-2"]');
|
||||
await expect(secondModal).toBeVisible();
|
||||
|
||||
await page.click('ion-modal[data-testid="modal-2"] ion-button.dismiss-by-id');
|
||||
await ionModalDidDismiss.next();
|
||||
await secondModal.waitFor({ state: 'detached' });
|
||||
|
||||
await expect(firstModal).toBeVisible();
|
||||
await expect(secondModal).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Modal } from '../modal';
|
||||
import { h } from '@stencil/core';
|
||||
|
||||
describe('modal: id', () => {
|
||||
it('modal should be assigned an incrementing id', async () => {
|
||||
@@ -52,4 +52,21 @@ describe('modal: id', () => {
|
||||
const alert = page.body.querySelector('ion-modal')!;
|
||||
expect(alert.id).toBe(id);
|
||||
});
|
||||
|
||||
it('should allow multiple modals with the same id', async () => {
|
||||
const sharedId = 'shared-modal-id';
|
||||
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
template: () => [
|
||||
<ion-modal id={sharedId} overlayIndex={1} is-open={true}></ion-modal>,
|
||||
<ion-modal id={sharedId} overlayIndex={2} is-open={true}></ion-modal>,
|
||||
],
|
||||
});
|
||||
|
||||
const modals = page.body.querySelectorAll('ion-modal');
|
||||
expect(modals.length).toBe(2);
|
||||
expect(modals[0].id).toBe(sharedId);
|
||||
expect(modals[1].id).toBe(sharedId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
private destroyTriggerInteraction?: () => void;
|
||||
private destroyKeyboardInteraction?: () => void;
|
||||
private destroyDismissInteraction?: () => void;
|
||||
private headerResizeObserver?: ResizeObserver;
|
||||
|
||||
private inline = false;
|
||||
private workingDelegate?: FrameworkDelegate;
|
||||
@@ -361,6 +362,11 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
if (destroyTriggerInteraction) {
|
||||
destroyTriggerInteraction();
|
||||
}
|
||||
|
||||
if (this.headerResizeObserver) {
|
||||
this.headerResizeObserver.disconnect();
|
||||
this.headerResizeObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
@@ -491,6 +497,8 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
inline
|
||||
);
|
||||
|
||||
this.recalculateContentOnHeaderReady();
|
||||
|
||||
if (!this.keyboardEvents) {
|
||||
this.configureKeyboardInteraction();
|
||||
}
|
||||
@@ -540,6 +548,39 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the header for height changes and trigger content dimension
|
||||
* recalculation when the header has a height > 0. This sets the offset-top
|
||||
* of the content to the height of the header correctly.
|
||||
*/
|
||||
private recalculateContentOnHeaderReady() {
|
||||
const popoverContent = this.el.shadowRoot?.querySelector('.popover-content');
|
||||
if (!popoverContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentContainer = this.usersElement || popoverContent;
|
||||
|
||||
const header = contentContainer.querySelector('ion-header') as HTMLElement | null;
|
||||
const contentElements = contentContainer.querySelectorAll('ion-content');
|
||||
|
||||
if (!header || contentElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.headerResizeObserver = new ResizeObserver(async () => {
|
||||
if (header.offsetHeight > 0) {
|
||||
this.headerResizeObserver?.disconnect();
|
||||
this.headerResizeObserver = undefined;
|
||||
for (const contentEl of contentElements) {
|
||||
await contentEl.recalculateDimensions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.headerResizeObserver.observe(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the popover overlay after it has been presented.
|
||||
* This is a no-op if the overlay has not been presented yet. If you want
|
||||
|
||||
@@ -556,14 +556,19 @@ export class Select implements ComponentInterface {
|
||||
.filter((cls) => cls !== 'hydrated')
|
||||
.join(' ');
|
||||
const optClass = `${OPTION_CLASS} ${copyClasses}`;
|
||||
const isSelected = isOptionSelected(selectValue, value, this.compareWith);
|
||||
|
||||
return {
|
||||
role: isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '',
|
||||
role: isSelected ? 'selected' : '',
|
||||
text: option.textContent,
|
||||
cssClass: optClass,
|
||||
handler: () => {
|
||||
this.setValue(value);
|
||||
},
|
||||
htmlAttributes: {
|
||||
'aria-checked': isSelected ? 'true' : 'false',
|
||||
role: 'radio',
|
||||
},
|
||||
} as ActionSheetButton;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('textarea: a11y'), () => {
|
||||
test.describe(title('select: a11y'), () => {
|
||||
test('default layout should not have accessibility violations', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { TabBarChangedEventDetail } from './tab-bar-interface';
|
||||
})
|
||||
export class TabBar implements ComponentInterface {
|
||||
private keyboardCtrl: KeyboardController | null = null;
|
||||
private keyboardCtrlPromise: Promise<KeyboardController> | null = null;
|
||||
private didLoad = false;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
@@ -88,7 +89,7 @@ export class TabBar implements ComponentInterface {
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
|
||||
const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
|
||||
/**
|
||||
* If the keyboard is hiding, then we need to wait
|
||||
* for the webview to resize. Otherwise, the tab bar
|
||||
@@ -100,11 +101,32 @@ export class TabBar implements ComponentInterface {
|
||||
|
||||
this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
|
||||
});
|
||||
this.keyboardCtrlPromise = promise;
|
||||
|
||||
const keyboardCtrl = await promise;
|
||||
|
||||
/**
|
||||
* Only assign if this is still the current promise.
|
||||
* Otherwise, a new connectedCallback has started or
|
||||
* disconnectedCallback was called, so destroy this instance.
|
||||
*/
|
||||
if (this.keyboardCtrlPromise === promise) {
|
||||
this.keyboardCtrl = keyboardCtrl;
|
||||
this.keyboardCtrlPromise = null;
|
||||
} else {
|
||||
keyboardCtrl.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.keyboardCtrlPromise) {
|
||||
this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
|
||||
this.keyboardCtrlPromise = null;
|
||||
}
|
||||
|
||||
if (this.keyboardCtrl) {
|
||||
this.keyboardCtrl.destroy();
|
||||
this.keyboardCtrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,9 +205,13 @@
|
||||
// otherwise the .input-cover will not be rendered at all
|
||||
// The input cover is not clickable when the input is disabled
|
||||
.cloned-input {
|
||||
@include position(0, null, 0, 0);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
// Reset height since absolute positioning with top/bottom handles sizing
|
||||
height: auto;
|
||||
max-height: none;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<ion-content class="ion-padding">
|
||||
<button id="baseline" onclick="openToast(baselineConfig)">Open Baseline Layout Toast</button>
|
||||
<button id="stacked" onclick="openToast(stackedConfig)">Open Stacked Layout Toast</button>
|
||||
<button id="stacked-long-message" onclick="openToast(stackedLongMessageConfig)">
|
||||
Open Stacked Layout Toast with Long Message
|
||||
</button>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
@@ -52,6 +55,13 @@
|
||||
message: 'This is a stacked layout toast.',
|
||||
layout: 'stacked',
|
||||
};
|
||||
|
||||
const stackedLongMessageConfig = {
|
||||
...baselineConfig,
|
||||
message:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in elit varius, maximus sem in, bibendum justo.',
|
||||
layout: 'stacked',
|
||||
};
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('toast: stacked layout'), () => {
|
||||
test('should render stacked buttons', async ({ page }) => {
|
||||
test('should render stacked toast', async ({ page }) => {
|
||||
await page.goto('/src/components/toast/test/layout', config);
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
|
||||
@@ -13,5 +13,21 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
const toastWrapper = page.locator('ion-toast .toast-wrapper');
|
||||
await expect(toastWrapper).toHaveScreenshot(screenshot(`toast-stacked`));
|
||||
});
|
||||
|
||||
test('should render stacked toast with long message', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30908',
|
||||
});
|
||||
|
||||
await page.goto('/src/components/toast/test/layout', config);
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
|
||||
await page.click('#stacked-long-message');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
const toastWrapper = page.locator('ion-toast .toast-wrapper');
|
||||
await expect(toastWrapper).toHaveScreenshot(screenshot(`toast-stacked-long-message`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -132,7 +132,6 @@
|
||||
.toast-layout-baseline .toast-content {
|
||||
display: flex;
|
||||
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -142,6 +141,8 @@
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,15 @@ html.ios ion-modal.modal-card .ion-page {
|
||||
z-index: $z-index-page-container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows pointer events to pass through child route page wrappers
|
||||
* when they only contain a sheet modal that permits background interaction.
|
||||
* https://github.com/ionic-team/ionic-framework/issues/30700
|
||||
*/
|
||||
.ion-page.ion-page-overlay-passthrough {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* When making custom dialogs, using
|
||||
* ion-content is not required. As a result,
|
||||
@@ -243,10 +252,12 @@ html.plt-ios.plt-hybrid, html.plt-ios.plt-pwa {
|
||||
|
||||
@supports (padding-top: env(safe-area-inset-top)) {
|
||||
html {
|
||||
--ion-safe-area-top: env(safe-area-inset-top);
|
||||
--ion-safe-area-bottom: env(safe-area-inset-bottom);
|
||||
--ion-safe-area-left: env(safe-area-inset-left);
|
||||
--ion-safe-area-right: env(safe-area-inset-right);
|
||||
// `--safe-area-inset-*` are set by Capacitor
|
||||
// @see https://capacitorjs.com/docs/apis/system-bars#android-note
|
||||
--ion-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||
--ion-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||
--ion-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||
--ion-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,26 @@ const addClone = (
|
||||
if (disabledClonedInput) {
|
||||
clonedEl.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the clone at the same horizontal offset as the native input
|
||||
* to prevent the placeholder from overlapping start slot content (e.g., icons).
|
||||
*/
|
||||
const doc = componentEl.ownerDocument!;
|
||||
const isRTL = doc.dir === 'rtl';
|
||||
|
||||
if (isRTL) {
|
||||
const parentWidth = (parentEl as HTMLElement).offsetWidth;
|
||||
const startOffset = parentWidth - inputEl.offsetLeft - inputEl.offsetWidth;
|
||||
clonedEl.style.insetInlineStart = `${startOffset}px`;
|
||||
} else {
|
||||
clonedEl.style.insetInlineStart = `${inputEl.offsetLeft}px`;
|
||||
}
|
||||
|
||||
parentEl.appendChild(clonedEl);
|
||||
cloneMap.set(componentEl, clonedEl);
|
||||
|
||||
const doc = componentEl.ownerDocument!;
|
||||
const tx = doc.dir === 'rtl' ? 9999 : -9999;
|
||||
const tx = isRTL ? 9999 : -9999;
|
||||
componentEl.style.pointerEvents = 'none';
|
||||
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
|
||||
};
|
||||
|
||||
@@ -291,8 +291,14 @@ const jsSetFocus = async (
|
||||
// give the native text input focus
|
||||
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);
|
||||
|
||||
// ensure this is the focused input
|
||||
setManualFocus(inputEl);
|
||||
/**
|
||||
* If focus has moved to another element while scroll assist was running,
|
||||
* don't steal focus back. This prevents focus jumping when users
|
||||
* quickly switch between inputs or tap other elements.
|
||||
*/
|
||||
if (document.activeElement === inputEl) {
|
||||
setManualFocus(inputEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the input is about to be blurred
|
||||
|
||||
@@ -38,6 +38,20 @@ let lastId = 0;
|
||||
|
||||
export const activeAnimations = new WeakMap<OverlayInterface, Animation[]>();
|
||||
|
||||
type OverlayWithFocusTrapProps = HTMLIonOverlayElement & {
|
||||
focusTrap?: boolean;
|
||||
showBackdrop?: boolean;
|
||||
backdropBreakpoint?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the overlay's backdrop is always blocking (no background interaction).
|
||||
* Returns false if showBackdrop=false or backdropBreakpoint > 0.
|
||||
*/
|
||||
const isBackdropAlwaysBlocking = (el: OverlayWithFocusTrapProps): boolean => {
|
||||
return el.showBackdrop !== false && !((el.backdropBreakpoint ?? 0) > 0);
|
||||
};
|
||||
|
||||
const createController = <Opts extends object, HTMLElm>(tagName: string) => {
|
||||
return {
|
||||
create(options: Opts): Promise<HTMLElm> {
|
||||
@@ -459,7 +473,9 @@ export const getPresentedOverlay = (
|
||||
id?: string
|
||||
): HTMLIonOverlayElement | undefined => {
|
||||
const overlays = getPresentedOverlays(doc, overlayTag);
|
||||
return id === undefined ? overlays[overlays.length - 1] : overlays.find((o) => o.id === id);
|
||||
// If no id is provided, return the last presented overlay
|
||||
// Otherwise, return the last overlay with the given id
|
||||
return (id === undefined ? overlays : overlays.filter((o: HTMLIonOverlayElement) => o.id === id)).slice(-1)[0];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -539,11 +555,9 @@ 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 overlayEl = overlay.el as OverlayWithFocusTrapProps;
|
||||
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false;
|
||||
// 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 && isBackdropAlwaysBlocking(overlayEl);
|
||||
|
||||
overlay.presented = true;
|
||||
overlay.willPresent.emit();
|
||||
@@ -680,12 +694,12 @@ export const dismiss = async <OverlayDismissOptions>(
|
||||
* is dismissed.
|
||||
*/
|
||||
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 el = o as OverlayWithFocusTrapProps;
|
||||
return el.tagName !== 'ION-TOAST' && el.focusTrap !== false && isBackdropAlwaysBlocking(el);
|
||||
});
|
||||
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
||||
const overlayEl = overlay.el as OverlayWithFocusTrapProps;
|
||||
const locksRoot =
|
||||
overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && overlayEl.showBackdrop !== false;
|
||||
overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && isBackdropAlwaysBlocking(overlayEl);
|
||||
|
||||
/**
|
||||
* If this is the last visible overlay that is trapping focus
|
||||
|
||||
@@ -102,6 +102,13 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* URL query parameters cause the custom Playwright `page.route`
|
||||
* interceptor to fail, which is necessary to inject the test HTML.
|
||||
*
|
||||
* To avoid this, the final navigation URL is kept simple by using
|
||||
* hash params to ensure the route interceptor always works.
|
||||
*/
|
||||
await page.goto(`${baseUrl}#`, options);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -186,7 +186,21 @@ test.describe.only('group of tests', () => {
|
||||
> [!IMPORTANT]
|
||||
> After debugging, make sure to remove the `.only` suffix to ensure all tests run again during normal execution.
|
||||
|
||||
#### 3. Pausing Test Execution
|
||||
#### 3. Repeating Tests to Reproduce Flaky Failures
|
||||
|
||||
Flaky tests may not fail consistently. To help reproduce intermittent failures locally, you can use Playwright's `--repeat-each` flag to run the same test multiple times in a row.
|
||||
|
||||
This is especially useful when CI reports a failure you cannot reproduce on your first local run.
|
||||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
npm run test.e2e.docker.update-snapshots src/components/radio/test/a11y/radio.e2e.ts -- --repeat-each=10
|
||||
```
|
||||
|
||||
This runs the test 10 times, increasing the chance of catching the flaky behavior.
|
||||
|
||||
#### 4. Pausing Test Execution
|
||||
|
||||
Additionally, you can pause execution of a test by using the `page.pause()` method. This pauses the script execution and allows you to manually inspect the page in the browser.
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"core",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "8.7.9"
|
||||
"version": "8.7.17"
|
||||
}
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.17](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.17) (2026-01-14)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.11](https://github.com/ionic-team/ionic-framework/compare/v8.7.10...v8.7.11) (2025-11-26)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
21
packages/angular-server/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.9"
|
||||
"@ionic/core": "^8.7.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/eslint-plugin": "^16.0.0",
|
||||
@@ -1031,14 +1031,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.9.tgz",
|
||||
"integrity": "sha512-6cYp3LHjCGX32MiYR09l/O+8klkJ27WpKzllvnLRe5bxX3nOuqJvJJIshzT85qTdZNTFu7zJJrjWkgEOXGarzg==",
|
||||
"version": "8.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.17.tgz",
|
||||
"integrity": "sha512-gp7PIEJX27NX/FkjiUlpjQUtJiFFE5W1lofRC5CfORQ8p4PrLh9wJO9EJH0YryCr2qZS0k47sYgRQP5FwiXlpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -7306,9 +7309,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.9.tgz",
|
||||
"integrity": "sha512-6cYp3LHjCGX32MiYR09l/O+8klkJ27WpKzllvnLRe5bxX3nOuqJvJJIshzT85qTdZNTFu7zJJrjWkgEOXGarzg==",
|
||||
"version": "8.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.17.tgz",
|
||||
"integrity": "sha512-gp7PIEJX27NX/FkjiUlpjQUtJiFFE5W1lofRC5CfORQ8p4PrLh9wJO9EJH0YryCr2qZS0k47sYgRQP5FwiXlpg==",
|
||||
"requires": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"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.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,76 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.7.17](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.17) (2026-01-14)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.16](https://github.com/ionic-team/ionic-framework/compare/v8.7.15...v8.7.16) (2025-12-31)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** allow interaction with parent content through sheet modals in child routes ([#30839](https://github.com/ionic-team/ionic-framework/issues/30839)) ([b9e3cf0](https://github.com/ionic-team/ionic-framework/commit/b9e3cf0f5aae79a1f27a07b102c77e51f24825f4)), closes [#30700](https://github.com/ionic-team/ionic-framework/issues/30700)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.11](https://github.com/ionic-team/ionic-framework/compare/v8.7.10...v8.7.11) (2025-11-26)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
44
packages/angular/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.9",
|
||||
"@ionic/core": "^8.7.17",
|
||||
"ionicons": "^8.0.13",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -32,7 +32,7 @@
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@schematics/angular": "^17.0.0",
|
||||
"@types/node": "12.12.5",
|
||||
"@types/node": "20.19.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"ng-packagr": "^16.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"rxjs": "~7.5.0",
|
||||
"typescript": "~4.9.3",
|
||||
"typescript": "~5.0.2",
|
||||
"typescript-eslint-language-service": "^5.0.0",
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
@@ -1398,14 +1398,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.9.tgz",
|
||||
"integrity": "sha512-6cYp3LHjCGX32MiYR09l/O+8klkJ27WpKzllvnLRe5bxX3nOuqJvJJIshzT85qTdZNTFu7zJJrjWkgEOXGarzg==",
|
||||
"version": "8.7.17",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.17.tgz",
|
||||
"integrity": "sha512-gp7PIEJX27NX/FkjiUlpjQUtJiFFE5W1lofRC5CfORQ8p4PrLh9wJO9EJH0YryCr2qZS0k47sYgRQP5FwiXlpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/eslint-config": {
|
||||
@@ -2402,9 +2405,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "12.12.5",
|
||||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
@@ -8719,16 +8727,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
|
||||
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint-language-service": {
|
||||
@@ -8756,6 +8765,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "8.7.9",
|
||||
"version": "8.7.17",
|
||||
"description": "Angular specific wrappers for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/core": "^8.7.9",
|
||||
"@ionic/core": "^8.7.17",
|
||||
"ionicons": "^8.0.13",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -78,7 +78,7 @@
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@schematics/angular": "^17.0.0",
|
||||
"@types/node": "12.12.5",
|
||||
"@types/node": "20.19.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
@@ -87,7 +87,7 @@
|
||||
"ng-packagr": "^16.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"rxjs": "~7.5.0",
|
||||
"typescript": "~4.9.3",
|
||||
"typescript": "~5.0.2",
|
||||
"typescript-eslint-language-service": "^5.0.0",
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
|
||||
59
packages/angular/test/apps/ng17/package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"@angular/language-service": "^17.0.2",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/node": "^18.19.0",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
@@ -567,17 +568,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/@types/node": {
|
||||
"version": "20.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
|
||||
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/@vitejs/plugin-basic-ssl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz",
|
||||
@@ -595,6 +585,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
@@ -848,6 +839,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.2.tgz",
|
||||
"integrity": "sha512-32RHWhTgFLMonI3kRdstACay/nvetfxXjdwcTtABjcvBoND7nD9GMhkISQdgS+hcR/IhgXxaPidq8f2UAY5DBw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -863,6 +855,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.0.tgz",
|
||||
"integrity": "sha512-VzKYmN+1wSE2wLBEFu/NHSrRQbeaKkEDO9aZxd7nAnxC5T1Zcva7dfqZIXMHXH2HESYAZCvOS8ikBE/7DCtWWw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.1700.0",
|
||||
"@angular-devkit/core": "17.0.0",
|
||||
@@ -896,6 +889,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.2.tgz",
|
||||
"integrity": "sha512-hCW0njHgrcwTWNoKZDwf02DnhYLVWNXM2FMw66MKpfxTp7McSyaXjGBU9/hchW3dZJ0xTwyxoyoqJFoHYvg0yg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -911,6 +905,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.2.tgz",
|
||||
"integrity": "sha512-ewUFbKhMEhAmw2dGfk0ImhTlyrO2y4pJSKIZdFrkR1d0HiJX8bCHUdTiiR/2jeP7w2eamjXj15Rptb+iZZes2Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -931,6 +926,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.2.tgz",
|
||||
"integrity": "sha512-IUYL3Yz5RbR0Z0/x7it4GK3sMb2qVihxu0tlgfUW53P1Vi6nU/Zda0bCJTu6Z64qEtS8zwCwF1Ekomuq6UaiKg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.23.2",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -958,6 +954,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.2.tgz",
|
||||
"integrity": "sha512-MjDxWeyn3Txi0qo/V/I+B/gndh0uptQ0XWgBRwOx6Wcr5zRGeZIFlXBxPpyXnGTlJkeyErsTN7FfFCZ4C3kCPA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -973,6 +970,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.2.tgz",
|
||||
"integrity": "sha512-w1QKifaVG4daxUktcBNZqBtOH1vn8t0YiwDR3woEdUYt0XYKMipfDzQfyIK+6fIVPOJUd42pRns1nbWJQHOInA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -999,6 +997,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.2.tgz",
|
||||
"integrity": "sha512-eTnPILEA/eAMkVUR/+g6fWhhMTmnmOzcZSGX/bBgQcvOhayZrDDxA6/Qf+jIB4RwC0wd3KA9zT5BCMmNojoUsg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1037,6 +1036,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-17.0.2.tgz",
|
||||
"integrity": "sha512-+1uCnAw7Ql2r4BDnfaRvQrXI1H5qUB/1f8CwCjaVoIn7kLJs/ps4I0WbOVtujJ2VPnxIggfVtenXRRMlungZlg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0",
|
||||
"xhr2": "^0.2.0"
|
||||
@@ -1056,6 +1056,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.2.tgz",
|
||||
"integrity": "sha512-A1Ulv4qBAtJyK5g1yBlK1qZHe+KaaL5vMPAaPWUxICH8lHEodDkJlbYAUI2e4VL2BN7zBmdOep6tlBKPmHY3mw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1115,6 +1116,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
|
||||
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
@@ -4492,10 +4494,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
|
||||
"dev": true
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
"version": "1.3.9",
|
||||
@@ -4617,6 +4623,7 @@
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz",
|
||||
"integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.10.0",
|
||||
"@typescript-eslint/types": "6.10.0",
|
||||
@@ -5024,6 +5031,7 @@
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5095,6 +5103,7 @@
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
@@ -5822,6 +5831,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001541",
|
||||
"electron-to-chromium": "^1.4.535",
|
||||
@@ -7605,6 +7615,7 @@
|
||||
"version": "7.32.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
|
||||
"integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.3",
|
||||
@@ -9838,6 +9849,7 @@
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
|
||||
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^2.0.1",
|
||||
"parse-node-version": "^1.0.1",
|
||||
@@ -11135,6 +11147,7 @@
|
||||
"integrity": "sha512-VShJISKCYt3iVJoMUPZiv67+0tiItxWMnfVmTmPZPio2Fu+wGc9U4ijjPxcmp2RJmLRaxkB9cn5rlrAvkIrNMA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nrwl/tao": "17.0.3",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
@@ -12111,6 +12124,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -12856,6 +12870,7 @@
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -12889,6 +12904,7 @@
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
|
||||
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
@@ -13913,6 +13929,7 @@
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
|
||||
"integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
@@ -13965,6 +13982,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -14275,6 +14293,7 @@
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14332,9 +14351,7 @@
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
@@ -14586,6 +14603,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
|
||||
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^1.0.0",
|
||||
@@ -14633,6 +14651,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
|
||||
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^1.2.0",
|
||||
@@ -14717,6 +14736,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
|
||||
"integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.9",
|
||||
"@types/connect-history-api-fallback": "^1.3.5",
|
||||
@@ -14891,6 +14911,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -14912,6 +14933,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -15251,6 +15273,7 @@
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.2.tgz",
|
||||
"integrity": "sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
|
||||