Compare commits
142 Commits
ROU-11368
...
fix/dateti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8772e8bd9 | ||
|
|
12ef9345d6 | ||
|
|
79d5716baf | ||
|
|
14419288c0 | ||
|
|
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 | ||
|
|
92db36489c | ||
|
|
c37e2a5d9e | ||
|
|
0a02e0f8cf | ||
|
|
e1293ff9f6 | ||
|
|
32df083e87 | ||
|
|
58d563805f | ||
|
|
cfd8c42f07 | ||
|
|
0b2e766609 | ||
|
|
10f895b8b6 | ||
|
|
66abc05c46 | ||
|
|
8a8eec4247 | ||
|
|
abd3eacadf | ||
|
|
54a1c86d6a | ||
|
|
ba73988750 | ||
|
|
bdc80d8e82 | ||
|
|
9f013b7a51 | ||
|
|
e4fc33f331 | ||
|
|
4a49e52b6d | ||
|
|
7a293d768c | ||
|
|
72c2b3e916 | ||
|
|
12084af163 | ||
|
|
add33c5995 | ||
|
|
03303d73f0 | ||
|
|
18e1d3e1b8 | ||
|
|
820fa28543 | ||
|
|
f44585657c | ||
|
|
2586284dce | ||
|
|
ce048a507a | ||
|
|
2156f99c2a | ||
|
|
daf311f63a | ||
|
|
003de2d85e | ||
|
|
7bb9535f60 | ||
|
|
3b80473f2f | ||
|
|
99d2b731f5 | ||
|
|
515249d2c3 | ||
|
|
a40d957ad9 | ||
|
|
5a06503d4a | ||
|
|
49f7cc7704 | ||
|
|
024d090122 | ||
|
|
36c56e71b6 | ||
|
|
9e361727b8 | ||
|
|
6d4cb0f4e2 | ||
|
|
2847681f7b | ||
|
|
4c774601ec | ||
|
|
b3b02416a3 | ||
|
|
be1f3f32f0 | ||
|
|
66f517d5b2 | ||
|
|
c339bc3682 | ||
|
|
49f96d7f1e | ||
|
|
b49ba6bdfe | ||
|
|
58d9445139 | ||
|
|
86751985e9 | ||
|
|
b57a7c1d49 | ||
|
|
ac6968cc10 | ||
|
|
92ad4ca511 | ||
|
|
1899b49d25 | ||
|
|
aadf06c493 | ||
|
|
e9e6605862 | ||
|
|
ed13318209 | ||
|
|
e5ed8a10ed | ||
|
|
8b0769ce75 | ||
|
|
2229c24bf5 | ||
|
|
a6b19f40b2 | ||
|
|
d91edcffdf | ||
|
|
4c1d0127b6 | ||
|
|
7075808ba8 | ||
|
|
67a1800094 | ||
|
|
895ef4798e | ||
|
|
f1e6a4bb71 | ||
|
|
a671b74756 | ||
|
|
b9013c9457 | ||
|
|
dc764e45e9 | ||
|
|
1cd81b9230 | ||
|
|
05026c5a48 | ||
|
|
a2e803a553 | ||
|
|
56265e35d1 | ||
|
|
95a7d710e7 | ||
|
|
a00a389df7 | ||
|
|
a62d382171 | ||
|
|
8d39ea0c6e | ||
|
|
e4c042834c | ||
|
|
6203b88b9f | ||
|
|
fdb77960ad | ||
|
|
227d637998 | ||
|
|
cbb4ad5683 | ||
|
|
75f6c05fb9 | ||
|
|
d5627c7368 | ||
|
|
74cd71af24 | ||
|
|
30d1910d6e | ||
|
|
b154f4ed09 | ||
|
|
d25b8a34f2 | ||
|
|
244f9a1dd6 | ||
|
|
ffcf5d0268 | ||
|
|
759147fab6 | ||
|
|
dc713167f9 | ||
|
|
0f7dd51767 | ||
|
|
31c5416a51 | ||
|
|
baed34722b | ||
|
|
708d5845b9 | ||
|
|
5780a95a9c | ||
|
|
dd68369338 |
@@ -8,48 +8,53 @@ inputs:
|
||||
tag:
|
||||
description: 'The tag to publish to on NPM.'
|
||||
preid:
|
||||
description: 'The prerelease identifier used when doing a prerelease.'
|
||||
description: "Prerelease identifier such as 'alpha', 'beta', 'rc', or 'next'. Leave blank to skip prerelease tagging."
|
||||
working-directory:
|
||||
description: 'The directory of the package.'
|
||||
folder:
|
||||
default: './'
|
||||
description: 'A folder containing a package.json file.'
|
||||
token:
|
||||
description: 'The NPM authentication token required to publish.'
|
||||
node-version:
|
||||
description: 'Node.js version to use when publishing.'
|
||||
required: false
|
||||
default: '24.x'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- name: 🟢 Configure Node for Publish
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: ${{ inputs.node-version }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@ionic'
|
||||
# Provenance requires npm 9.5.0+
|
||||
- name: Install latest npm
|
||||
- name: 📦 Install latest npm
|
||||
run: npm install -g npm@latest
|
||||
shell: bash
|
||||
# This ensures the local version of Lerna is installed
|
||||
# and that we do not use the global Lerna version
|
||||
- name: Install root dependencies
|
||||
- name: 🕸️ Install root dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
- name: Install Dependencies
|
||||
- name: 📦 Install Dependencies
|
||||
run: npx lerna@5 bootstrap --include-dependencies --scope ${{ inputs.scope }} --ignore-scripts -- --legacy-peer-deps
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
- name: Update Version
|
||||
run: npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version --preid=${{ inputs.preid }}
|
||||
- name: 🏷️ Set Version
|
||||
run: |
|
||||
if [ -z "${{ inputs.preid }}" ]; then
|
||||
npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version
|
||||
else
|
||||
npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version --preid=${{ inputs.preid }}
|
||||
fi
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
- name: Run Build
|
||||
- name: 🏗️ Run Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
- name: Prepare NPM Token
|
||||
run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
shell: bash
|
||||
env:
|
||||
NPM_TOKEN: ${{ inputs.token }}
|
||||
- name: Publish to NPM
|
||||
- name: 🚀 Publish to NPM
|
||||
run: npm publish ${{ inputs.folder }} --tag ${{ inputs.tag }} --provenance
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
67
.github/ionic-issue-bot.yml
vendored
@@ -56,14 +56,6 @@ closeAndLock:
|
||||
bug reports and feature requests. Please use our [forum](https://forum.ionicframework.com) for questions about the framework.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: appflow"
|
||||
message: >
|
||||
Thanks for the issue! This issue appears to be related to Ionic Appflow. We use this issue tracker exclusively for
|
||||
bug reports and feature requests. Please use the [Ionic Appflow Support Forum](https://ionic.zendesk.com/hc/en-us/requests/new)
|
||||
to report this issue.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: missing template"
|
||||
message: >
|
||||
@@ -145,65 +137,6 @@ noReproduction:
|
||||
lock: true
|
||||
dryRun: false
|
||||
|
||||
wrongRepo:
|
||||
repos:
|
||||
- label: "ionitron: capacitor"
|
||||
repo: capacitor
|
||||
message: >
|
||||
Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests
|
||||
associated with the Ionic Framework. It appears that this issue is associated with Capacitor.
|
||||
I am moving this issue to the Capacitor repository. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: v3"
|
||||
repo: ionic-v3
|
||||
message: >
|
||||
Thanks for the issue! We have moved the source code and issues for Ionic 3 into a separate repository.
|
||||
I am moving this issue to the repository for Ionic 3. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: cli"
|
||||
repo: ionic-cli
|
||||
message: >
|
||||
Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests
|
||||
associated with the Ionic Framework. It appears that this issue is associated with the Ionic CLI.
|
||||
I am moving this issue to the Ionic CLI repository. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: docs"
|
||||
repo: ionic-docs
|
||||
message: >
|
||||
Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests
|
||||
associated with the Ionic Framework. It appears that this issue is associated with the Ionic Documentation.
|
||||
I am moving this issue to the Ionic Docs repository. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: stencil"
|
||||
repo: stencil
|
||||
message: >
|
||||
Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests
|
||||
associated with the Ionic Framework. It appears that this issue is associated with Stencil.
|
||||
I am moving this issue to the Stencil repository. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
- label: "ionitron: native"
|
||||
repo: ionic-native
|
||||
message: >
|
||||
Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests
|
||||
associated with the Ionic Framework. It appears that this issue is associated with Ionic Native.
|
||||
I am moving this issue to the Ionic Native repository. Please track this issue over there.
|
||||
|
||||
|
||||
Thank you for using Ionic!
|
||||
close: true
|
||||
lock: true
|
||||
dryRun: false
|
||||
|
||||
screenshot:
|
||||
appId: 18001
|
||||
checkName: "build"
|
||||
|
||||
@@ -3,23 +3,23 @@ description: 'Build Ionic Angular Server'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Install Angular Server Dependencies
|
||||
- name: 🕸️ Install Angular Server Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/angular-server
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/angular-server
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build.prod
|
||||
shell: bash
|
||||
working-directory: ./packages/angular-server
|
||||
|
||||
@@ -3,31 +3,31 @@ description: 'Build Ionic Angular'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Install Angular Dependencies
|
||||
- name: 🕸️ Install Angular Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/angular
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/angular
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./packages/angular
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/angular
|
||||
- name: Check Diff
|
||||
- name: 🔍 Check Diff
|
||||
run: git diff --exit-code
|
||||
shell: bash
|
||||
working-directory: ./packages/angular
|
||||
|
||||
@@ -8,20 +8,20 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
- name: Install Stencil ${{ inputs.stencil-version }}
|
||||
- name: 📦 Install Stencil ${{ inputs.stencil-version }}
|
||||
working-directory: ./core
|
||||
run: npm i @stencil/core@${{ inputs.stencil-version }}
|
||||
shell: bash
|
||||
- name: Build Core
|
||||
- name: 🏗️ Build Core
|
||||
run: npm run build -- --ci --debug --verbose
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
|
||||
16
.github/workflows/actions/build-core/action.yml
vendored
@@ -8,22 +8,22 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Install Dependencies
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm install
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
# If an Ionicons version was specified install that.
|
||||
# Otherwise just use the version defined in the package.json.
|
||||
- name: Install Ionicons Version
|
||||
- name: 📦 Install Ionicons Version
|
||||
if: inputs.ionicons-version != ''
|
||||
run: npm install ionicons@${{ inputs.ionicons-version }}
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
- name: Build Core
|
||||
- name: 🏗️ Build Core
|
||||
run: npm run build -- --ci
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
@@ -31,4 +31,6 @@ runs:
|
||||
with:
|
||||
name: ionic-core
|
||||
output: core/CoreBuild.zip
|
||||
paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt
|
||||
# Include generated proxy files from Stencil output targets so
|
||||
# framework builds can detect when they need to be updated
|
||||
paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt packages/angular/src/directives/proxies.ts packages/angular/src/directives/proxies-list.ts packages/angular/standalone/src/directives/proxies.ts packages/vue/src/proxies.ts packages/react/src/components/proxies.ts packages/react/src/components/inner-proxies.ts packages/react/src/components/routing-proxies.ts
|
||||
|
||||
@@ -3,9 +3,9 @@ description: 'Build Ionic React Router'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -16,19 +16,19 @@ runs:
|
||||
name: ionic-react
|
||||
path: ./packages/react
|
||||
filename: ReactBuild.zip
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router
|
||||
|
||||
16
.github/workflows/actions/build-react/action.yml
vendored
@@ -3,35 +3,35 @@ description: 'Build Ionic React'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Install React Dependencies
|
||||
- name: 🕸️ Install React Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
- name: Test Spec
|
||||
- name: 🧪 Test Spec
|
||||
run: npm run test.spec
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
- name: Check Diff
|
||||
- name: 🔍 Check Diff
|
||||
run: git diff --exit-code
|
||||
shell: bash
|
||||
working-directory: ./packages/react
|
||||
|
||||
@@ -3,9 +3,9 @@ description: 'Builds Ionic Vue Router'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -16,23 +16,23 @@ runs:
|
||||
name: ionic-vue
|
||||
path: ./packages/vue
|
||||
filename: VueBuild.zip
|
||||
- name: Install Vue Router Dependencies
|
||||
- name: 🕸️ Install Vue Router Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/vue-router
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/vue-router
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./packages/vue-router
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/vue-router
|
||||
- name: Test Spec
|
||||
- name: 🧪 Test Spec
|
||||
run: npm run test.spec
|
||||
shell: bash
|
||||
working-directory: ./packages/vue-router
|
||||
|
||||
14
.github/workflows/actions/build-vue/action.yml
vendored
@@ -3,31 +3,31 @@ description: 'Build Ionic Vue'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Install Vue Dependencies
|
||||
- name: 🕸️ Install Vue Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: ./packages/vue
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/vue
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./packages/vue
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/vue
|
||||
- name: Check Diff
|
||||
- name: 🔍 Check Diff
|
||||
run: git diff --exit-code
|
||||
shell: bash
|
||||
working-directory: ./packages/vue
|
||||
|
||||
@@ -10,10 +10,10 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
- name: Extract Archive
|
||||
- name: 🔎 Extract Archive
|
||||
run: unzip -q -o ${{ inputs.path }}/${{ inputs.filename }}
|
||||
shell: bash
|
||||
|
||||
@@ -6,9 +6,9 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -24,19 +24,23 @@ runs:
|
||||
name: ionic-angular-server
|
||||
path: ./packages/angular-server
|
||||
filename: AngularServerBuild.zip
|
||||
- name: Create Test App
|
||||
- name: 🧪 Create Test App
|
||||
run: ./build.sh ${{ inputs.app }}
|
||||
shell: bash
|
||||
working-directory: ./packages/angular/test
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
working-directory: ./packages/angular/test/build/${{ inputs.app }}
|
||||
- name: Sync Built Changes
|
||||
- name: 📦 Install Playwright Browsers
|
||||
run: npx playwright install
|
||||
shell: bash
|
||||
working-directory: ./packages/angular/test/build/${{ inputs.app }}
|
||||
- name: 🔄 Sync Built Changes
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/angular/test/build/${{ inputs.app }}
|
||||
- name: Run Tests
|
||||
- name: 🧪 Run Tests
|
||||
run: npm run test
|
||||
shell: bash
|
||||
working-directory: ./packages/angular/test/build/${{ inputs.app }}
|
||||
|
||||
@@ -3,16 +3,16 @@ description: 'Test Core Clean Build'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Check Diff
|
||||
- name: 🔍 Check Diff
|
||||
run: |
|
||||
git diff --exit-code || {
|
||||
echo -e "\033[1;31m⚠️ Error: Differences Detected ⚠️\033[0m"
|
||||
|
||||
@@ -3,21 +3,21 @@ description: 'Test Core Lint'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Install Dependencies
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
- name: Lint
|
||||
- name: 🖌️ Lint
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
# Lint changes should be pushed
|
||||
# to the branch before the branch
|
||||
# is merge eligible.
|
||||
- name: Check Lint Results
|
||||
- name: 🔎 Check Lint Results
|
||||
run: git diff --exit-code
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
|
||||
@@ -13,19 +13,19 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
- name: Test
|
||||
- name: 🧪 Test
|
||||
if: inputs.update != 'true'
|
||||
run: npm run test.e2e.docker.ci ${{ inputs.component }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }}
|
||||
shell: bash
|
||||
@@ -60,13 +60,13 @@ runs:
|
||||
fi
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
- name: Archive Updated Screenshots
|
||||
- name: 📦 Archive Updated Screenshots
|
||||
if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
|
||||
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip
|
||||
- name: Archive Test Results
|
||||
- name: 📦 Archive Test Results
|
||||
# The always() ensures that this step
|
||||
# runs even if the previous step fails.
|
||||
# We want the test results to be archived
|
||||
|
||||
@@ -6,14 +6,14 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Install Dependencies
|
||||
node-version: 24.x
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ./core
|
||||
shell: bash
|
||||
- name: Install Stencil ${{ inputs.stencil-version }}
|
||||
- name: 📦 Install Stencil ${{ inputs.stencil-version }}
|
||||
run: npm install @stencil/core@${{ inputs.stencil-version }}
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
@@ -23,7 +23,7 @@ runs:
|
||||
name: ionic-core
|
||||
path: ./core
|
||||
filename: CoreBuild.zip
|
||||
- name: Test
|
||||
- name: 🧪 Test
|
||||
run: npm run test.spec -- --ci
|
||||
shell: bash
|
||||
working-directory: ./core
|
||||
|
||||
@@ -6,9 +6,9 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -24,23 +24,23 @@ runs:
|
||||
name: ionic-react-router
|
||||
path: ./packages/react-router
|
||||
filename: ReactRouterBuild.zip
|
||||
- name: Create Test App
|
||||
- name: 🧪 Create Test App
|
||||
run: ./build.sh ${{ inputs.app }}
|
||||
shell: bash
|
||||
working-directory: ./packages/react/test
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
working-directory: ./packages/react/test/build/${{ inputs.app }}
|
||||
- name: Sync Built Changes
|
||||
- name: 🔄 Sync Built Changes
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/react/test/build/${{ inputs.app }}
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/react/test/build/${{ inputs.app }}
|
||||
- name: Run Tests
|
||||
- name: 🧪 Run Tests
|
||||
run: npm run e2e
|
||||
shell: bash
|
||||
working-directory: ./packages/react/test/build/${{ inputs.app }}
|
||||
|
||||
@@ -6,9 +6,9 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -24,23 +24,23 @@ runs:
|
||||
name: ionic-react-router
|
||||
path: ./packages/react-router
|
||||
filename: ReactRouterBuild.zip
|
||||
- name: Create Test App
|
||||
- name: 🧪 Create Test App
|
||||
run: ./build.sh ${{ inputs.app }}
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router/test
|
||||
- name: Install Dependencies
|
||||
- name: 🕸️ Install Dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
|
||||
- name: Sync Built Changes
|
||||
- name: 🔄 Sync Built Changes
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
|
||||
- name: Build
|
||||
- name: 🏗️ Build
|
||||
run: npm run build
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
|
||||
- name: Run Tests
|
||||
- name: 🧪 Run Tests
|
||||
run: npm run e2e
|
||||
shell: bash
|
||||
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
|
||||
|
||||
@@ -6,9 +6,9 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
@@ -24,23 +24,23 @@ runs:
|
||||
name: ionic-vue-router
|
||||
path: ./packages/vue-router
|
||||
filename: VueRouterBuild.zip
|
||||
- name: Create Test App
|
||||
- name: 🧪 Create Test App
|
||||
run: ./build.sh ${{ inputs.app }}
|
||||
shell: bash
|
||||
working-directory: ./packages/vue/test
|
||||
- name: Install Dependencies
|
||||
- name: 📦 Install Dependencies
|
||||
run: npm install
|
||||
shell: bash
|
||||
working-directory: ./packages/vue/test/build/${{ inputs.app }}
|
||||
- name: Sync
|
||||
- name: 🔄 Sync
|
||||
run: npm run sync
|
||||
shell: bash
|
||||
working-directory: ./packages/vue/test/build/${{ inputs.app }}
|
||||
- name: Run Spec Tests
|
||||
- name: 🧪 Run Spec Tests
|
||||
run: npm run test:unit
|
||||
shell: bash
|
||||
working-directory: ./packages/vue/test/build/${{ inputs.app }}
|
||||
- name: Run E2E Tests
|
||||
- name: 🧪 Run E2E Tests
|
||||
run: npm run test:e2e
|
||||
shell: bash
|
||||
working-directory: ./packages/vue/test/build/${{ inputs.app }}
|
||||
|
||||
@@ -7,13 +7,13 @@ on:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
- uses: actions/download-artifact@v4
|
||||
node-version: 24.x
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ./artifacts
|
||||
- name: Extract Archives
|
||||
- name: 🔎 Extract Archives
|
||||
# This finds all .zip files in the ./artifacts
|
||||
# directory, including nested directories.
|
||||
# It then unzips every .zip to the root directory
|
||||
@@ -21,7 +21,7 @@ runs:
|
||||
find . -type f -name 'UpdatedScreenshots-*.zip' -exec unzip -q -o -d ../ {} \;
|
||||
shell: bash
|
||||
working-directory: ./artifacts
|
||||
- name: Push Screenshots
|
||||
- name: 📸 Push Screenshots
|
||||
# Configure user as Ionitron
|
||||
# and push only the changed .png snapshots
|
||||
# to the remote branch.
|
||||
|
||||
@@ -10,10 +10,10 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Create Archive
|
||||
- name: 🗄️ Create Archive
|
||||
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
|
||||
shell: bash
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.output }}
|
||||
|
||||
2
.github/workflows/assign-issues.yml
vendored
@@ -13,6 +13,6 @@ jobs:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
|
||||
with:
|
||||
assignees: brandyscarney, ShaneK
|
||||
assignees: brandyscarney, thetaPC, ShaneK
|
||||
numOfAssignee: 1
|
||||
allowSelfAssign: false
|
||||
|
||||
30
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
@@ -14,8 +14,8 @@ jobs:
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: github/codeql-action/init@v3
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: javascript
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
- uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/conventional-commit.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
if: |
|
||||
!contains(github.event.pull_request.title, 'release') &&
|
||||
!contains(github.event.pull_request.title, 'chore')
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
11
.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@v4
|
||||
- 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,13 +29,12 @@ jobs:
|
||||
release-ionic:
|
||||
needs: [create-dev-hash]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/release-ionic.yml
|
||||
with:
|
||||
tag: dev
|
||||
version: ${{ needs.create-dev-hash.outputs.dev-hash }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
get-build:
|
||||
name: Get your dev build!
|
||||
|
||||
2
.github/workflows/label.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
||||
|
||||
15
.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@v4
|
||||
- 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,10 +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 }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
47
.github/workflows/release-ionic.yml
vendored
@@ -14,23 +14,23 @@ on:
|
||||
preid:
|
||||
description: 'The prerelease identifier used when doing a prerelease.'
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'core'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Cache Built @ionic/core
|
||||
uses: ./.github/workflows/actions/upload-archive
|
||||
with:
|
||||
@@ -48,34 +48,33 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/docs'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
release-angular:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
@@ -83,7 +82,6 @@ jobs:
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/angular'
|
||||
folder: './dist'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Cache Built @ionic/angular
|
||||
uses: ./.github/workflows/actions/upload-archive
|
||||
with:
|
||||
@@ -95,21 +93,20 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/react'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Cache Built @ionic/react
|
||||
uses: ./.github/workflows/actions/upload-archive
|
||||
with:
|
||||
@@ -121,21 +118,20 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/vue'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Cache Built @ionic/vue
|
||||
uses: ./.github/workflows/actions/upload-archive
|
||||
with:
|
||||
@@ -147,14 +143,14 @@ jobs:
|
||||
needs: [release-core]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
@@ -162,13 +158,12 @@ jobs:
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/angular-server'
|
||||
folder: './dist'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
release-react-router:
|
||||
needs: [release-react]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
@@ -181,20 +176,19 @@ 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/react-router'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
release-vue-router:
|
||||
needs: [release-vue]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore @ionic/core built cache
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
@@ -207,11 +201,10 @@ 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 }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
working-directory: 'packages/vue-router'
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
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 }}
|
||||
69
.github/workflows/release.yml
vendored
@@ -1,54 +1,64 @@
|
||||
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".
|
||||
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
|
||||
uses: ./.github/workflows/release-ionic.yml
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
version: ${{ inputs.version }}
|
||||
preid: ${{ inputs.preid }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
finalize-release:
|
||||
needs: [release-ionic]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
- 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
|
||||
|
||||
62
.github/workflows/stencil-nightly.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
build-core-with-stencil-nightly:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: ./.github/workflows/actions/test-react-e2e
|
||||
with:
|
||||
app: ${{ matrix.apps }}
|
||||
@@ -225,3 +225,35 @@ jobs:
|
||||
- name: Check build matrix status
|
||||
if: ${{ needs.test-react-e2e.result != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
send-success-messages:
|
||||
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
|
||||
steps:
|
||||
- name: Notify success on Discord
|
||||
run: |
|
||||
curl -H "Content-Type:application/json" \
|
||||
-d '{"embeds": [{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "color": 65280, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
|
||||
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
|
||||
- name: Notify success on Slack
|
||||
run: |
|
||||
curl -H "Content-Type:application/json" \
|
||||
-d '{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
|
||||
${{secrets.SLACK_NOTIFY_SUCCESS_WEBHOOK}}
|
||||
|
||||
send-failure-messages:
|
||||
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
|
||||
steps:
|
||||
- name: Notify failure on Discord
|
||||
run: |
|
||||
curl -H "Content-Type:application/json" \
|
||||
-d '{"content": "Alerting <@&1347593178580254761>!", "embeds": [{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "color": 16711680, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
|
||||
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
|
||||
- name: Notify failure on Slack
|
||||
run: |
|
||||
curl -H "Content-Type:application/json" \
|
||||
-d '{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
|
||||
${{secrets.SLACK_NOTIFY_FAILURE_WEBHOOK}}
|
||||
|
||||
6
.github/workflows/update-screenshots.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
- 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@v4
|
||||
- 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
|
||||
|
||||
178
CHANGELOG.md
@@ -3,6 +3,184 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b))
|
||||
* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326)
|
||||
* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684)
|
||||
* **vue:** emit component-specific overlay events ([#30688](https://github.com/ionic-team/ionic-framework/issues/30688)) ([024d090](https://github.com/ionic-team/ionic-framework/commit/024d090122548e26ec2cdcfae4637dde8f288278)), closes [#30641](https://github.com/ionic-team/ionic-framework/issues/30641)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input:** improve error text accessibility ([#30635](https://github.com/ionic-team/ionic-framework/issues/30635)) ([c339bc3](https://github.com/ionic-team/ionic-framework/commit/c339bc36827b62ef871325869a9a5db9b17ac785))
|
||||
* **overlays,picker:** remove invalid aria-hidden attribute ([#30563](https://github.com/ionic-team/ionic-framework/issues/30563)) ([49f96d7](https://github.com/ionic-team/ionic-framework/commit/49f96d7f1e9050a95e3e33a821c0467ecc0bed64)), closes [#30040](https://github.com/ionic-team/ionic-framework/issues/30040)
|
||||
* **segment-view:** scroll and select the right item when the component is in RTL context; ([#30675](https://github.com/ionic-team/ionic-framework/issues/30675)) ([66f517d](https://github.com/ionic-team/ionic-framework/commit/66f517d5b2154fff00b294a78f4107f057a580c6)), closes [#30079](https://github.com/ionic-team/ionic-framework/issues/30079)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.3](https://github.com/ionic-team/ionic-framework/compare/v8.7.2...v8.7.3) (2025-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox:** add aria attributes to ignore checkbox icon ([#30633](https://github.com/ionic-team/ionic-framework/issues/30633)) ([e9e6605](https://github.com/ionic-team/ionic-framework/commit/e9e6605862a05a46d26c26a144ed1cf22133a2b7)), closes [#30231](https://github.com/ionic-team/ionic-framework/issues/30231)
|
||||
* **refresher:** prevent focus-related scroll jumps on refresh ([#30636](https://github.com/ionic-team/ionic-framework/issues/30636)) ([1899b49](https://github.com/ionic-team/ionic-framework/commit/1899b49d252abc6003f763cea8db2a51efa941ec))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **reorder-group:** add children fallback for framework compatibility ([#30593](https://github.com/ionic-team/ionic-framework/issues/30593)) ([1cd81b9](https://github.com/ionic-team/ionic-framework/commit/1cd81b92301378d55bce63a01dfcf95a91c92652)), closes [#30592](https://github.com/ionic-team/ionic-framework/issues/30592)
|
||||
* **tabs:** add fallback to select tab if router integration fails ([#30599](https://github.com/ionic-team/ionic-framework/issues/30599)) ([a2e803a](https://github.com/ionic-team/ionic-framework/commit/a2e803a553dc58fc0e1599e515a56180a7ab263a)), closes [#30552](https://github.com/ionic-team/ionic-framework/issues/30552)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** upgrade `@stencil/core` to version 4.36.2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **css:** add new css utility classes for display and flex utils ([#30567](https://github.com/ionic-team/ionic-framework/issues/30567)) ([75f6c05](https://github.com/ionic-team/ionic-framework/commit/75f6c05fb96313ef890cc80a229a3a3ed3d57460)), closes [#22469](https://github.com/ionic-team/ionic-framework/issues/22469)
|
||||
* **datetime:** add border property to highlightedDates ([#30534](https://github.com/ionic-team/ionic-framework/issues/30534)) ([d5627c7](https://github.com/ionic-team/ionic-framework/commit/d5627c73681faf658ea3b869f3fb04d708391eb9)), closes [#29833](https://github.com/ionic-team/ionic-framework/issues/29833)
|
||||
* **deps:** update ionicons to v8 ([#30390](https://github.com/ionic-team/ionic-framework/issues/30390)) ([74cd71a](https://github.com/ionic-team/ionic-framework/commit/74cd71af243183aa738d11b280e155bdfd652126)), closes [#30445](https://github.com/ionic-team/ionic-framework/issues/30445)
|
||||
* **modal:** add IonModalToken for injecting modal elements in Angular components ([#30474](https://github.com/ionic-team/ionic-framework/issues/30474)) ([30d1910](https://github.com/ionic-team/ionic-framework/commit/30d1910d6ea5428b414d0e127e7681f59426c538))
|
||||
* **reorder-group:** add ionReorderStart, ionReorderMove, ionReorderEnd events ([#30471](https://github.com/ionic-team/ionic-framework/issues/30471)) ([b154f4e](https://github.com/ionic-team/ionic-framework/commit/b154f4ed095890f57ccab539fd9217976a5466e5)), closes [#23148](https://github.com/ionic-team/ionic-framework/issues/23148) [#27614](https://github.com/ionic-team/ionic-framework/issues/27614)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** downgrade `@stencil/core` to version 4.33.1
|
||||
|
||||
_Stencil has been downgraded due to an uncaught regression in Reorder._
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** upgrade `@stencil/core` to version 4.36.2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,182 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b))
|
||||
* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326)
|
||||
* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input:** improve error text accessibility ([#30635](https://github.com/ionic-team/ionic-framework/issues/30635)) ([c339bc3](https://github.com/ionic-team/ionic-framework/commit/c339bc36827b62ef871325869a9a5db9b17ac785))
|
||||
* **overlays,picker:** remove invalid aria-hidden attribute ([#30563](https://github.com/ionic-team/ionic-framework/issues/30563)) ([49f96d7](https://github.com/ionic-team/ionic-framework/commit/49f96d7f1e9050a95e3e33a821c0467ecc0bed64)), closes [#30040](https://github.com/ionic-team/ionic-framework/issues/30040)
|
||||
* **segment-view:** scroll and select the right item when the component is in RTL context; ([#30675](https://github.com/ionic-team/ionic-framework/issues/30675)) ([66f517d](https://github.com/ionic-team/ionic-framework/commit/66f517d5b2154fff00b294a78f4107f057a580c6)), closes [#30079](https://github.com/ionic-team/ionic-framework/issues/30079)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.3](https://github.com/ionic-team/ionic-framework/compare/v8.7.2...v8.7.3) (2025-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox:** add aria attributes to ignore checkbox icon ([#30633](https://github.com/ionic-team/ionic-framework/issues/30633)) ([e9e6605](https://github.com/ionic-team/ionic-framework/commit/e9e6605862a05a46d26c26a144ed1cf22133a2b7)), closes [#30231](https://github.com/ionic-team/ionic-framework/issues/30231)
|
||||
* **refresher:** prevent focus-related scroll jumps on refresh ([#30636](https://github.com/ionic-team/ionic-framework/issues/30636)) ([1899b49](https://github.com/ionic-team/ionic-framework/commit/1899b49d252abc6003f763cea8db2a51efa941ec))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **reorder-group:** add children fallback for framework compatibility ([#30593](https://github.com/ionic-team/ionic-framework/issues/30593)) ([1cd81b9](https://github.com/ionic-team/ionic-framework/commit/1cd81b92301378d55bce63a01dfcf95a91c92652)), closes [#30592](https://github.com/ionic-team/ionic-framework/issues/30592)
|
||||
* **tabs:** add fallback to select tab if router integration fails ([#30599](https://github.com/ionic-team/ionic-framework/issues/30599)) ([a2e803a](https://github.com/ionic-team/ionic-framework/commit/a2e803a553dc58fc0e1599e515a56180a7ab263a)), closes [#30552](https://github.com/ionic-team/ionic-framework/issues/30552)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** upgrade `@stencil/core` to version 4.36.2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **css:** add new css utility classes for display and flex utils ([#30567](https://github.com/ionic-team/ionic-framework/issues/30567)) ([75f6c05](https://github.com/ionic-team/ionic-framework/commit/75f6c05fb96313ef890cc80a229a3a3ed3d57460)), closes [#22469](https://github.com/ionic-team/ionic-framework/issues/22469)
|
||||
* **datetime:** add border property to highlightedDates ([#30534](https://github.com/ionic-team/ionic-framework/issues/30534)) ([d5627c7](https://github.com/ionic-team/ionic-framework/commit/d5627c73681faf658ea3b869f3fb04d708391eb9)), closes [#29833](https://github.com/ionic-team/ionic-framework/issues/29833)
|
||||
* **deps:** update ionicons to v8 ([#30390](https://github.com/ionic-team/ionic-framework/issues/30390)) ([74cd71a](https://github.com/ionic-team/ionic-framework/commit/74cd71af243183aa738d11b280e155bdfd652126)), closes [#30445](https://github.com/ionic-team/ionic-framework/issues/30445)
|
||||
* **reorder-group:** add ionReorderStart, ionReorderMove, ionReorderEnd events ([#30471](https://github.com/ionic-team/ionic-framework/issues/30471)) ([b154f4e](https://github.com/ionic-team/ionic-framework/commit/b154f4ed095890f57ccab539fd9217976a5466e5)), closes [#23148](https://github.com/ionic-team/ionic-framework/issues/23148) [#27614](https://github.com/ionic-team/ionic-framework/issues/27614)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** downgrade `@stencil/core` to version 4.33.1
|
||||
|
||||
_Stencil has been downgraded due to an uncaught regression in Reorder._
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* **stencil:** upgrade `@stencil/core` to version 4.36.2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Get Playwright
|
||||
FROM mcr.microsoft.com/playwright:v1.54.1
|
||||
FROM mcr.microsoft.com/playwright:v1.56.1
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /ionic
|
||||
|
||||
@@ -1508,6 +1508,9 @@ ion-reorder-group,none
|
||||
ion-reorder-group,prop,disabled,boolean,true,false,false
|
||||
ion-reorder-group,method,complete,complete(listOrReorder?: boolean | any[]) => Promise<any>
|
||||
ion-reorder-group,event,ionItemReorder,ItemReorderEventDetail,true
|
||||
ion-reorder-group,event,ionReorderEnd,ReorderEndEventDetail,true
|
||||
ion-reorder-group,event,ionReorderMove,ReorderMoveEventDetail,true
|
||||
ion-reorder-group,event,ionReorderStart,void,true
|
||||
|
||||
ion-ripple-effect,shadow
|
||||
ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false
|
||||
|
||||
10715
core/package-lock.json
generated
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.6.5",
|
||||
"version": "8.7.12",
|
||||
"description": "Base components for Ionic",
|
||||
"engines": {
|
||||
"node": "24.x"
|
||||
},
|
||||
"keywords": [
|
||||
"ionic",
|
||||
"framework",
|
||||
@@ -31,20 +34,20 @@
|
||||
"loader/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.33.1",
|
||||
"ionicons": "^7.2.2",
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.2",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@axe-core/playwright": "^4.11.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",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@stencil/angular-output-target": "^0.10.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);
|
||||
}
|
||||
|
||||
|
||||
28
core/src/components.d.ts
vendored
@@ -30,7 +30,7 @@ import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
|
||||
import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
|
||||
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
|
||||
import { RefresherEventDetail } from "./components/refresher/refresher-interface";
|
||||
import { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
|
||||
import { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
|
||||
import { NavigationHookCallback } from "./components/route/route-interface";
|
||||
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
|
||||
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
|
||||
@@ -68,7 +68,7 @@ export { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
|
||||
export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
|
||||
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
|
||||
export { RefresherEventDetail } from "./components/refresher/refresher-interface";
|
||||
export { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
|
||||
export { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
|
||||
export { NavigationHookCallback } from "./components/route/route-interface";
|
||||
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
|
||||
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
|
||||
@@ -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.
|
||||
@@ -2783,7 +2787,7 @@ export namespace Components {
|
||||
}
|
||||
interface IonReorderGroup {
|
||||
/**
|
||||
* Completes the reorder operation. Must be called by the `ionItemReorder` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position.
|
||||
* Completes the reorder operation. Must be called by the `ionReorderEnd` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position.
|
||||
* @param listOrReorder A list of items to be sorted and returned in the new order or a boolean of whether or not the reorder should reposition the item.
|
||||
*/
|
||||
"complete": (listOrReorder?: boolean | any[]) => Promise<any>;
|
||||
@@ -4769,6 +4773,9 @@ declare global {
|
||||
};
|
||||
interface HTMLIonReorderGroupElementEventMap {
|
||||
"ionItemReorder": ItemReorderEventDetail;
|
||||
"ionReorderStart": void;
|
||||
"ionReorderMove": ReorderMoveEventDetail;
|
||||
"ionReorderEnd": ReorderEndEventDetail;
|
||||
}
|
||||
interface HTMLIonReorderGroupElement extends Components.IonReorderGroup, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonReorderGroupElementEventMap>(type: K, listener: (this: HTMLIonReorderGroupElement, ev: IonReorderGroupCustomEvent<HTMLIonReorderGroupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
@@ -8053,9 +8060,22 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* Event that needs to be listened to in order to complete the reorder action. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action.
|
||||
* Event that needs to be listened to in order to complete the reorder action.
|
||||
* @deprecated Use `ionReorderEnd` instead. If you are accessing `event.detail.from` or `event.detail.to` and relying on them being different you should now add checks as they are always emitted in `ionReorderEnd`, even when they are the same.
|
||||
*/
|
||||
"onIonItemReorder"?: (event: IonReorderGroupCustomEvent<ItemReorderEventDetail>) => void;
|
||||
/**
|
||||
* Event that is emitted when the reorder gesture ends. The from and to properties are always available, regardless of if the reorder gesture moved the item. If the item did not change from its start position, the from and to properties will be the same. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action.
|
||||
*/
|
||||
"onIonReorderEnd"?: (event: IonReorderGroupCustomEvent<ReorderEndEventDetail>) => void;
|
||||
/**
|
||||
* Event that is emitted as the reorder gesture moves.
|
||||
*/
|
||||
"onIonReorderMove"?: (event: IonReorderGroupCustomEvent<ReorderMoveEventDetail>) => void;
|
||||
/**
|
||||
* Event that is emitted when the reorder gesture starts.
|
||||
*/
|
||||
"onIonReorderStart"?: (event: IonReorderGroupCustomEvent<void>) => void;
|
||||
}
|
||||
interface IonRippleEffect {
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,40 @@ const enum AccordionState {
|
||||
})
|
||||
export class Accordion implements ComponentInterface {
|
||||
private accordionGroupEl?: HTMLIonAccordionGroupElement | null;
|
||||
private updateListener = () => this.updateState(false);
|
||||
private accordionGroupUpdateHandler = () => {
|
||||
/**
|
||||
* Determine if this update will cause an actual state change.
|
||||
* We only want to mark as "interacted" if the state is changing.
|
||||
*/
|
||||
const accordionGroup = this.accordionGroupEl;
|
||||
if (accordionGroup) {
|
||||
const value = accordionGroup.value;
|
||||
const accordionValue = this.value;
|
||||
const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
|
||||
const isExpanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding;
|
||||
const stateWillChange = shouldExpand !== isExpanded;
|
||||
|
||||
/**
|
||||
* Only mark as interacted if:
|
||||
* 1. This is not the first update we've received with a defined value
|
||||
* 2. The state is actually changing (prevents redundant updates from enabling animations)
|
||||
*/
|
||||
if (this.hasReceivedFirstUpdate && stateWillChange) {
|
||||
this.hasInteracted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only count this as the first update if the group value is defined.
|
||||
* This prevents the initial undefined value from the group's componentDidLoad
|
||||
* from being treated as the first real update.
|
||||
*/
|
||||
if (value !== undefined) {
|
||||
this.hasReceivedFirstUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateState();
|
||||
};
|
||||
private contentEl: HTMLDivElement | undefined;
|
||||
private contentElWrapper: HTMLDivElement | undefined;
|
||||
private headerEl: HTMLDivElement | undefined;
|
||||
@@ -50,6 +83,25 @@ export class Accordion implements ComponentInterface {
|
||||
@State() state: AccordionState = AccordionState.Collapsed;
|
||||
@State() isNext = false;
|
||||
@State() isPrevious = false;
|
||||
/**
|
||||
* Tracks whether a user-initiated interaction has occurred.
|
||||
* Animations are disabled until the first interaction happens.
|
||||
* This prevents the accordion from animating when it's programmatically
|
||||
* set to an expanded or collapsed state on initial load.
|
||||
*/
|
||||
@State() hasInteracted = false;
|
||||
|
||||
/**
|
||||
* Tracks if this accordion has ever been expanded.
|
||||
* Used to prevent the first expansion from animating.
|
||||
*/
|
||||
private hasEverBeenExpanded = false;
|
||||
|
||||
/**
|
||||
* Tracks if this accordion has received its first update from the group.
|
||||
* Used to distinguish initial programmatic sets from user interactions.
|
||||
*/
|
||||
private hasReceivedFirstUpdate = false;
|
||||
|
||||
/**
|
||||
* The value of the accordion. Defaults to an autogenerated
|
||||
@@ -88,15 +140,15 @@ export class Accordion implements ComponentInterface {
|
||||
connectedCallback() {
|
||||
const accordionGroupEl = (this.accordionGroupEl = this.el?.closest('ion-accordion-group'));
|
||||
if (accordionGroupEl) {
|
||||
this.updateState(true);
|
||||
addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
|
||||
this.updateState();
|
||||
addEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
const accordionGroupEl = this.accordionGroupEl;
|
||||
if (accordionGroupEl) {
|
||||
removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
|
||||
removeEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +264,16 @@ export class Accordion implements ComponentInterface {
|
||||
ionItem.appendChild(iconEl);
|
||||
};
|
||||
|
||||
private expandAccordion = (initialUpdate = false) => {
|
||||
private expandAccordion = () => {
|
||||
const { contentEl, contentElWrapper } = this;
|
||||
if (initialUpdate || contentEl === undefined || contentElWrapper === undefined) {
|
||||
|
||||
/**
|
||||
* If the content elements aren't available yet, just set the state.
|
||||
* This happens on initial render before the DOM is ready.
|
||||
*/
|
||||
if (contentEl === undefined || contentElWrapper === undefined) {
|
||||
this.state = AccordionState.Expanded;
|
||||
this.hasEverBeenExpanded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,6 +285,12 @@ export class Accordion implements ComponentInterface {
|
||||
cancelAnimationFrame(this.currentRaf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that this accordion has been expanded at least once.
|
||||
* This allows subsequent expansions to animate.
|
||||
*/
|
||||
this.hasEverBeenExpanded = true;
|
||||
|
||||
if (this.shouldAnimate()) {
|
||||
raf(() => {
|
||||
this.state = AccordionState.Expanding;
|
||||
@@ -247,9 +311,14 @@ export class Accordion implements ComponentInterface {
|
||||
}
|
||||
};
|
||||
|
||||
private collapseAccordion = (initialUpdate = false) => {
|
||||
private collapseAccordion = () => {
|
||||
const { contentEl } = this;
|
||||
if (initialUpdate || contentEl === undefined) {
|
||||
|
||||
/**
|
||||
* If the content element isn't available yet, just set the state.
|
||||
* This happens on initial render before the DOM is ready.
|
||||
*/
|
||||
if (contentEl === undefined) {
|
||||
this.state = AccordionState.Collapsed;
|
||||
return;
|
||||
}
|
||||
@@ -291,6 +360,19 @@ export class Accordion implements ComponentInterface {
|
||||
* of what is set in the config.
|
||||
*/
|
||||
private shouldAnimate = () => {
|
||||
/**
|
||||
* Don't animate until after the first user interaction.
|
||||
* This prevents animations on initial load when accordions
|
||||
* start in an expanded or collapsed state programmatically.
|
||||
*
|
||||
* Additionally, don't animate the very first expansion even if
|
||||
* hasInteracted is true. This handles edge cases like React StrictMode
|
||||
* where effects run twice and might incorrectly mark as interacted.
|
||||
*/
|
||||
if (!this.hasInteracted || !this.hasEverBeenExpanded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof (window as any) === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
@@ -312,7 +394,7 @@ export class Accordion implements ComponentInterface {
|
||||
return true;
|
||||
};
|
||||
|
||||
private updateState = async (initialUpdate = false) => {
|
||||
private updateState = async () => {
|
||||
const accordionGroup = this.accordionGroupEl;
|
||||
const accordionValue = this.value;
|
||||
|
||||
@@ -325,10 +407,10 @@ export class Accordion implements ComponentInterface {
|
||||
const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue;
|
||||
|
||||
if (shouldExpand) {
|
||||
this.expandAccordion(initialUpdate);
|
||||
this.expandAccordion();
|
||||
this.isNext = this.isPrevious = false;
|
||||
} else {
|
||||
this.collapseAccordion(initialUpdate);
|
||||
this.collapseAccordion();
|
||||
|
||||
/**
|
||||
* When using popout or inset,
|
||||
@@ -386,6 +468,12 @@ export class Accordion implements ComponentInterface {
|
||||
|
||||
if (disabled || readonly) return;
|
||||
|
||||
/**
|
||||
* Mark that the user has interacted with the accordion.
|
||||
* This enables animations for all future state changes.
|
||||
*/
|
||||
this.hasInteracted = true;
|
||||
|
||||
if (accordionGroupEl) {
|
||||
/**
|
||||
* Because the accordion group may or may
|
||||
|
||||
@@ -200,6 +200,87 @@ it('should set default values if not provided', async () => {
|
||||
expect(accordion.classList.contains('accordion-collapsed')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not animate when initial value is set before load', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Item, Accordion, AccordionGroup],
|
||||
});
|
||||
|
||||
const accordionGroup = page.doc.createElement('ion-accordion-group');
|
||||
accordionGroup.innerHTML = `
|
||||
<ion-accordion value="first">
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
<ion-accordion value="second">
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
`;
|
||||
|
||||
accordionGroup.value = 'first';
|
||||
page.body.appendChild(accordionGroup);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const firstAccordion = accordionGroup.querySelector('ion-accordion[value="first"]')!;
|
||||
|
||||
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
|
||||
expect(firstAccordion.classList.contains('accordion-expanding')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not animate when initial value is set after load', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Item, Accordion, AccordionGroup],
|
||||
});
|
||||
|
||||
const accordionGroup = page.doc.createElement('ion-accordion-group');
|
||||
accordionGroup.innerHTML = `
|
||||
<ion-accordion value="first">
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
<ion-accordion value="second">
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
`;
|
||||
|
||||
page.body.appendChild(accordionGroup);
|
||||
await page.waitForChanges();
|
||||
|
||||
accordionGroup.value = 'first';
|
||||
await page.waitForChanges();
|
||||
|
||||
const firstAccordion = accordionGroup.querySelector('ion-accordion[value="first"]')!;
|
||||
|
||||
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
|
||||
expect(firstAccordion.classList.contains('accordion-expanding')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not have animated class on first expansion', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Item, Accordion, AccordionGroup],
|
||||
html: `
|
||||
<ion-accordion-group>
|
||||
<ion-accordion value="first">
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
`,
|
||||
});
|
||||
|
||||
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
|
||||
const firstAccordion = page.body.querySelector('ion-accordion[value="first"]')!;
|
||||
|
||||
// First expansion should not have the animated class
|
||||
accordionGroup.value = 'first';
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(firstAccordion.classList.contains('accordion-animated')).toEqual(false);
|
||||
expect(firstAccordion.classList.contains('accordion-expanded')).toEqual(true);
|
||||
});
|
||||
|
||||
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/27047
|
||||
it('should not have animated class when animated="false"', async () => {
|
||||
const page = await newSpecPage({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -125,7 +125,7 @@
|
||||
|
||||
<ion-toolbar color="dark">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button class="ion-hide"></ion-back-button>
|
||||
<ion-back-button class="ion-display-none"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Hidden</ion-title>
|
||||
</ion-toolbar>
|
||||
|
||||
@@ -361,11 +361,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
target,
|
||||
};
|
||||
let fill = this.fill;
|
||||
/**
|
||||
* We check both undefined and null to
|
||||
* work around https://github.com/ionic-team/stencil/issues/3586.
|
||||
*/
|
||||
if (fill == null) {
|
||||
if (fill === undefined) {
|
||||
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,5 +1,6 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
|
||||
import { Build, Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core';
|
||||
import { checkInvalidState } from '@utils/forms';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -34,8 +35,8 @@ export class Checkbox implements ComponentInterface {
|
||||
private inputLabelId = `${this.inputId}-lbl`;
|
||||
private helperTextId = `${this.inputId}-helper-text`;
|
||||
private errorTextId = `${this.inputId}-error-text`;
|
||||
private focusEl?: HTMLElement;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private validationObserver?: MutationObserver;
|
||||
|
||||
@Element() el!: HTMLIonCheckboxElement;
|
||||
|
||||
@@ -121,6 +122,13 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Track validation state for proper aria-live announcements.
|
||||
*/
|
||||
@State() isInvalid = false;
|
||||
|
||||
@State() private hintTextId?: string;
|
||||
|
||||
/**
|
||||
* Emitted when the checked property has changed as a result of a user action such as a click.
|
||||
*
|
||||
@@ -138,18 +146,69 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
|
||||
connectedCallback() {
|
||||
const { el } = this;
|
||||
|
||||
// Watch for class changes to update validation state.
|
||||
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
|
||||
this.validationObserver = new MutationObserver(() => {
|
||||
const newIsInvalid = checkInvalidState(el);
|
||||
if (this.isInvalid !== newIsInvalid) {
|
||||
this.isInvalid = newIsInvalid;
|
||||
/**
|
||||
* Screen readers tend to announce changes
|
||||
* to `aria-describedby` when the attribute
|
||||
* is changed during a blur event for a
|
||||
* native form control.
|
||||
* However, the announcement can be spotty
|
||||
* when using a non-native form control
|
||||
* and `forceUpdate()`.
|
||||
* This is due to `forceUpdate()` internally
|
||||
* rescheduling the DOM update to a lower
|
||||
* priority queue regardless if it's called
|
||||
* inside a Promise or not, thus causing
|
||||
* the screen reader to potentially miss the
|
||||
* change.
|
||||
* By using a State variable inside a Promise,
|
||||
* it guarantees a re-render immediately at
|
||||
* a higher priority.
|
||||
*/
|
||||
Promise.resolve().then(() => {
|
||||
this.hintTextId = this.getHintTextId();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.validationObserver.observe(el, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
}
|
||||
|
||||
// Always set initial state
|
||||
this.isInvalid = checkInvalidState(el);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.inheritedAttributes = {
|
||||
...inheritAriaAttributes(this.el),
|
||||
};
|
||||
|
||||
this.hintTextId = this.getHintTextId();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Clean up validation observer to prevent memory leaks.
|
||||
if (this.validationObserver) {
|
||||
this.validationObserver.disconnect();
|
||||
this.validationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
if (this.focusEl) {
|
||||
this.focusEl.focus();
|
||||
}
|
||||
this.el.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +228,6 @@ export class Checkbox implements ComponentInterface {
|
||||
private toggleChecked = (ev: Event) => {
|
||||
ev.preventDefault();
|
||||
|
||||
this.setFocus();
|
||||
this.setChecked(!this.checked);
|
||||
this.indeterminate = false;
|
||||
};
|
||||
@@ -207,10 +265,10 @@ export class Checkbox implements ComponentInterface {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private getHintTextID(): string | undefined {
|
||||
const { el, helperText, errorText, helperTextId, errorTextId } = this;
|
||||
private getHintTextId(): string | undefined {
|
||||
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
||||
|
||||
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
|
||||
if (isInvalid && errorText) {
|
||||
return errorTextId;
|
||||
}
|
||||
|
||||
@@ -226,7 +284,7 @@ export class Checkbox implements ComponentInterface {
|
||||
* This element should only be rendered if hint text is set.
|
||||
*/
|
||||
private renderHintText() {
|
||||
const { helperText, errorText, helperTextId, errorTextId } = this;
|
||||
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
||||
|
||||
/**
|
||||
* undefined and empty string values should
|
||||
@@ -239,11 +297,11 @@ export class Checkbox implements ComponentInterface {
|
||||
|
||||
return (
|
||||
<div class="checkbox-bottom">
|
||||
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
|
||||
{helperText}
|
||||
<div id={helperTextId} class="helper-text" part="supporting-text helper-text" aria-live="polite">
|
||||
{!isInvalid ? helperText : null}
|
||||
</div>
|
||||
<div id={errorTextId} class="error-text" part="supporting-text error-text">
|
||||
{errorText}
|
||||
<div id={errorTextId} class="error-text" part="supporting-text error-text" role="alert">
|
||||
{isInvalid ? errorText : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -278,13 +336,17 @@ export class Checkbox implements ComponentInterface {
|
||||
<Host
|
||||
role="checkbox"
|
||||
aria-checked={indeterminate ? 'mixed' : `${checked}`}
|
||||
aria-describedby={this.getHintTextID()}
|
||||
aria-invalid={this.getHintTextID() === this.errorTextId}
|
||||
aria-describedby={this.hintTextId}
|
||||
aria-invalid={this.isInvalid ? 'true' : undefined}
|
||||
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
|
||||
aria-label={inheritedAttributes['aria-label'] || null}
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
aria-required={required ? 'true' : undefined}
|
||||
tabindex={disabled ? undefined : 0}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onClick={this.onClick}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
'in-item': hostContext('ion-item', el),
|
||||
@@ -296,7 +358,6 @@ export class Checkbox implements ComponentInterface {
|
||||
[`checkbox-alignment-${alignment}`]: alignment !== undefined,
|
||||
[`checkbox-label-placement-${labelPlacement}`]: true,
|
||||
})}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<label class="checkbox-wrapper" htmlFor={inputId}>
|
||||
{/*
|
||||
@@ -309,9 +370,6 @@ export class Checkbox implements ComponentInterface {
|
||||
disabled={disabled}
|
||||
id={inputId}
|
||||
onChange={this.toggleChecked}
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...inheritedAttributes}
|
||||
/>
|
||||
@@ -328,7 +386,7 @@ export class Checkbox implements ComponentInterface {
|
||||
{this.renderHintText()}
|
||||
</div>
|
||||
<div class="native-wrapper">
|
||||
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
|
||||
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container" aria-hidden="true">
|
||||
{path}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -44,7 +44,10 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('checkbox: ionChange'), () => {
|
||||
test('should fire ionChange when interacting with checkbox', async ({ page }) => {
|
||||
await page.setContent(
|
||||
@@ -133,4 +136,195 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
expect(clickCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('checkbox: ionFocus'), () => {
|
||||
test('should not have visual regressions', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
#container {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="container">
|
||||
<ion-checkbox>Unchecked</ion-checkbox>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
const container = page.locator('#container');
|
||||
|
||||
await expect(container).toHaveScreenshot(screenshot(`checkbox-focus`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions when interacting with checkbox in item', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-item class="ion-focused">
|
||||
<ion-checkbox>Unchecked</ion-checkbox>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
// Test focus with keyboard navigation
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
|
||||
await expect(item).toHaveScreenshot(screenshot(`checkbox-in-item-focus`));
|
||||
});
|
||||
|
||||
test('should fire ionFocus when checkbox is focused', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionFocus = await page.spyOnEvent('ionFocus');
|
||||
|
||||
// Test focus with keyboard navigation
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
expect(ionFocus).toHaveReceivedEventTimes(1);
|
||||
|
||||
// Reset focus
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
const checkboxBoundingBox = (await checkbox.boundingBox())!;
|
||||
await page.mouse.click(0, checkboxBoundingBox.height + 1);
|
||||
|
||||
// Test focus with click
|
||||
await checkbox.click();
|
||||
|
||||
expect(ionFocus).toHaveReceivedEventTimes(2);
|
||||
});
|
||||
|
||||
test('should fire ionFocus when interacting with checkbox in item', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-item>
|
||||
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionFocus = await page.spyOnEvent('ionFocus');
|
||||
|
||||
// Test focus with keyboard navigation
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
expect(ionFocus).toHaveReceivedEventTimes(1);
|
||||
|
||||
// Verify that the event target is the checkbox and not the item
|
||||
const eventByKeyboard = ionFocus.events[0];
|
||||
expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
|
||||
|
||||
// Reset focus
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
const checkboxBoundingBox = (await checkbox.boundingBox())!;
|
||||
await page.mouse.click(0, checkboxBoundingBox.height + 1);
|
||||
|
||||
// Test focus with click
|
||||
const item = page.locator('ion-item');
|
||||
await item.click();
|
||||
|
||||
expect(ionFocus).toHaveReceivedEventTimes(2);
|
||||
|
||||
// Verify that the event target is the checkbox and not the item
|
||||
const eventByClick = ionFocus.events[0];
|
||||
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
|
||||
});
|
||||
|
||||
test('should not fire when programmatically setting a value', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionFocus = await page.spyOnEvent('ionFocus');
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
|
||||
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
|
||||
expect(ionFocus).not.toHaveReceivedEvent();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('checkbox: ionBlur'), () => {
|
||||
test('should fire ionBlur when checkbox is blurred', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionBlur = await page.spyOnEvent('ionBlur');
|
||||
|
||||
// Test blur with keyboard navigation
|
||||
// Focus the checkbox
|
||||
await pageUtils.pressKeys('Tab');
|
||||
// Blur the checkbox
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
expect(ionBlur).toHaveReceivedEventTimes(1);
|
||||
|
||||
// Test blur with click
|
||||
const checkbox = page.locator('ion-checkbox');
|
||||
// Focus the checkbox
|
||||
await checkbox.click();
|
||||
// Blur the checkbox by clicking outside of it
|
||||
const checkboxBoundingBox = (await checkbox.boundingBox())!;
|
||||
await page.mouse.click(0, checkboxBoundingBox.height + 1);
|
||||
|
||||
expect(ionBlur).toHaveReceivedEventTimes(2);
|
||||
});
|
||||
|
||||
test('should fire ionBlur after interacting with checkbox in item', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-item>
|
||||
<ion-checkbox aria-label="checkbox" value="my-checkbox"></ion-checkbox>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionBlur = await page.spyOnEvent('ionBlur');
|
||||
|
||||
// Test blur with keyboard navigation
|
||||
// Focus the checkbox
|
||||
await pageUtils.pressKeys('Tab');
|
||||
// Blur the checkbox
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
expect(ionBlur).toHaveReceivedEventTimes(1);
|
||||
|
||||
// Verify that the event target is the checkbox and not the item
|
||||
const event = ionBlur.events[0];
|
||||
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
|
||||
|
||||
// Test blur with click
|
||||
const item = page.locator('ion-item');
|
||||
await item.click();
|
||||
// Blur the checkbox by clicking outside of it
|
||||
const itemBoundingBox = (await item.boundingBox())!;
|
||||
await page.mouse.click(0, itemBoundingBox.height + 1);
|
||||
|
||||
expect(ionBlur).toHaveReceivedEventTimes(2);
|
||||
|
||||
// Verify that the event target is the checkbox and not the item
|
||||
const eventByClick = ionBlur.events[0];
|
||||
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -50,6 +50,20 @@
|
||||
<ion-checkbox checked style="width: 200px">Specified width</ion-checkbox><br />
|
||||
<ion-checkbox checked style="width: 100%">Full-width</ion-checkbox><br />
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
document.addEventListener('ionBlur', (ev) => {
|
||||
console.log('ionBlur', ev);
|
||||
});
|
||||
|
||||
document.addEventListener('ionChange', (ev) => {
|
||||
console.log('ionChange', ev);
|
||||
});
|
||||
|
||||
document.addEventListener('ionFocus', (ev) => {
|
||||
console.log('ionFocus', ev);
|
||||
});
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -246,6 +246,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
document.addEventListener('ionBlur', (ev) => {
|
||||
console.log('ionBlur', ev);
|
||||
});
|
||||
|
||||
document.addEventListener('ionChange', (ev) => {
|
||||
console.log('ionChange', ev);
|
||||
});
|
||||
|
||||
document.addEventListener('ionFocus', (ev) => {
|
||||
console.log('ionFocus', ev);
|
||||
});
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
184
core/src/components/checkbox/test/validation/index.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Checkbox - Validation</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-row-gap: 30px;
|
||||
grid-column-gap: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: var(--ion-color-step-600);
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.validation-info {
|
||||
margin: 20px;
|
||||
padding: 10px;
|
||||
background: var(--ion-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Checkbox - Validation Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="validation-info">
|
||||
<h2>Screen Reader Testing Instructions:</h2>
|
||||
<ol>
|
||||
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
|
||||
<li>Tab through the form fields</li>
|
||||
<li>When you tab away from an empty required field, the error should be announced immediately</li>
|
||||
<li>The error text should be announced BEFORE the next field is announced</li>
|
||||
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h2>Required Field</h2>
|
||||
<ion-checkbox
|
||||
id="terms-checkbox"
|
||||
helper-text="You must agree to continue"
|
||||
error-text="Please accept the terms and conditions"
|
||||
required
|
||||
>I agree to the terms and conditions</ion-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Optional Field (No Validation)</h2>
|
||||
<ion-checkbox id="optional-checkbox" helper-text="You can skip this field">Optional Checkbox</ion-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ion-padding">
|
||||
<ion-button id="submit-btn" expand="block" disabled>Submit Form</ion-button>
|
||||
<ion-button id="reset-btn" expand="block" fill="outline">Reset Form</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
// Simple validation logic
|
||||
const checkboxes = document.querySelectorAll('ion-checkbox');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const resetBtn = document.getElementById('reset-btn');
|
||||
|
||||
// Track which fields have been touched
|
||||
const touchedFields = new Set();
|
||||
|
||||
// Validation functions
|
||||
const validators = {
|
||||
'terms-checkbox': (checked) => {
|
||||
return checked === true;
|
||||
},
|
||||
'optional-checkbox': () => true, // Always valid
|
||||
};
|
||||
|
||||
function validateField(checkbox) {
|
||||
const checkboxId = checkbox.id;
|
||||
const checked = checkbox.checked;
|
||||
const isValid = validators[checkboxId] ? validators[checkboxId](checked) : true;
|
||||
|
||||
// Only show validation state if field has been touched
|
||||
if (touchedFields.has(checkboxId)) {
|
||||
if (isValid) {
|
||||
checkbox.classList.remove('ion-invalid');
|
||||
checkbox.classList.add('ion-valid');
|
||||
} else {
|
||||
checkbox.classList.remove('ion-valid');
|
||||
checkbox.classList.add('ion-invalid');
|
||||
}
|
||||
checkbox.classList.add('ion-touched');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
let allValid = true;
|
||||
checkboxes.forEach((checkbox) => {
|
||||
if (checkbox.id !== 'optional-checkbox') {
|
||||
const isValid = validateField(checkbox);
|
||||
if (!isValid) {
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
submitBtn.disabled = !allValid;
|
||||
return allValid;
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
checkboxes.forEach((checkbox) => {
|
||||
// Mark as touched on blur
|
||||
checkbox.addEventListener('ionBlur', (e) => {
|
||||
console.log('Blur event on:', checkbox.id);
|
||||
touchedFields.add(checkbox.id);
|
||||
validateField(checkbox);
|
||||
validateForm();
|
||||
|
||||
const isInvalid = checkbox.classList.contains('ion-invalid');
|
||||
if (isInvalid) {
|
||||
console.log('Field marked invalid:', checkbox.innerText, checkbox.errorText);
|
||||
}
|
||||
});
|
||||
|
||||
// Validate on change
|
||||
checkbox.addEventListener('ionChange', (e) => {
|
||||
console.log('Change event on:', checkbox.id);
|
||||
if (touchedFields.has(checkbox.id)) {
|
||||
validateField(checkbox);
|
||||
validateForm();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reset button
|
||||
resetBtn.addEventListener('click', () => {
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
checkbox.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
|
||||
});
|
||||
touchedFields.clear();
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Submit button
|
||||
submitBtn.addEventListener('click', () => {
|
||||
if (validateForm()) {
|
||||
alert('Form submitted successfully!');
|
||||
}
|
||||
});
|
||||
|
||||
// Initial setup
|
||||
validateForm();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -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);
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
@@ -21,6 +21,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
|
||||
);
|
||||
|
||||
const datetimeButton = page.locator('ion-datetime-button');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale`));
|
||||
});
|
||||
@@ -40,6 +41,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
|
||||
);
|
||||
|
||||
const datetimeButton = page.locator('ion-datetime-button');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale-wrap`));
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -24,6 +24,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await dateButton.click();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
// Wait for datetime to be ready before taking screenshot
|
||||
await page.locator('ion-datetime.datetime-ready').waitFor();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-modal`));
|
||||
});
|
||||
|
||||
@@ -44,6 +47,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await dateButton.click();
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
// Wait for datetime to be ready before taking screenshot
|
||||
await page.locator('ion-datetime.datetime-ready').waitFor();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-popover`));
|
||||
});
|
||||
});
|
||||
@@ -176,5 +182,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,15 +22,15 @@ export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' |
|
||||
|
||||
export type TitleSelectedDatesFormatter = (selectedDates: string[]) => string;
|
||||
|
||||
export type DatetimeHighlightStyle =
|
||||
| {
|
||||
textColor: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
| {
|
||||
textColor?: string;
|
||||
backgroundColor: string;
|
||||
};
|
||||
/**
|
||||
* DatetimeHighlightStyle must include textColor, backgroundColor, or border.
|
||||
* It cannot be an empty object.
|
||||
*/
|
||||
export type DatetimeHighlightStyle = {
|
||||
textColor?: string;
|
||||
backgroundColor?: string;
|
||||
border?: string;
|
||||
} & ({ textColor: string } | { backgroundColor: string } | { border: string });
|
||||
|
||||
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTa
|
||||
import { startFocusVisible } from '@utils/focus-visible';
|
||||
import { getElementRoot, raf, renderHiddenInput } from '@utils/helpers';
|
||||
import { printIonError, printIonWarning } from '@utils/logging';
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
import { isRTL } from '@utils/rtl';
|
||||
import { createColorClasses } from '@utils/theme';
|
||||
import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward } from 'ionicons/icons';
|
||||
@@ -107,8 +108,8 @@ export class Datetime implements ComponentInterface {
|
||||
private inputId = `ion-dt-${datetimeIds++}`;
|
||||
private calendarBodyRef?: HTMLElement;
|
||||
private popoverRef?: HTMLIonPopoverElement;
|
||||
private intersectionTrackerRef?: HTMLElement;
|
||||
private clearFocusVisible?: () => void;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private parsedMinuteValues?: number[];
|
||||
private parsedHourValues?: number[];
|
||||
private parsedMonthValues?: number[];
|
||||
@@ -117,6 +118,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
private destroyCalendarListener?: () => void;
|
||||
private destroyKeyboardMO?: () => void;
|
||||
private destroyOverlayListeners?: () => void;
|
||||
|
||||
// TODO(FW-2832): types (DatetimeParts causes some errors that need untangling)
|
||||
private minParts?: any;
|
||||
@@ -1076,6 +1078,14 @@ export class Datetime implements ComponentInterface {
|
||||
this.clearFocusVisible();
|
||||
this.clearFocusVisible = undefined;
|
||||
}
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = undefined;
|
||||
}
|
||||
if (this.destroyOverlayListeners) {
|
||||
this.destroyOverlayListeners();
|
||||
this.destroyOverlayListeners = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1100,76 +1110,101 @@ export class Datetime implements ComponentInterface {
|
||||
this.initializeKeyboardListeners();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
const { el, intersectionTrackerRef } = this;
|
||||
/**
|
||||
* Sets up visibility detection for the datetime component.
|
||||
*
|
||||
* Uses multiple strategies to reliably detect when the datetime becomes
|
||||
* visible, which is necessary for proper initialization of scrollable areas:
|
||||
* 1. ResizeObserver - detects dimension changes
|
||||
* 2. Overlay event listeners - for datetime inside modals/popovers
|
||||
* 3. Polling fallback - for browsers where observers are unreliable (WebKit)
|
||||
*/
|
||||
private initializeVisibilityObserver() {
|
||||
const { el } = this;
|
||||
|
||||
/**
|
||||
* If a scrollable element is hidden using `display: none`,
|
||||
* it will not have a scroll height meaning we cannot scroll elements
|
||||
* into view. As a result, we will need to wait for the datetime to become
|
||||
* visible if used inside of a modal or a popover otherwise the scrollable
|
||||
* areas will not have the correct values snapped into place.
|
||||
*/
|
||||
const visibleCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
if (!ev.isIntersecting) {
|
||||
const markReady = () => {
|
||||
if (el.classList.contains('datetime-ready')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializeListeners();
|
||||
|
||||
/**
|
||||
* TODO FW-2793: Datetime needs a frame to ensure that it
|
||||
* can properly scroll contents into view. As a result
|
||||
* we hide the scrollable content until after that frame
|
||||
* so users do not see the content quickly shifting. The downside
|
||||
* is that the content will pop into view a frame after. Maybe there
|
||||
* is a better way to handle this?
|
||||
*/
|
||||
writeTask(() => {
|
||||
this.el.classList.add('datetime-ready');
|
||||
el.classList.add('datetime-ready');
|
||||
});
|
||||
};
|
||||
const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el });
|
||||
|
||||
/**
|
||||
* Use raf to avoid a race condition between the component loading and
|
||||
* its display animation starting (such as when shown in a modal). This
|
||||
* could cause the datetime to start at a visibility of 0, erroneously
|
||||
* triggering the `hiddenIO` observer below.
|
||||
*/
|
||||
raf(() => visibleIO?.observe(intersectionTrackerRef!));
|
||||
|
||||
/**
|
||||
* We need to clean up listeners when the datetime is hidden
|
||||
* in a popover/modal so that we can properly scroll containers
|
||||
* back into view if they are re-presented. When the datetime is hidden
|
||||
* the scroll areas have scroll widths/heights of 0px, so any snapping
|
||||
* we did originally has been lost.
|
||||
*/
|
||||
const hiddenCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
if (ev.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markHidden = () => {
|
||||
this.destroyInteractionListeners();
|
||||
|
||||
/**
|
||||
* When datetime is hidden, we need to make sure that
|
||||
* the month/year picker is closed. Otherwise,
|
||||
* it will be open when the datetime re-appears
|
||||
* and the scroll area of the calendar grid will be 0.
|
||||
* As a result, the wrong month will be shown.
|
||||
*/
|
||||
this.showMonthAndYear = false;
|
||||
|
||||
writeTask(() => {
|
||||
this.el.classList.remove('datetime-ready');
|
||||
el.classList.remove('datetime-ready');
|
||||
});
|
||||
startVisibilityPolling();
|
||||
};
|
||||
const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0, root: el });
|
||||
raf(() => hiddenIO?.observe(intersectionTrackerRef!));
|
||||
|
||||
/**
|
||||
* FW-6931: Poll for visibility as a fallback for browsers where
|
||||
* ResizeObserver doesn't fire reliably (e.g., WebKit).
|
||||
*/
|
||||
const startVisibilityPolling = () => {
|
||||
let pollCount = 0;
|
||||
const poll = () => {
|
||||
if (el.classList.contains('datetime-ready') || pollCount++ >= 60) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
if (width > 0 && height > 0) {
|
||||
markReady();
|
||||
} else {
|
||||
raf(poll);
|
||||
}
|
||||
};
|
||||
raf(poll);
|
||||
};
|
||||
|
||||
/**
|
||||
* FW-6931: Listen for overlay present/dismiss events when datetime
|
||||
* is inside a modal or popover.
|
||||
*/
|
||||
const parentOverlay = el.closest('ion-modal, ion-popover') as HTMLIonModalElement | HTMLIonPopoverElement | null;
|
||||
if (parentOverlay) {
|
||||
const handlePresent = () => markReady();
|
||||
const handleDismiss = () => markHidden();
|
||||
|
||||
parentOverlay.addEventListener('didPresent', handlePresent);
|
||||
parentOverlay.addEventListener('didDismiss', handleDismiss);
|
||||
|
||||
this.destroyOverlayListeners = () => {
|
||||
parentOverlay.removeEventListener('didPresent', handlePresent);
|
||||
parentOverlay.removeEventListener('didDismiss', handleDismiss);
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
const isVisible = width > 0 && height > 0;
|
||||
const isReady = el.classList.contains('datetime-ready');
|
||||
|
||||
if (isVisible && !isReady) {
|
||||
markReady();
|
||||
} else if (!isVisible && isReady) {
|
||||
markHidden();
|
||||
}
|
||||
});
|
||||
|
||||
// Use raf to avoid race condition with modal/popover animations
|
||||
raf(() => this.resizeObserver?.observe(el));
|
||||
startVisibilityPolling();
|
||||
} else {
|
||||
// Test environment fallback - mark ready immediately
|
||||
writeTask(() => {
|
||||
el.classList.add('datetime-ready');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.initializeVisibilityObserver();
|
||||
|
||||
/**
|
||||
* Datetime uses Ionic components that emit
|
||||
@@ -1598,7 +1633,7 @@ export class Datetime implements ComponentInterface {
|
||||
forcePresentation === 'time-date'
|
||||
? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)]
|
||||
: [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)];
|
||||
return <ion-picker>{renderArray}</ion-picker>;
|
||||
return <ion-picker class={FOCUS_TRAP_DISABLE_CLASS}>{renderArray}</ion-picker>;
|
||||
}
|
||||
|
||||
private renderDatePickerColumns(forcePresentation: string) {
|
||||
@@ -2335,6 +2370,7 @@ export class Datetime implements ComponentInterface {
|
||||
`${dateStyle ? dateStyle.backgroundColor : ''}`,
|
||||
'important'
|
||||
);
|
||||
el.style.setProperty('border', `${dateStyle ? dateStyle.border : ''}`, 'important');
|
||||
}
|
||||
}}
|
||||
tabindex="-1"
|
||||
@@ -2653,20 +2689,6 @@ export class Datetime implements ComponentInterface {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
WebKit has a quirk where IntersectionObserver callbacks are delayed until after
|
||||
an accelerated animation finishes if the "root" specified in the config is the
|
||||
browser viewport (the default behavior if "root" is not specified). This means
|
||||
that when presenting a datetime in a modal on iOS the calendar body appears
|
||||
blank until the modal animation finishes.
|
||||
|
||||
We can work around this by observing .intersection-tracker and using the host
|
||||
(ion-datetime) as the "root". This allows the IO callback to fire the moment
|
||||
the datetime is visible. The .intersection-tracker element should not have
|
||||
dimensions or additional styles, and it should not be positioned absolutely
|
||||
otherwise the IO callback may fire at unexpected times.
|
||||
*/}
|
||||
<div class="intersection-tracker" ref={(el) => (this.intersectionTrackerRef = el)}></div>
|
||||
{this.renderDatetime(mode)}
|
||||
</Host>
|
||||
);
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -394,6 +394,39 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that datetime becomes ready via ResizeObserver.
|
||||
* This tests that the datetime properly initializes when it has
|
||||
* dimensions, using ResizeObserver to detect visibility.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: visibility detection'), () => {
|
||||
test('should become ready when rendered with dimensions', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-05-03"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
// Wait for the datetime to become ready via ResizeObserver
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
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
|
||||
|
||||
@@ -5,6 +5,8 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('datetime: custom'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/datetime/test/custom`, config);
|
||||
|
||||
await page.locator('.datetime-ready').last().waitFor();
|
||||
});
|
||||
|
||||
test('should allow styling wheel style datetimes', async ({ page }) => {
|
||||
@@ -30,6 +32,13 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test('should allow styling calendar days in grid style datetimes', async ({ page }) => {
|
||||
const datetime = page.locator('#custom-calendar-days');
|
||||
|
||||
// Wait for calendar days to be rendered
|
||||
await page.waitForFunction(() => {
|
||||
const datetime = document.querySelector('#custom-calendar-days');
|
||||
const calendarDays = datetime?.shadowRoot?.querySelectorAll('.calendar-day');
|
||||
return calendarDays && calendarDays.length > 0;
|
||||
});
|
||||
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-calendar-days`));
|
||||
});
|
||||
});
|
||||
@@ -43,6 +52,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('datetime: custom focus'), () => {
|
||||
test('should focus the selected day and then the day after', async ({ page }) => {
|
||||
await page.goto(`/src/components/datetime/test/custom`, config);
|
||||
await page.locator('.datetime-ready').last().waitFor();
|
||||
|
||||
const datetime = page.locator('#custom-calendar-days');
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
const customDatetime = document.querySelector('#custom-calendar-days');
|
||||
|
||||
// Mock the current day to always have the same screenshots
|
||||
const mockToday = '2023-06-10T16:22';
|
||||
const mockToday = '2023-06-10T16:22:00.000Z';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
|
||||
@@ -22,11 +22,23 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(monthYearToggle).toContainText('January 2022');
|
||||
|
||||
// Click to open the picker
|
||||
await monthYearToggle.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// February
|
||||
await monthColumnItems.nth(1).click();
|
||||
// Wait for the picker to be open
|
||||
await page.locator('.month-year-picker-open').waitFor();
|
||||
|
||||
// Wait a bit for the picker to fully load
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
// Click on February
|
||||
await monthColumnItems.filter({ hasText: 'February' }).click();
|
||||
|
||||
// Wait for changes
|
||||
await ionChange.next();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(monthYearToggle).toContainText('February 2022');
|
||||
@@ -38,13 +50,23 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
// Click to open the picker
|
||||
await monthYearToggle.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// February
|
||||
await monthColumnItems.nth(1).click();
|
||||
// Wait for the picker to be open
|
||||
await page.locator('.month-year-picker-open').waitFor();
|
||||
|
||||
// Wait a bit for the picker to fully load
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Click on February
|
||||
await monthColumnItems.filter({ hasText: 'February' }).click();
|
||||
|
||||
// Wait for changes
|
||||
await ionChange.next();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventTimes(1);
|
||||
await expect(datetime).toHaveJSProperty('value', '2022-02-28');
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
await page.goto('/src/components/datetime/test/first-day-of-week', config);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-day-of-week`));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
`,
|
||||
config
|
||||
);
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
});
|
||||
|
||||
test('should render highlights correctly when using an array', async ({ page }) => {
|
||||
@@ -21,16 +22,19 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
date: '2023-01-01', // ensure selected date style overrides highlight
|
||||
textColor: '#800080',
|
||||
backgroundColor: '#ffc0cb',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
{
|
||||
date: '2023-01-02',
|
||||
textColor: '#b22222',
|
||||
backgroundColor: '#fa8072',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
{
|
||||
date: '2023-01-03',
|
||||
textColor: '#0000ff',
|
||||
backgroundColor: '#add8e6',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -52,6 +56,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#b22222',
|
||||
backgroundColor: '#fa8072',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +64,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#800080',
|
||||
backgroundColor: '#ffc0cb',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,6 +72,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#0000ff',
|
||||
backgroundColor: '#add8e6',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,7 +84,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-callback`));
|
||||
});
|
||||
|
||||
test('should render highlights correctly when only using one color or the other', async ({ page }) => {
|
||||
test('should render highlights correctly when only using only one color property', async ({ page }) => {
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
|
||||
@@ -90,6 +97,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
date: '2023-01-03',
|
||||
textColor: '#0000ff',
|
||||
},
|
||||
{
|
||||
date: '2023-01-04',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 40 KiB |