Compare commits

..

4 Commits

Author SHA1 Message Date
Brandy Smith
360fce73d4 chore: grab theme changes from next 2025-08-13 10:46:59 -04:00
Maria Hutt
8c21980a70 feat(theme): more changes 2025-08-12 15:50:23 -04:00
Maria Hutt
3c6bb9fb00 feat(core): component level theming 2025-08-12 15:50:11 -04:00
Maria Hutt
a130a4a63a feat(core): separate mode and theme 2025-08-12 15:48:22 -04:00
287 changed files with 2833 additions and 7365 deletions

View File

@@ -56,6 +56,14 @@ 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: >
@@ -137,6 +145,65 @@ 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"

View File

@@ -3,23 +3,23 @@ description: 'Build Ionic Angular Server'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -3,31 +3,31 @@ description: 'Build Ionic Angular'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -8,20 +8,20 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -8,22 +8,22 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24.x
- name: 🕸️ Install Dependencies
node-version: 22.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

View File

@@ -3,9 +3,9 @@ description: 'Build Ionic React Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -3,35 +3,35 @@ description: 'Build Ionic React'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -3,9 +3,9 @@ description: 'Builds Ionic Vue Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -3,31 +3,31 @@ description: 'Build Ionic Vue'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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

View File

@@ -10,10 +10,10 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v5
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
- name: 🔎 Extract Archive
- name: Extract Archive
run: unzip -q -o ${{ inputs.path }}/${{ inputs.filename }}
shell: bash

View File

@@ -8,51 +8,48 @@ inputs:
tag:
description: 'The tag to publish to on NPM.'
preid:
description: "Prerelease identifier such as 'alpha', 'beta', 'rc', or 'next'. Leave blank to skip prerelease tagging."
description: 'The prerelease identifier used when doing a prerelease.'
working-directory:
description: 'The directory of the package.'
folder:
default: './'
description: 'A folder containing a package.json file.'
node-version:
description: 'Node.js version to use when publishing.'
required: false
default: '24.x'
token:
description: 'The NPM authentication token required to publish.'
runs:
using: 'composite'
steps:
- name: 🟢 Configure Node for Publish
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: 'https://registry.npmjs.org'
node-version: 22.x
# 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: 🏷️ 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
- name: Update Version
run: npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version --preid=${{ inputs.preid }}
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: 🚀 Publish to NPM
- 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
run: npm publish ${{ inputs.folder }} --tag ${{ inputs.tag }} --provenance
shell: bash
working-directory: ${{ inputs.working-directory }}

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.x
- uses: ./.github/workflows/actions/download-archive
with:
name: ionic-core
@@ -24,23 +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: 📦 Install Playwright Browsers
- name: Install Playwright Browsers
run: npx playwright install
shell: bash
working-directory: ./packages/angular/test/build/${{ inputs.app }}
- name: 🔄 Sync Built Changes
- 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 }}

View File

@@ -3,16 +3,16 @@ description: 'Test Core Clean Build'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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"

View File

@@ -3,21 +3,21 @@ description: 'Test Core Lint'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
- name: 🕸️ Install Dependencies
node-version: 22.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

View File

@@ -13,19 +13,19 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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@v5
uses: actions/upload-artifact@v4
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

View File

@@ -6,14 +6,14 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
- name: 🕸️ Install Dependencies
node-version: 22.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

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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 }}

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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 }}

View File

@@ -6,9 +6,9 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.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 }}

View File

@@ -7,13 +7,13 @@ on:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@v4
with:
node-version: 24.x
- uses: actions/download-artifact@v6
node-version: 22.x
- uses: actions/download-artifact@v5
with:
path: ./artifacts
- name: 🔎 Extract Archives
- 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.

View File

@@ -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@v5
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.output }}

View File

@@ -13,6 +13,6 @@ jobs:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
with:
assignees: brandyscarney, thetaPC, ShaneK
assignees: brandyscarney, ShaneK
numOfAssignee: 1
allowSelfAssign: false

View File

@@ -22,7 +22,7 @@ jobs:
build-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-core
with:
ionicons-version: ${{ inputs.ionicons_npm_release_tag }}
@@ -31,21 +31,21 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-clean-build
test-core-lint:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-lint
test-core-spec:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-spec
test-core-screenshot:
@@ -62,7 +62,7 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -90,14 +90,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-vue
build-vue-router:
needs: [build-vue]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-vue-router
test-vue-e2e:
@@ -108,7 +108,7 @@ jobs:
needs: [build-vue, build-vue-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-vue-e2e
with:
app: ${{ matrix.apps }}
@@ -126,14 +126,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-angular
build-angular-server:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-angular-server
test-angular-e2e:
@@ -144,7 +144,7 @@ jobs:
needs: [build-angular, build-angular-server]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-angular-e2e
with:
app: ${{ matrix.apps }}
@@ -162,14 +162,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-react
build-react-router:
needs: [build-react]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-react-router
test-react-router-e2e:
@@ -180,7 +180,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-react-router-e2e
with:
app: ${{ matrix.apps }}
@@ -202,7 +202,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-react-e2e
with:
app: ${{ matrix.apps }}

View File

@@ -14,8 +14,8 @@ jobs:
permissions:
security-events: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: github/codeql-action/init@v4
- uses: actions/checkout@v5
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/analyze@v3

View File

@@ -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@v6
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -9,7 +9,7 @@ jobs:
outputs:
dev-hash: ${{ steps.create-dev-hash.outputs.DEV_HASH }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
# 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,6 +30,8 @@ jobs:
with:
tag: dev
version: ${{ needs.create-dev-hash.outputs.dev-hash }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
get-build:
name: Get your dev build!

View File

@@ -13,7 +13,7 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v6
- uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@@ -12,7 +12,7 @@ jobs:
outputs:
nightly-hash: ${{ steps.create-nightly-hash.outputs.NIGHTLY_HASH }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
# 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
@@ -35,3 +35,5 @@ jobs:
with:
tag: nightly
version: ${{ needs.create-nightly-hash.outputs.nightly-hash }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -14,16 +14,15 @@ on:
preid:
description: 'The prerelease identifier used when doing a prerelease.'
type: string
permissions:
contents: read
id-token: write
secrets:
NPM_TOKEN:
required: true
jobs:
release-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/publish-npm
with:
scope: '@ionic/core'
@@ -31,6 +30,7 @@ jobs:
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,7 +48,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/docs built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -62,12 +62,13 @@ jobs:
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -82,6 +83,7 @@ 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:
@@ -93,7 +95,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -107,6 +109,7 @@ jobs:
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:
@@ -118,7 +121,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -132,6 +135,7 @@ jobs:
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:
@@ -143,7 +147,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -158,12 +162,13 @@ 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -183,12 +188,13 @@ jobs:
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -208,3 +214,4 @@ jobs:
version: ${{ inputs.version }}
preid: ${{ inputs.preid }}
working-directory: 'packages/vue-router'
token: ${{ secrets.NPM_TOKEN }}

View File

@@ -25,7 +25,6 @@ on:
preid:
type: choice
description: Which prerelease identifier should be used? This is only needed when version is "prepatch", "preminor", "premajor", or "prerelease".
default: ''
options:
- ''
- alpha
@@ -36,19 +35,20 @@ on:
jobs:
release-ionic:
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
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
with:
token: ${{ secrets.IONITRON_TOKEN }}
fetch-depth: 0
@@ -76,7 +76,7 @@ jobs:
needs: [finalize-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
# Pull the latest version of the reference
# branch instead of the revision that triggered
# the workflow otherwise we won't get the commit

View File

@@ -26,7 +26,7 @@ jobs:
build-core-with-stencil-nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-core-stencil-prerelease
with:
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
@@ -35,21 +35,21 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-clean-build
test-core-lint:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-lint
test-core-spec:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-spec
with:
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
@@ -72,7 +72,7 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -100,14 +100,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-vue
build-vue-router:
needs: [build-vue]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-vue-router
test-vue-e2e:
@@ -118,7 +118,7 @@ jobs:
needs: [build-vue, build-vue-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-vue-e2e
with:
app: ${{ matrix.apps }}
@@ -136,14 +136,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-angular
build-angular-server:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-angular-server
test-angular-e2e:
@@ -154,7 +154,7 @@ jobs:
needs: [build-angular, build-angular-server]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-angular-e2e
with:
app: ${{ matrix.apps }}
@@ -172,14 +172,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-react
build-react-router:
needs: [build-react]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-react-router
test-react-router-e2e:
@@ -190,7 +190,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-react-router-e2e
with:
app: ${{ matrix.apps }}
@@ -212,7 +212,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-react-e2e
with:
app: ${{ matrix.apps }}
@@ -225,35 +225,3 @@ jobs:
- name: Check build matrix status
if: ${{ needs.test-react-e2e.result != 'success' }}
run: exit 1
send-success-messages:
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
runs-on: ubuntu-latest
if: ${{ !cancelled() && contains(needs.*.result, 'success') }}
steps:
- name: Notify success on Discord
run: |
curl -H "Content-Type:application/json" \
-d '{"embeds": [{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "color": 65280, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
- name: Notify success on Slack
run: |
curl -H "Content-Type:application/json" \
-d '{"title": "✅ Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
${{secrets.SLACK_NOTIFY_SUCCESS_WEBHOOK}}
send-failure-messages:
needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e]
runs-on: ubuntu-latest
if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
steps:
- name: Notify failure on Discord
run: |
curl -H "Content-Type:application/json" \
-d '{"content": "Alerting <@&1347593178580254761>!", "embeds": [{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "color": 16711680, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \
${{secrets.DISCORD_NOTIFY_WEBHOOK}}
- name: Notify failure on Slack
run: |
curl -H "Content-Type:application/json" \
-d '{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \
${{secrets.SLACK_NOTIFY_FAILURE_WEBHOOK}}

View File

@@ -26,7 +26,7 @@ jobs:
build-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/build-core
test-core-screenshot:
@@ -47,7 +47,7 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
needs: [test-core-screenshot]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@v5
# Normally, we could just push with the
# default GITHUB_TOKEN, but that will
# not cause the build workflow

View File

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

View File

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

View File

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

View File

@@ -1205,6 +1205,37 @@ ion-modal,part,backdrop
ion-modal,part,content
ion-modal,part,handle
ion-my-chip,shadow
ion-my-chip,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-my-chip,prop,disabled,boolean,false,false,false
ion-my-chip,prop,hue,"bold" | "subtle" | undefined,'subtle',false,false
ion-my-chip,prop,mode,"ios" | "md",undefined,false,false
ion-my-chip,prop,outline,boolean,false,false,false
ion-my-chip,prop,shape,"rectangular" | "round" | "soft" | undefined,'soft',false,false
ion-my-chip,prop,size,"large" | "small" | undefined,'small',false,false
ion-my-chip,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-my-chip,css-prop,--ion-chip-border-width
ion-my-chip,css-prop,--ion-chip-focus-ring-color
ion-my-chip,css-prop,--ion-chip-focus-ring-width
ion-my-chip,css-prop,--ion-chip-font-weight
ion-my-chip,css-prop,--ion-chip-gap
ion-my-chip,css-prop,--ion-chip-hue-bold-bg
ion-my-chip,css-prop,--ion-chip-hue-bold-border-color
ion-my-chip,css-prop,--ion-chip-hue-bold-color
ion-my-chip,css-prop,--ion-chip-hue-subtle-bg
ion-my-chip,css-prop,--ion-chip-hue-subtle-border-color
ion-my-chip,css-prop,--ion-chip-hue-subtle-color
ion-my-chip,css-prop,--ion-chip-line-height
ion-my-chip,css-prop,--ion-chip-padding-horizontal
ion-my-chip,css-prop,--ion-chip-padding-vertical
ion-my-chip,css-prop,--ion-chip-shape-rectangular-border-radius
ion-my-chip,css-prop,--ion-chip-shape-round-border-radius
ion-my-chip,css-prop,--ion-chip-shape-soft-border-radius
ion-my-chip,css-prop,--ion-chip-size-large-font-size
ion-my-chip,css-prop,--ion-chip-size-large-min-height
ion-my-chip,css-prop,--ion-chip-size-small-font-size
ion-my-chip,css-prop,--ion-chip-size-small-min-height
ion-nav,shadow
ion-nav,prop,animated,boolean,true,false,false
ion-nav,prop,animation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
@@ -1231,6 +1262,7 @@ ion-nav,event,ionNavWillChange,void,false
ion-nav-link,none
ion-nav-link,prop,component,Function | HTMLElement | ViewController | null | string | undefined,undefined,false,false
ion-nav-link,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false
ion-nav-link,prop,mode,"ios" | "md",undefined,false,false
ion-nav-link,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-nav-link,prop,routerDirection,"back" | "forward" | "root",'forward',false,false

143
core/package-lock.json generated
View File

@@ -1,20 +1,20 @@
{
"name": "@ionic/core",
"version": "8.7.9",
"version": "8.7.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.7.9",
"version": "8.7.2",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"tslib": "^2.1.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.0",
"@axe-core/playwright": "^4.10.2",
"@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
@@ -22,7 +22,7 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.56.1",
"@playwright/test": "^1.54.2",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.10.0",
@@ -57,12 +57,12 @@
"dev": true
},
"node_modules/@axe-core/playwright": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz",
"integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==",
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz",
"integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==",
"dev": true,
"dependencies": {
"axe-core": "~4.11.0"
"axe-core": "~4.10.3"
},
"peerDependencies": {
"playwright-core": ">= 1.0.0"
@@ -663,9 +663,9 @@
"dev": true
},
"node_modules/@capacitor/core": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz",
"integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
@@ -681,18 +681,18 @@
}
},
"node_modules/@capacitor/keyboard": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz",
"integrity": "sha512-BIBKjmky5rOYNhvYhNeDi0MMvjwYZ6YF9JoCYcGKvKY+XLJKtezsEL78XfOlgWZBkbfR8uq3tzktY6PqgoYLKA==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz",
"integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==",
"dev": true,
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.3.tgz",
"integrity": "sha512-JyRpVnKwHij9hgPWolF6PK+HT3e2HSPjN11/h2OmKxq8GAdPGARFLv+97eZl0pvuvm0Kka/LpiLb5whXISBg7Q==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz",
"integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==",
"dev": true,
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
@@ -1715,12 +1715,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"dev": true,
"dependencies": {
"playwright": "1.56.1"
"playwright": "1.54.2"
},
"bin": {
"playwright": "cli.js"
@@ -1914,9 +1914,10 @@
}
},
"node_modules/@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
},
@@ -3033,9 +3034,9 @@
}
},
"node_modules/axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
"integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
"dev": true,
"engines": {
"node": ">=4"
@@ -3473,9 +3474,9 @@
]
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -8592,12 +8593,12 @@
}
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"dev": true,
"dependencies": {
"playwright-core": "1.56.1"
"playwright-core": "1.54.2"
},
"bin": {
"playwright": "cli.js"
@@ -8610,9 +8611,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -10652,12 +10653,12 @@
},
"dependencies": {
"@axe-core/playwright": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz",
"integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==",
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz",
"integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==",
"dev": true,
"requires": {
"axe-core": "~4.11.0"
"axe-core": "~4.10.3"
}
},
"@babel/code-frame": {
@@ -11101,9 +11102,9 @@
"dev": true
},
"@capacitor/core": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz",
"integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
@@ -11117,16 +11118,16 @@
"requires": {}
},
"@capacitor/keyboard": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz",
"integrity": "sha512-BIBKjmky5rOYNhvYhNeDi0MMvjwYZ6YF9JoCYcGKvKY+XLJKtezsEL78XfOlgWZBkbfR8uq3tzktY6PqgoYLKA==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz",
"integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==",
"dev": true,
"requires": {}
},
"@capacitor/status-bar": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.3.tgz",
"integrity": "sha512-JyRpVnKwHij9hgPWolF6PK+HT3e2HSPjN11/h2OmKxq8GAdPGARFLv+97eZl0pvuvm0Kka/LpiLb5whXISBg7Q==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz",
"integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==",
"dev": true,
"requires": {}
},
@@ -11862,12 +11863,12 @@
}
},
"@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"dev": true,
"requires": {
"playwright": "1.56.1"
"playwright": "1.54.2"
}
},
"@rollup/plugin-node-resolve": {
@@ -11983,9 +11984,9 @@
"requires": {}
},
"@stencil/core": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -12770,9 +12771,9 @@
}
},
"axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
"integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
"dev": true
},
"babel-jest": {
@@ -13075,9 +13076,9 @@
"dev": true
},
"chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"dev": true
},
"chalk-template": {
@@ -16811,19 +16812,19 @@
}
},
"playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.56.1"
"playwright-core": "1.54.2"
}
},
"playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"dev": true
},
"postcss": {

View File

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

View File

@@ -2072,6 +2072,45 @@ export namespace Components {
*/
"trigger": string | undefined;
}
interface IonMyChip {
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* If `true`, the user cannot interact with the chip.
* @default false
*/
"disabled": boolean;
/**
* Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Only applies to the `ionic` theme.
* @default 'subtle'
*/
"hue"?: 'bold' | 'subtle';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Display an outline style button.
* @default false
*/
"outline": boolean;
/**
* Set to `"soft"` for a chip with slightly rounded corners, `"round"` for a chip with fully rounded corners, or `"rectangular"` for a chip without rounded corners. Defaults to `"round"` for the `"ionic"` theme and `"soft"` for all other themes.
* @default 'soft'
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes.
* @default 'small'
*/
"size"?: 'small' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
}
interface IonNav {
/**
* If `true`, the nav should animate the transition of components.
@@ -2208,6 +2247,10 @@ export namespace Components {
* Data you want to pass to the component as props. Only used if the `"routerDirection"` is `"forward"` or `"root"`.
*/
"componentProps"?: ComponentProps;
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* The transition animation when navigating to another page.
*/
@@ -4535,6 +4578,12 @@ declare global {
prototype: HTMLIonModalElement;
new (): HTMLIonModalElement;
};
interface HTMLIonMyChipElement extends Components.IonMyChip, HTMLStencilElement {
}
var HTMLIonMyChipElement: {
prototype: HTMLIonMyChipElement;
new (): HTMLIonMyChipElement;
};
interface HTMLIonNavElementEventMap {
"ionNavWillLoad": void;
"ionNavWillChange": void;
@@ -5236,6 +5285,7 @@ declare global {
"ion-menu-button": HTMLIonMenuButtonElement;
"ion-menu-toggle": HTMLIonMenuToggleElement;
"ion-modal": HTMLIonModalElement;
"ion-my-chip": HTMLIonMyChipElement;
"ion-nav": HTMLIonNavElement;
"ion-nav-link": HTMLIonNavLinkElement;
"ion-note": HTMLIonNoteElement;
@@ -7375,6 +7425,45 @@ declare namespace LocalJSX {
*/
"trigger"?: string | undefined;
}
interface IonMyChip {
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* If `true`, the user cannot interact with the chip.
* @default false
*/
"disabled"?: boolean;
/**
* Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Only applies to the `ionic` theme.
* @default 'subtle'
*/
"hue"?: 'bold' | 'subtle';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Display an outline style button.
* @default false
*/
"outline"?: boolean;
/**
* Set to `"soft"` for a chip with slightly rounded corners, `"round"` for a chip with fully rounded corners, or `"rectangular"` for a chip without rounded corners. Defaults to `"round"` for the `"ionic"` theme and `"soft"` for all other themes.
* @default 'soft'
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes.
* @default 'small'
*/
"size"?: 'small' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
}
interface IonNav {
/**
* If `true`, the nav should animate the transition of components.
@@ -7420,6 +7509,10 @@ declare namespace LocalJSX {
* Data you want to pass to the component as props. Only used if the `"routerDirection"` is `"forward"` or `"root"`.
*/
"componentProps"?: ComponentProps;
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* The transition animation when navigating to another page.
*/
@@ -9166,6 +9259,7 @@ declare namespace LocalJSX {
"ion-menu-button": IonMenuButton;
"ion-menu-toggle": IonMenuToggle;
"ion-modal": IonModal;
"ion-my-chip": IonMyChip;
"ion-nav": IonNav;
"ion-nav-link": IonNavLink;
"ion-note": IonNote;
@@ -9269,6 +9363,7 @@ declare module "@stencil/core" {
"ion-menu-button": LocalJSX.IonMenuButton & JSXBase.HTMLAttributes<HTMLIonMenuButtonElement>;
"ion-menu-toggle": LocalJSX.IonMenuToggle & JSXBase.HTMLAttributes<HTMLIonMenuToggleElement>;
"ion-modal": LocalJSX.IonModal & JSXBase.HTMLAttributes<HTMLIonModalElement>;
"ion-my-chip": LocalJSX.IonMyChip & JSXBase.HTMLAttributes<HTMLIonMyChipElement>;
"ion-nav": LocalJSX.IonNav & JSXBase.HTMLAttributes<HTMLIonNavElement>;
"ion-nav-link": LocalJSX.IonNavLink & JSXBase.HTMLAttributes<HTMLIonNavLinkElement>;
"ion-note": LocalJSX.IonNote & JSXBase.HTMLAttributes<HTMLIonNoteElement>;

View File

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

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 1004 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,6 +1,5 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core';
import { checkInvalidState } from '@utils/forms';
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
import { createColorClasses, hostContext } from '@utils/theme';
@@ -35,8 +34,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;
@@ -122,13 +121,6 @@ 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.
*
@@ -146,69 +138,18 @@ 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() {
this.el.focus();
if (this.focusEl) {
this.focusEl.focus();
}
}
/**
@@ -228,6 +169,7 @@ export class Checkbox implements ComponentInterface {
private toggleChecked = (ev: Event) => {
ev.preventDefault();
this.setFocus();
this.setChecked(!this.checked);
this.indeterminate = false;
};
@@ -265,10 +207,10 @@ export class Checkbox implements ComponentInterface {
ev.stopPropagation();
};
private getHintTextId(): string | undefined {
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
private getHintTextID(): string | undefined {
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (isInvalid && errorText) {
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
@@ -284,7 +226,7 @@ export class Checkbox implements ComponentInterface {
* This element should only be rendered if hint text is set.
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
const { helperText, errorText, helperTextId, errorTextId } = this;
/**
* undefined and empty string values should
@@ -297,11 +239,11 @@ export class Checkbox implements ComponentInterface {
return (
<div class="checkbox-bottom">
<div id={helperTextId} class="helper-text" part="supporting-text helper-text" aria-live="polite">
{!isInvalid ? helperText : null}
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
{helperText}
</div>
<div id={errorTextId} class="error-text" part="supporting-text error-text" role="alert">
{isInvalid ? errorText : null}
<div id={errorTextId} class="error-text" part="supporting-text error-text">
{errorText}
</div>
</div>
);
@@ -336,17 +278,13 @@ export class Checkbox implements ComponentInterface {
<Host
role="checkbox"
aria-checked={indeterminate ? 'mixed' : `${checked}`}
aria-describedby={this.hintTextId}
aria-invalid={this.isInvalid ? 'true' : undefined}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
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),
@@ -358,6 +296,7 @@ export class Checkbox implements ComponentInterface {
[`checkbox-alignment-${alignment}`]: alignment !== undefined,
[`checkbox-label-placement-${labelPlacement}`]: true,
})}
onClick={this.onClick}
>
<label class="checkbox-wrapper" htmlFor={inputId}>
{/*
@@ -370,6 +309,9 @@ export class Checkbox implements ComponentInterface {
disabled={disabled}
id={inputId}
onChange={this.toggleChecked}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
required={required}
{...inheritedAttributes}
/>
@@ -386,7 +328,7 @@ export class Checkbox implements ComponentInterface {
{this.renderHintText()}
</div>
<div class="native-wrapper">
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container" aria-hidden="true">
<svg class="checkbox-icon" viewBox="0 0 24 24" part="container">
{path}
</svg>
</div>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

View File

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

View File

@@ -1,184 +0,0 @@
<!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>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -3,7 +3,6 @@ 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';
@@ -1599,7 +1598,7 @@ export class Datetime implements ComponentInterface {
forcePresentation === 'time-date'
? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)]
: [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)];
return <ion-picker class={FOCUS_TRAP_DISABLE_CLASS}>{renderArray}</ion-picker>;
return <ion-picker>{renderArray}</ion-picker>;
}
private renderDatePickerColumns(forcePresentation: string) {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import {
h,
} from '@stencil/core';
import type { NotchController } from '@utils/forms';
import { createNotchController, checkInvalidState } from '@utils/forms';
import { createNotchController } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
import { createSlotMutationController } from '@utils/slot-mutation-controller';
@@ -79,15 +79,8 @@ export class Input implements ComponentInterface {
*/
@State() hasFocus = false;
/**
* Track validation state for proper aria-live announcements
*/
@State() isInvalid = false;
@Element() el!: HTMLIonInputElement;
private validationObserver?: MutationObserver;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -413,26 +406,6 @@ export class Input implements ComponentInterface {
() => this.labelSlot
);
// 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;
// Force a re-render to update aria-describedby immediately
forceUpdate(this);
}
});
this.validationObserver.observe(el, {
attributes: true,
attributeFilter: ['class'],
});
}
// Always set initial state
this.isInvalid = checkInvalidState(el);
this.debounceChanged();
if (Build.isBrowser) {
document.dispatchEvent(
@@ -478,12 +451,6 @@ export class Input implements ComponentInterface {
this.notchController.destroy();
this.notchController = undefined;
}
// Clean up validation observer to prevent memory leaks
if (this.validationObserver) {
this.validationObserver.disconnect();
this.validationObserver = undefined;
}
}
/**
@@ -659,22 +626,22 @@ export class Input implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
const { helperText, errorText, helperTextId, errorTextId } = this;
return [
<div id={helperTextId} class="helper-text" aria-live="polite">
{!isInvalid ? helperText : null}
<div id={helperTextId} class="helper-text">
{helperText}
</div>,
<div id={errorTextId} class="error-text" role="alert">
{isInvalid ? errorText : null}
<div id={errorTextId} class="error-text">
{errorText}
</div>,
];
}
private getHintTextID(): string | undefined {
const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (isInvalid && errorText) {
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
@@ -897,7 +864,7 @@ export class Input implements ComponentInterface {
onCompositionstart={this.onCompositionStart}
onCompositionend={this.onCompositionEnd}
aria-describedby={this.getHintTextID()}
aria-invalid={this.isInvalid ? 'true' : undefined}
aria-invalid={this.getHintTextID() === this.errorTextId}
{...this.inheritedAttributes}
/>
{this.clearInput && !readonly && !disabled && (

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,284 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - 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: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: var(--ion-color-step-600);
margin-top: 10px;
margin-bottom: 5px;
}
.validation-info {
margin: 20px;
padding: 10px;
background: var(--ion-color-light);
border-radius: 4px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - 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 Email Field</h2>
<ion-input
id="email-input"
type="email"
label="Email"
label-placement="floating"
fill="outline"
placeholder="Enter your email"
helper-text="We'll never share your email"
error-text="Please enter a valid email address"
required
></ion-input>
</div>
<div>
<h2>Required Name Field</h2>
<ion-input
id="name-input"
type="text"
label="Full Name"
label-placement="floating"
fill="outline"
placeholder="Enter your full name"
helper-text="First and last name"
error-text="Name is required"
required
></ion-input>
</div>
<div>
<h2>Phone Number (Pattern Validation)</h2>
<ion-input
id="phone-input"
type="tel"
label="Phone"
label-placement="floating"
fill="outline"
placeholder="(555) 555-5555"
pattern="^\(\d{3}\) \d{3}-\d{4}$"
helper-text="Format: (555) 555-5555"
error-text="Please enter a valid phone number"
required
></ion-input>
</div>
<div>
<h2>Password (Min Length)</h2>
<ion-input
id="password-input"
type="password"
label="Password"
label-placement="floating"
fill="outline"
placeholder="Enter password"
minlength="8"
helper-text="At least 8 characters"
error-text="Password must be at least 8 characters"
required
></ion-input>
</div>
<div>
<h2>Age (Number Range)</h2>
<ion-input
id="age-input"
type="number"
label="Age"
label-placement="floating"
fill="outline"
placeholder="Enter your age"
min="18"
max="120"
helper-text="Must be 18 or older"
error-text="Please enter a valid age (18-120)"
required
></ion-input>
</div>
<div>
<h2>Optional Field (No Validation)</h2>
<ion-input
id="optional-input"
type="text"
label="Optional Info"
label-placement="floating"
fill="outline"
placeholder="This field is optional"
helper-text="You can skip this field"
></ion-input>
</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 inputs = document.querySelectorAll('ion-input');
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 = {
'email-input': (value) => {
if (!value) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
},
'name-input': (value) => {
return value && value.trim().length > 0;
},
'phone-input': (value) => {
if (!value) return false;
return /^\(\d{3}\) \d{3}-\d{4}$/.test(value);
},
'password-input': (value) => {
return value && value.length >= 8;
},
'age-input': (value) => {
if (!value) return false;
const age = parseInt(value);
return age >= 18 && age <= 120;
},
'optional-input': () => true, // Always valid
};
function validateField(input) {
const inputId = input.id;
const value = input.value;
const isValid = validators[inputId] ? validators[inputId](value) : true;
// Only show validation state if field has been touched
if (touchedFields.has(inputId)) {
if (isValid) {
input.classList.remove('ion-invalid');
input.classList.add('ion-valid');
} else {
input.classList.remove('ion-valid');
input.classList.add('ion-invalid');
}
input.classList.add('ion-touched');
}
return isValid;
}
function validateForm() {
let allValid = true;
inputs.forEach((input) => {
if (input.id !== 'optional-input') {
const isValid = validateField(input);
if (!isValid) {
allValid = false;
}
}
});
submitBtn.disabled = !allValid;
return allValid;
}
// Add event listeners
inputs.forEach((input) => {
// Mark as touched on blur
input.addEventListener('ionBlur', (e) => {
touchedFields.add(input.id);
validateField(input);
validateForm();
const isInvalid = input.classList.contains('ion-invalid');
if (isInvalid) {
console.log('Field marked invalid:', input.label, input.errorText);
}
});
// Validate on input
input.addEventListener('ionInput', (e) => {
if (touchedFields.has(input.id)) {
validateField(input);
validateForm();
}
});
// Also validate on focus loss via native blur
input.addEventListener('focusout', (e) => {
// Small delay to ensure Ionic's classes are updated
setTimeout(() => {
touchedFields.add(input.id);
validateField(input);
validateForm();
}, 10);
});
});
// Reset button
resetBtn.addEventListener('click', () => {
inputs.forEach((input) => {
input.value = '';
input.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
});
touchedFields.clear();
submitBtn.disabled = true;
});
// Submit button
submitBtn.addEventListener('click', () => {
if (validateForm()) {
alert('Form submitted successfully!');
}
});
// Initial setup
validateForm();
</script>
</body>
</html>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -418,9 +418,6 @@ export class Menu implements ComponentInterface, MenuI {
*/
@Method()
setOpen(shouldOpen: boolean, animated = true, role?: string): Promise<boolean> {
// Blur the active element to prevent it from being kept focused inside an element that will be set with aria-hidden="true"
(document.activeElement as HTMLElement)?.blur();
return menuController._setOpen(this, shouldOpen, animated, role);
}

View File

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

View File

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

View File

@@ -28,24 +28,6 @@ describe('modal: focus trap', () => {
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should set the focus trap class when disabled via string prop', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')! as any;
// Simulate frameworks passing the boolean as a string prop, e.g. focusTrap="false" in Angular
modal.focusTrap = 'false';
await page.waitForChanges();
// This currently fails because the host class only inspects focusTrap === false
// and treats the string "false" as truthy.
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should not set the focus trap class by default', async () => {
const page = await newSpecPage({
components: [Modal],

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,187 @@
@import "../../themes/ionic.functions.color";
@import "../../themes/ionic.mixins";
:host {
/**
* @prop --ion-chip-focus-ring-color: Color of the focus ring
* @prop --ion-chip-focus-ring-width: Width of the focus ring
* @prop --ion-chip-padding-horizontal: Padding top and bottom of the chip
* @prop --ion-chip-padding-vertical: Padding start and end of the chip
* @prop --ion-chip-font-weight: Font weight of the chip
* @prop --ion-chip-line-height: Line height of the chip
* @prop --ion-chip-gap: Gap between the chip and the text
* @prop --ion-chip-border-width: Border width of the chip
*
* @prop --ion-chip-shape-soft-border-radius: Border radius of the chip for the soft shape
* @prop --ion-chip-shape-round-border-radius: Border radius of the chip for the round shape
* @prop --ion-chip-shape-rectangular-border-radius: Border radius of the chip for the rectangular shape
*
* @prop --ion-chip-size-small-min-height: Minimum height of the chip for the small size
* @prop --ion-chip-size-large-min-height: Minimum height of the chip for the large size
* @prop --ion-chip-size-small-font-size: Font size of the chip for the small size
* @prop --ion-chip-size-large-font-size: Font size of the chip for the large size
*
* @prop --ion-chip-hue-subtle-bg: Background of the chip for the subtle hue
* @prop --ion-chip-hue-subtle-color: Color of the chip for the subtle hue
* @prop --ion-chip-hue-subtle-border-color: Border color of the chip for the subtle hue
* @prop --ion-chip-hue-bold-bg: Background of the chip for the bold hue
* @prop --ion-chip-hue-bold-color: Color of the chip for the bold hue
* @prop --ion-chip-hue-bold-border-color: Border color of the chip for the bold hue
*/
--ion-chip-focus-ring-color: var(--ion-color-blue-50);
--ion-chip-focus-ring-width: var(--ion-spacing-xs);
--ion-chip-padding-horizontal: var(--ion-spacing-xs);
--ion-chip-padding-vertical: var(--ion-spacing-sm);
--ion-chip-font-weight: var(--ion-font-weights-normal);
--ion-chip-line-height: var(--ion-line-heights-md);
--ion-chip-gap: var(--ion-spacing-xs);
--ion-chip-border-width: var(--ion-spacing-xxs);
--ion-chip-shape-soft-border-radius: var(--ion-radii-lg);
--ion-chip-shape-round-border-radius: var(--ion-radii-xxl);
--ion-chip-shape-rectangular-border-radius: var(--ion-radii-none);
--ion-chip-size-small-min-height: var(--ion-scaling-600);
--ion-chip-size-large-min-height: var(--ion-scaling-900);
--ion-chip-size-small-font-size: var(--ion-font-sizes-sm-rem);
--ion-chip-size-large-font-size: var(--ion-font-sizes-lg-rem);
--ion-chip-hue-subtle-bg: var(--ion-color-gray-100);
--ion-chip-hue-subtle-color: var(--ion-color-gray-800);
--ion-chip-hue-subtle-border-color: var(--ion-color-gray-300);
--ion-chip-hue-bold-bg: var(--ion-color-gray-700);
--ion-chip-hue-bold-color: var(--ion-color-white);
--ion-chip-hue-bold-border-color: var(--ion-color-gray-800);
@include font-smoothing();
@include padding(var(--ion-chip-padding-horizontal), var(--ion-chip-padding-vertical));
@include border-radius(var(--ion-chip-border-radius));
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
background: var(--ion-chip-background);
color: var(--ion-chip-color);
cursor: pointer;
font-weight: var(--ion-chip-font-weight);
line-height: var(--ion-chip-line-height);
gap: var(--ion-chip-gap);
overflow: hidden;
box-sizing: border-box;
vertical-align: middle;
}
:host(.chip-disabled) {
cursor: default;
pointer-events: none;
}
// Outline Chip
// ---------------------------------------------
:host(.chip-outline) {
border-width: var(--ion-chip-border-width);
border-style: solid; // Do we need to use a variable here? ionic uses tokens but all themes use solid so it's not really necessary
}
// Chip Shapes
// ---------------------------------------------
:host(.chip-soft) {
--ion-chip-border-radius: var(--ion-chip-shape-soft-border-radius);
}
:host(.chip-round) {
--ion-chip-border-radius: var(--ion-chip-shape-round-border-radius);
}
:host(.chip-rectangular) {
--ion-chip-border-radius: var(--ion-chip-shape-rectangular-border-radius);
}
// Size
// ---------------------------------------------
:host(.chip-small) {
min-height: var(--ion-chip-size-small-min-height);
font-size: var(--ion-chip-size-small-font-size);
}
:host(.chip-large) {
min-height: var(--ion-chip-size-large-min-height);
font-size: var(--ion-chip-size-large-font-size);
}
// Subtle Chip
// ---------------------------------------------
:host(.chip-subtle) {
--ion-chip-background: var(--ion-chip-hue-subtle-bg);
--ion-chip-color: var(--ion-chip-hue-subtle-color);
}
:host(.chip-outline.chip-subtle) {
border-color: var(--ion-chip-hue-subtle-border-color);
}
// Bold Chip
// ---------------------------------------------
:host(.chip-bold) {
--ion-chip-background: var(--ion-chip-hue-bold-bg);
--ion-chip-color: var(--ion-chip-hue-bold-color);
}
:host(.chip-outline.chip-bold) {
border-color: var(--ion-chip-hue-bold-border-color);
}
// Chip Colors
// ---------------------------------------------
// Subtle
:host(.chip-subtle.ion-color) {
background: current-color(
base,
$subtle: true
); // these don't work because we need to update the function to use the new color system
color: current-color(
contrast,
$subtle: true
); // these don't work because we need to update the function to use the new color system
}
:host(.chip-subtle.chip-outline.ion-color) {
border-color: current-color(
shade,
$subtle: true
); // these don't work because we need to update the function to use the new color system
}
// Bold
:host(.chip-bold.ion-color) {
background: current-color(
base
); // these don't work because we need to update the function to use the new color system
color: current-color(
contrast
); // these don't work because we need to update the function to use the new color system
}
:host(.chip-bold.chip-outline.ion-color) {
border-color: current-color(
shade
); // these don't work because we need to update the function to use the new color system
}

View File

@@ -0,0 +1,113 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, h, Element } from '@stencil/core';
import { createColorClasses } from '@utils/theme';
import { getIonMode, getIonCustomTheme } from '../../global/ionic-global';
import type { Color } from '../../interface';
import { generateCSSVars } from '../../themes/base/generate-css-vars';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
* @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
*/
@Component({
tag: 'ion-my-chip',
styleUrl: 'my-chip.base.scss',
shadow: true,
})
export class MyChip implements ComponentInterface {
private isThemed = false;
@Element() el!: HTMLElement;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
* For more information on colors, see [theming](/docs/theming/basics).
*/
@Prop({ reflect: true }) color?: Color;
/**
* Display an outline style button.
*/
@Prop() outline = false;
/**
* If `true`, the user cannot interact with the chip.
*/
@Prop() disabled = false;
/**
* Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for
* a chip with muted, subtle colors.
*
* Only applies to the `ionic` theme.
*/
// THOUGHT: Comparing to ChakraUI, they use variants (aka fills) for this
// maybe we should consider using variants: solid, outline, subtle
// they use solid for bold solid background, no outline, has states
// subtle for light background, no outline, has states
// outline for outline style, no background, has states
// surface for light background, with outline, has states
// ghost for no background, no outline, has states
// plain for no background, no outline, no states
@Prop() hue?: 'bold' | 'subtle' = 'subtle';
/**
* Set to `"soft"` for a chip with slightly rounded corners, `"round"` for a chip with fully
* rounded corners, or `"rectangular"` for a chip without rounded corners.
* Defaults to `"round"` for the `"ionic"` theme and `"soft"` for all other themes.
*/
@Prop() shape?: 'soft' | 'round' | 'rectangular' = 'soft';
/**
* Set to `"small"` for a chip with less height and padding.
*
* Defaults to `"large"` for the ionic theme, and undefined for all other themes.
*/
@Prop() size?: 'small' | 'large' = 'small';
componentWillLoad() {
const myCustomTheme = getIonCustomTheme();
const componentTheme = myCustomTheme.components['IonChip'];
// check if componentTheme is not an empty object or undefined
if (componentTheme !== undefined || Object.keys(componentTheme).length > 0) {
this.isThemed = true;
// apply a style tag to this component
const style = document.createElement('style');
style.innerHTML = [generateCSSVars(componentTheme, '--ion-chip-', ':host(.chip-themed)')].join('\n\n');
// Attach to Shadow Root if available, otherwise Light DOM
const root = this.el.shadowRoot ?? this.el;
root.appendChild(style);
}
}
render() {
const { hue, shape, size } = this;
const mode = getIonMode(this);
return (
<Host
aria-disabled={this.disabled ? 'true' : null}
class={createColorClasses(this.color, {
// Needed to properly add the custom styles else
// the styles would be ignored due to styling order
'chip-themed': this.isThemed,
[`chip-${shape}`]: shape !== undefined,
'chip-outline': this.outline,
'chip-disabled': this.disabled,
'ion-activatable': true,
'ion-focusable': !this.disabled,
[`chip-${size}`]: size !== undefined,
[`chip-${hue}`]: hue !== undefined,
})}
>
<slot></slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</Host>
);
}
}

View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>MyChip - Basic</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>MyChip - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" id="content" style="text-align: center">
<h2>My Chip</h2>
<p>
This is a POC, not all the features are working as expected. For example, the color property still needs to be
worked on.
</p>
<p>You can verify that the variables are being generated correctly by inspecting the root element.</p>
<ion-my-chip>default</ion-my-chip>
<ion-my-chip outline>outline</ion-my-chip>
<ion-my-chip size="large">large</ion-my-chip>
<ion-my-chip hue="bold">bold</ion-my-chip>
<ion-my-chip hue="bold" outline>bold with outline</ion-my-chip>
<ion-my-chip color="primary">primary, subtle hue</ion-my-chip>
<ion-my-chip color="primary" hue="bold">primary, bold hue</ion-my-chip>
<ion-my-chip color="secondary">secondary, subtle hue</ion-my-chip>
<ion-my-chip color="secondary" hue="bold">secondary, bold hue</ion-my-chip>
</ion-content>
</ion-app>
<script>
window.Ionic = {
config: {
customTheme: {
palette: {
light: {
color: {
primary: {
bold: {
base: 'pink',
constrast: 'black',
},
},
secondary: {
bold: {
base: '#510aa8',
},
},
// gray: {
// 800: 'pink',
// }
},
},
},
components: {
IonChip: {
hue: {
subtle: {
bg: 'red',
color: 'white',
borderColor: 'black'
},
bold: {
bg: 'blue',
color: 'white',
borderColor: 'black'
}
}
},
},
},
},
};
</script>
</body>
</html>

View File

@@ -7,6 +7,9 @@ import type { RouterDirection } from '../router/utils/interface';
import { navLink } from './nav-link-utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
*/
@Component({
tag: 'ion-nav-link',
})

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