diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e44acd31374..98905a6c843 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,10 +20,10 @@ body: id: affected-versions attributes: label: Ionic Framework Version - description: Which version(s) of Ionic Framework does this issue impact? [Ionic Framework 1.x to 6.x are no longer supported](https://ionicframework.com/docs/reference/support#framework-maintenance-and-support-status). For extended support, considering visiting [Ionic's Enterprise offering](https://ionic.io/enterprise). + description: Which version(s) of Ionic Framework does this issue impact? [Ionic Framework 1.x to 7.x are no longer supported](https://ionicframework.com/docs/reference/support#framework-maintenance-and-support-status). options: - - v7.x - v8.x + - v9.x - Nightly multiple: true validations: diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index e21a39669bf..70c65316883 100644 --- a/.github/workflows/actions/build-angular-server/action.yml +++ b/.github/workflows/actions/build-angular-server/action.yml @@ -19,10 +19,25 @@ runs: run: npm run sync shell: bash working-directory: ./packages/angular-server + - name: πŸ–ŒοΈ Lint + run: npm run lint + shell: bash + working-directory: ./packages/angular-server + # No Typecheck step here. Unlike the rollup-built packages, which report + # type errors as warnings and still exit 0, ng-packagr fails the build on + # them, so the Build step below already covers this. - name: πŸ—οΈ Build run: npm run build.prod shell: bash working-directory: ./packages/angular-server + - name: Clean core package.json + run: git checkout ./package.json + shell: bash + working-directory: ./core + - name: πŸ” Check Diff + run: git diff --exit-code + shell: bash + working-directory: ./packages/angular-server - uses: ./.github/workflows/actions/upload-archive with: name: ionic-angular-server diff --git a/.github/workflows/actions/build-angular/action.yml b/.github/workflows/actions/build-angular/action.yml index 12e1db1f5dc..121fa6f7da9 100644 --- a/.github/workflows/actions/build-angular/action.yml +++ b/.github/workflows/actions/build-angular/action.yml @@ -23,6 +23,9 @@ runs: run: npm run lint shell: bash working-directory: ./packages/angular + # No Typecheck step here. Unlike the rollup-built packages, which report + # type errors as warnings and still exit 0, ng-packagr fails the build on + # them, so the Build step below already covers this. - name: πŸ—οΈ Build run: npm run build shell: bash @@ -39,4 +42,4 @@ runs: with: name: ionic-angular output: ./packages/angular/AngularBuild.zip - paths: ./packages/angular/dist + paths: ./packages/angular/dist ./packages/angular/css diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index 8cc7016349f..f1e33c8b55e 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -1,10 +1,14 @@ name: 'Build Ionic Core with Stencil Prerelease' description: 'Build Ionic Core with a Prerelease Build of Stencil' inputs: - stencil-version: + stencil-core-version: description: 'The NPM tag of @stencil/core to install.' type: string required: true + stencil-react-output-target-version: + description: 'The NPM tag of @stencil/react-output-target to install.' + type: string + required: true runs: using: 'composite' steps: @@ -13,13 +17,17 @@ runs: with: node-version: 24.x - - name: πŸ•ΈοΈ Install Dependencies + - name: πŸ•ΈοΈ Install Core Dependencies run: npm ci working-directory: ./core shell: bash - - name: πŸ“¦ Install Stencil ${{ inputs.stencil-version }} + - name: πŸ“¦ Install Stencil Core ${{ inputs.stencil-core-version }} + working-directory: ./core + run: npm i @stencil/core@${{ inputs.stencil-core-version }} + shell: bash + - name: πŸ“¦ Install Stencil React Output Target ${{ inputs.stencil-react-output-target-version }} working-directory: ./core - run: npm i @stencil/core@${{ inputs.stencil-version }} + run: npm i @stencil/react-output-target@${{ inputs.stencil-react-output-target-version }} shell: bash - name: πŸ—οΈ Build Core run: npm run build -- --ci --debug --verbose @@ -29,8 +37,16 @@ runs: run: npm run test.lazy-imports working-directory: ./core shell: bash + - name: πŸ•ΈοΈ Install React Package Dependencies + run: npm ci + working-directory: ./packages/react + shell: bash + - name: πŸ—οΈ Lint React Package + run: npm run lint + working-directory: ./packages/react + shell: bash - uses: ./.github/workflows/actions/upload-archive with: name: ionic-core output: core/CoreBuild.zip - paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/package.json + paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/package.json packages/react/src/components/components.ts diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index 523b303139a..068eab5c471 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -37,4 +37,4 @@ runs: output: core/CoreBuild.zip # Include generated proxy files from Stencil output targets so # framework builds can detect when they need to be updated - paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt packages/angular/src/directives/proxies.ts packages/angular/src/directives/proxies-list.ts packages/angular/standalone/src/directives/proxies.ts packages/vue/src/proxies.ts packages/react/src/components/proxies.ts packages/react/src/components/inner-proxies.ts packages/react/src/components/routing-proxies.ts + paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt packages/angular/src/lazy/directives/proxies.ts packages/angular/src/lazy/directives/proxies-list.ts packages/angular/src/standalone/directives packages/vue/src/proxies.ts packages/react/src/components/proxies.ts packages/react/src/components/inner-proxies.ts packages/react/src/components/routing-proxies.ts diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index b104ebd429d..bb2cbb27eea 100644 --- a/.github/workflows/actions/build-react-router/action.yml +++ b/.github/workflows/actions/build-react-router/action.yml @@ -28,10 +28,22 @@ runs: run: npm run lint shell: bash working-directory: ./packages/react-router + # The rollup build reports type errors as warnings and still succeeds, so + # this step is what keeps the package type-clean. + - name: πŸ”Ž Typecheck + run: npm run typecheck + shell: bash + working-directory: ./packages/react-router - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/react-router + - name: Clean package.json updates + run: git checkout ./core/package.json ./packages/react/package.json + shell: bash + - name: πŸ” Check Diff + run: git diff --exit-code + shell: bash - uses: ./.github/workflows/actions/upload-archive with: name: ionic-react-router diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index 5335ea47af2..b7d3ac6b99a 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -23,6 +23,12 @@ runs: run: npm run lint shell: bash working-directory: ./packages/react + # The rollup build reports type errors as warnings and still succeeds, so + # this step is what keeps the package type-clean. + - name: πŸ”Ž Typecheck + run: npm run typecheck + shell: bash + working-directory: ./packages/react - name: πŸ—οΈ Build run: npm run build shell: bash @@ -31,16 +37,14 @@ runs: run: npm run test.spec shell: bash working-directory: ./packages/react - - name: Clean core package.json - run: git checkout ./package.json - shell: bash - working-directory: ./core - - name: πŸ” Check Diff - run: git diff --exit-code - shell: bash - working-directory: ./packages/react - uses: ./.github/workflows/actions/upload-archive with: name: ionic-react output: packages/react/ReactBuild.zip - paths: packages/react/dist packages/react/css + paths: packages/react/dist packages/react/css packages/react/package.json + - name: Clean package.json updates + run: git checkout ./core/package.json ./packages/react/package.json + shell: bash + - name: πŸ” Check Diff + run: git diff --exit-code + shell: bash diff --git a/.github/workflows/actions/build-vue-router/action.yml b/.github/workflows/actions/build-vue-router/action.yml index bf47bb0dec3..68972ef2324 100644 --- a/.github/workflows/actions/build-vue-router/action.yml +++ b/.github/workflows/actions/build-vue-router/action.yml @@ -28,14 +28,22 @@ runs: run: npm run lint shell: bash working-directory: ./packages/vue-router + # The rollup build reports type errors as warnings and still succeeds, so + # this step is what keeps the package type-clean. + - name: πŸ”Ž Typecheck + run: npm run typecheck + shell: bash + working-directory: ./packages/vue-router - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/vue-router - - name: πŸ§ͺ Test Spec - run: npm run test.spec + - name: Clean package.json updates + run: git checkout ./core/package.json + shell: bash + - name: πŸ” Check Diff + run: git diff --exit-code shell: bash - working-directory: ./packages/vue-router - uses: ./.github/workflows/actions/upload-archive with: name: ionic-vue-router diff --git a/.github/workflows/actions/build-vue/action.yml b/.github/workflows/actions/build-vue/action.yml index 4a7375786d2..22c731e99ad 100644 --- a/.github/workflows/actions/build-vue/action.yml +++ b/.github/workflows/actions/build-vue/action.yml @@ -23,18 +23,22 @@ runs: run: npm run lint shell: bash working-directory: ./packages/vue + # The rollup build reports type errors as warnings and still succeeds, so + # this step is what keeps the package type-clean. + - name: πŸ”Ž Typecheck + run: npm run typecheck + shell: bash + working-directory: ./packages/vue - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/vue - - name: Clean core package.json - run: git checkout ./package.json + - name: Clean package.json updates + run: git checkout ./core/package.json ./packages/react/package.json shell: bash - working-directory: ./core - name: πŸ” Check Diff run: git diff --exit-code shell: bash - working-directory: ./packages/vue - uses: ./.github/workflows/actions/upload-archive with: name: ionic-vue diff --git a/.github/workflows/actions/test-core-clean-build/action.yml b/.github/workflows/actions/test-core-clean-build/action.yml index 25c9a202652..f6275acbee5 100644 --- a/.github/workflows/actions/test-core-clean-build/action.yml +++ b/.github/workflows/actions/test-core-clean-build/action.yml @@ -12,10 +12,9 @@ runs: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Clean core package.json - run: git checkout ./package.json + - name: Clean package.json updates + run: git checkout ./core/package.json ./packages/react/package.json shell: bash - working-directory: ./core - name: πŸ” Check Diff run: | git diff --exit-code || { diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml index 663ae0be575..beea150b549 100644 --- a/.github/workflows/actions/test-react-router-e2e/action.yml +++ b/.github/workflows/actions/test-react-router-e2e/action.yml @@ -32,6 +32,10 @@ runs: run: npm install shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} + - name: πŸ“¦ Install Playwright Browsers + run: npx playwright install chromium + shell: bash + working-directory: ./packages/react-router/test/build/${{ inputs.app }} - name: πŸ”„ Sync Built Changes run: npm run sync shell: bash @@ -40,7 +44,13 @@ runs: run: npm run build shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} - - name: πŸ§ͺ Run Tests + - name: πŸ§ͺ Run Cypress Tests run: npm run e2e shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} + - name: 🎭 Run Playwright Tests + run: npx playwright test --retries=2 + env: + CI: true + shell: bash + working-directory: ./packages/react-router/test/build/${{ inputs.app }} diff --git a/.github/workflows/actions/test-vue-e2e/action.yml b/.github/workflows/actions/test-vue-e2e/action.yml index 5f7de702d08..2316e019a46 100644 --- a/.github/workflows/actions/test-vue-e2e/action.yml +++ b/.github/workflows/actions/test-vue-e2e/action.yml @@ -32,6 +32,10 @@ runs: run: npm install shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} + - name: πŸ“¦ Install Playwright Browsers + run: npx playwright install chromium + shell: bash + working-directory: ./packages/vue/test/build/${{ inputs.app }} - name: πŸ”„ Sync run: npm run sync shell: bash @@ -44,3 +48,9 @@ runs: run: npm run test:e2e shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} + - name: 🎭 Run Playwright Tests + run: npx playwright test --retries=2 + env: + CI: true + shell: bash + working-directory: ./packages/vue/test/build/${{ inputs.app }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 540d873c5db..90d65ee18f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,7 +140,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [ng16, ng17, ng18, ng19, ng20] + apps: [ng18, ng19, ng20, ng21, ng22] needs: [build-angular, build-angular-server] runs-on: ubuntu-latest steps: @@ -176,7 +176,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [reactrouter5] + apps: [reactrouter6-react18, reactrouter6-react19] needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: @@ -198,7 +198,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [react17, react18, react19] + apps: [react18, react19] needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml new file mode 100644 index 00000000000..3d9b03ea5c4 --- /dev/null +++ b/.github/workflows/migrate.yml @@ -0,0 +1,93 @@ +name: 'Ionic Migrate' + +# Lint, test, and build the @ionic/migrate CLI. +# +# The workflow always triggers (no trigger-level `paths` filter) so the +# `verify-migrate` gate always reports a status and can be marked a required +# check: a path-filtered *trigger* would skip the whole workflow on unrelated +# PRs, and a required check that never runs blocks merges forever. Instead the +# `changes` job detects whether the package was touched and the heavy job runs +# only then; `verify-migrate` reports success either way (skipped == fine). +on: + pull_request: + branches: [ '**' ] + merge_group: + workflow_dispatch: + +# Lint/test/build only reads the repo; keep the token least-privilege. +permissions: + contents: read + +concurrency: + group: migrate-${{ github.ref }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + migrate: ${{ steps.filter.outputs.migrate }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # Full history so the PR base commit is reachable for the diff below. + fetch-depth: 0 + - name: Detect migrate changes + id: filter + shell: bash + run: | + # Fail the step (not the else branch) if git diff itself errors, so a + # broken detector can't masquerade as "package untouched". + set -euo pipefail + # Only pull_request runs get the path optimization. merge_group and + # manual dispatch run the (fast) tests unconditionally, so the merge + # queue never blocks on a check that was skipped by a stale diff. + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "migrate=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + base="${{ github.event.pull_request.base.sha }}" + changed="$(git diff --name-only "$base" HEAD)" + if echo "$changed" \ + | grep -qE '^(packages/migrate/|\.github/workflows/migrate\.yml$)'; then + echo "migrate=true" >> "$GITHUB_OUTPUT" + else + echo "migrate=false" >> "$GITHUB_OUTPUT" + fi + + test-migrate: + needs: changes + if: ${{ needs.changes.outputs.migrate == 'true' }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/migrate + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24.x + cache: 'npm' + cache-dependency-path: packages/migrate/package-lock.json + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Test + run: npm test + - name: Build + run: npm run build + + # Always-running required gate. See header comment for why this exists. + # Green when the package was untouched (test-migrate skipped) or tests pass; + # red when detection failed or the tests actually failed. + verify-migrate: + if: ${{ always() }} + needs: [ changes, test-migrate ] + runs-on: ubuntu-latest + steps: + - name: Check results + # changes.result != 'success' catches a failed/cancelled detector, + # whose skipped test-migrate would otherwise slip through as green. + if: ${{ needs.changes.result != 'success' || needs.test-migrate.result == 'failure' || needs.test-migrate.result == 'cancelled' }} + run: exit 1 diff --git a/.github/workflows/release-ionic.yml b/.github/workflows/release-ionic.yml index 9671a057c28..e8b7030966d 100644 --- a/.github/workflows/release-ionic.yml +++ b/.github/workflows/release-ionic.yml @@ -81,13 +81,12 @@ jobs: version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/angular' - folder: './dist' - name: Cache Built @ionic/angular uses: ./.github/workflows/actions/upload-archive with: name: ionic-angular output: packages/angular/AngularBuild.zip - paths: packages/angular/dist + paths: packages/angular/dist packages/angular/css release-react: needs: [release-core] diff --git a/.github/workflows/release-migrate.yml b/.github/workflows/release-migrate.yml new file mode 100644 index 00000000000..d119e6830de --- /dev/null +++ b/.github/workflows/release-migrate.yml @@ -0,0 +1,72 @@ +name: 'Release @ionic/migrate' + +on: + workflow_call: + inputs: + tag: + description: 'Which npm tag should this be published to?' + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + release-migrate: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/migrate + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: 🟒 Configure Node for Publish + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + registry-url: 'https://registry.npmjs.org' + scope: '@ionic' + cache: 'npm' + cache-dependency-path: packages/migrate/package-lock.json + - name: πŸ“¦ Install latest npm + run: npm install -g npm@latest + shell: bash + - name: πŸ•ΈοΈ Install Dependencies + run: npm ci + shell: bash + - name: πŸ”Ž Resolve Version + id: resolve-version + run: | + set -euo pipefail + version="$(node -p "require('./package.json').version")" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Releasing @ionic/migrate@$version to the '${{ inputs.tag }}' tag" + shell: bash + - name: 🚧 Ensure Version Is Unpublished + run: | + set -euo pipefail + version="${{ steps.resolve-version.outputs.version }}" + # `npm view` exits non-zero both for an unpublished version and for a + # package that does not exist yet, and the latter is the expected state + # on the very first release. So inspect stdout rather than the exit + # code: empty output means the version is free to publish. + if [ -n "$(npm view "@ionic/migrate@$version" version 2>/dev/null || true)" ]; then + echo "::error::@ionic/migrate@$version is already published. Bump the version in packages/migrate/package.json." + exit 1 + fi + shell: bash + - name: πŸ” Lint + run: npm run lint + shell: bash + - name: πŸ§ͺ Test + run: npm test + shell: bash + - name: πŸ—οΈ Run Build + run: npm run build + shell: bash + # `--access public` is not passed here because publishConfig.access in + # package.json already sets it; a scoped package would otherwise default to + # restricted on its first publish. + - name: πŸš€ Publish to NPM + run: npm publish --tag ${{ inputs.tag }} --provenance + shell: bash diff --git a/.github/workflows/release-orchestrator.yml b/.github/workflows/release-orchestrator.yml index a0c05cbcd58..67964f340d5 100644 --- a/.github/workflows/release-orchestrator.yml +++ b/.github/workflows/release-orchestrator.yml @@ -15,6 +15,7 @@ on: options: - dev - production + - migrate version: description: 'Which version should be published? (Only for production releases)' required: false @@ -28,7 +29,7 @@ on: - premajor - prerelease tag: - description: 'Which npm tag should this be published to? (Only for production releases)' + description: 'Which npm tag should this be published to? (Only for production and migrate releases)' required: false type: choice default: latest @@ -68,6 +69,16 @@ jobs: uses: ./.github/workflows/dev-build.yml secrets: inherit + run-migrate: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'migrate' }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/release-migrate.yml + secrets: inherit + with: + tag: ${{ inputs.tag }} + run-production: if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'production' }} permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4228d33841..bb552039012 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,11 +135,13 @@ jobs: "/npm/@ionic/core@6/dist/ionic/ionic.esm.js", "/npm/@ionic/core@7/dist/ionic/ionic.esm.js", "/npm/@ionic/core@8/dist/ionic/ionic.esm.js", + "/npm/@ionic/core@9/dist/ionic/ionic.esm.js", "/npm/@ionic/core@latest/dist/ionic/ionic.esm.js", "/npm/@ionic/core@next/dist/ionic/ionic.esm.js", "/npm/@ionic/core@6/css/ionic.bundle.css", "/npm/@ionic/core@7/css/ionic.bundle.css", "/npm/@ionic/core@8/css/ionic.bundle.css", + "/npm/@ionic/core@9/css/ionic.bundle.css", "/npm/@ionic/core@latest/css/ionic.bundle.css" "/npm/@ionic/core@next/css/ionic.bundle.css" ]}' diff --git a/.github/workflows/stencil-nightly.yml b/.github/workflows/stencil-nightly.yml index a9be239edfa..5953e974d15 100644 --- a/.github/workflows/stencil-nightly.yml +++ b/.github/workflows/stencil-nightly.yml @@ -9,11 +9,16 @@ on: - cron: '00 06 * * 1-5' workflow_dispatch: inputs: - npm_release_tag: + npm_release_tag_core: required: true type: string - description: What version should be pulled from NPM? + description: What stencil/core version should be pulled from NPM? default: nightly + npm_release_tag_react_output_target: + required: true + type: string + description: What stencil/react-output-target version should be pulled from NPM? + default: latest # When pushing a new commit we should # cancel the previous test run to not @@ -29,7 +34,9 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/workflows/actions/build-core-stencil-prerelease with: - stencil-version: ${{ inputs.npm_release_tag || 'nightly' }} + stencil-core-version: ${{ inputs.npm_release_tag_core || 'nightly' }} + stencil-react-output-target-version: ${{ inputs.npm_release_tag_react_output_target || 'latest' }} + test-core-clean-build: needs: [build-core-with-stencil-nightly] @@ -52,7 +59,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/workflows/actions/test-core-spec with: - stencil-version: ${{ inputs.npm_release_tag || 'nightly' }} + stencil-version: ${{ inputs.npm_release_tag_core || 'nightly' }} test-core-screenshot: strategy: @@ -150,7 +157,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [ng16, ng17, ng18, ng19, ng20] + apps: [ng18, ng19, ng20, ng21, ng22] needs: [build-angular, build-angular-server] runs-on: ubuntu-latest steps: @@ -173,6 +180,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Stencil React Output Target + working-directory: ./packages/react + run: npm i @stencil/react-output-target@${{ inputs.npm_release_tag_react_output_target || 'latest' }} + shell: bash - uses: ./.github/workflows/actions/build-react build-react-router: @@ -186,7 +197,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [reactrouter5] + apps: [reactrouter6-react18, reactrouter6-react19] needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: @@ -208,7 +219,7 @@ jobs: strategy: fail-fast: false matrix: - apps: [react17, react18, react19] + apps: [react18, react19] needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index e610d8a11dd..d49be2c644e 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ core/www/ # playwright core/test-results/ core/playwright-report/ +packages/angular/test-results/ # ground truths generated outside of docker should not be committed to the repo core/**/*-snapshots/* diff --git a/BREAKING.md b/BREAKING.md index bf44f563dc8..2ca32ea114b 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -4,290 +4,709 @@ This is a comprehensive list of the breaking changes introduced in the major ver ## Versions -- [Version 8.x](#version-8x) +- [Version 9.x](#version-9x) +- [Version 8.x](./BREAKING_ARCHIVE/v8.md) - [Version 7.x](./BREAKING_ARCHIVE/v7.md) - [Version 6.x](./BREAKING_ARCHIVE/v6.md) - [Version 5.x](./BREAKING_ARCHIVE/v5.md) - [Version 4.x](./BREAKING_ARCHIVE/v4.md) - [Legacy](https://github.com/ionic-team/ionic-v3/blob/master/CHANGELOG.md) -## Version 8.x - -- [Browser and Platform Support](#version-8x-browser-platform-support) -- [Dark Mode](#version-8x-dark-mode) -- [Global Styles](#version-8x-global-styles) -- [Haptics](#version-8x-haptics) -- [Components](#version-8x-components) - - [Button](#version-8x-button) - - [Checkbox](#version-8x-checkbox) - - [Content](#version-8x-content) - - [Datetime](#version-8x-datetime) - - [Input](#version-8x-input) - - [Item](#version-8x-item) - - [Modal](#version-8x-modal) - - [Nav](#version-8x-nav) - - [Picker](#version-8x-picker) - - [Progress bar](#version-8x-progress-bar) - - [Radio](#version-8x-radio) - - [Range](#version-8x-range) - - [Searchbar](#version-8x-searchbar) - - [Select](#version-8x-select) - - [Textarea](#version-8x-textarea) - - [Toggle](#version-8x-toggle) -- [Framework Specific](#version-8x-framework-specific) - - [Angular](#version-8x-angular) - -

Browser and Platform Support

- -This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 8. +## Version 9.x + +- [Browser and Platform Support](#version-9x-browser-platform-support) +- [Package Exports](#version-9x-package-exports) +- [Components](#version-9x-components) + - [Input](#version-9x-input) + - [Legacy Picker](#version-9x-legacy-picker) + - [Modal](#version-9x-modal) + - [Nav](#version-9x-nav) + - [Router Outlet](#version-9x-router-outlet) + - [Searchbar](#version-9x-searchbar) + - [Select](#version-9x-select) + - [Textarea](#version-9x-textarea) +- [Framework Specific](#version-9x-framework-specific) + - [Angular](#version-9x-angular) + - [React](#version-9x-react) + - [Vue](#version-9x-vue) + +

Browser and Platform Support

+ +This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 9. **Minimum Browser Versions** | Desktop Browser | Supported Versions | -| --------------- | ----------------- | -| Chrome | 89+ | -| Safari | 15+ | -| Firefox | 75+ | -| Edge | 89+ | +| --------------- | ------------------ | +| Chrome | 89+ | +| Safari | 16+ | +| Edge | 89+ | +| Firefox | 75+ | **Minimum JavaScript Framework Versions** | Framework | Supported Version | | --------- | --------------------- | -| Angular | 16+ | -| React | 17+ | -| Vue | 3.0.6+ | +| Angular | 18+ | +| React | 18 or 19 | +| Vue | 3.5+ | **Minimum Mobile Platform Versions** | Platform | Supported Version | | -------- | ---------------------- | -| iOS | 15+ | +| iOS | 16+ | | Android | 5.1+ with Chromium 89+ | -Ionic Framework v8 removes backwards support for CSS Animations in favor of the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). All minimum browser versions listed above support the Web Animations API. +**Minimum Native Runtime Versions** +| Native Runtime | Supported Version | +| -------------- | ----------------- | +| Capacitor | 7+ | -

Dark Mode

+Ionic's native platform detection no longer checks the Capacitor 2 `isNative` flag. `isCapacitorNative` now relies solely on `Capacitor.isNativePlatform()`, which was added in Capacitor 3. Apps running Capacitor 2 will no longer be detected as a native/hybrid platform, so `isPlatform('capacitor')`, `isPlatform('hybrid')`, and `getPlatforms()` will report web instead of native. Upgrade to a supported Capacitor version (7 or later). +

Package Exports

-In previous versions, it was recommended to define the dark palette in the following way: +`@ionic/core`'s `package.json` now declares an `exports` field. Subpaths like `@ionic/core/components` and `@ionic/core/loader` previously failed under Node ESM (Angular 21's default Vitest builder, raw Node, etc.) with `ERR_UNSUPPORTED_DIR_IMPORT`, because the strict ESM resolver doesn't read the nested `package.json` files this package relied on. The new `exports` map declares the documented subpaths explicitly. -```css -@media (prefers-color-scheme: dark) { - body { - /* global app variables */ - } +`exports` is an allowlist. Apps using Node ESM, webpack 5, or TypeScript `moduleResolution: "bundler"`/`"node16"`/`"nodenext"` that import from undocumented internal paths need to switch to one of the supported subpaths: - .ios body { - /* global ios app variables */ - } +| Subpath | Use | +| ---------------------------------- | ----------------------------------------------------- | +| `@ionic/core` | Root entry, controllers, animation builders | +| `@ionic/core/components` | Custom-element constructors and shared utilities | +| `@ionic/core/components/ion-*.js` | Single-component custom-element constructor | +| `@ionic/core/loader` | `defineCustomElements` lazy loader | +| `@ionic/core/hydrate` | SSR hydration entry | +| `@ionic/core/css/*.css` | Global stylesheets and palettes | - .md body { - /* global md app variables */ - } -} +Apps on `moduleResolution: "node"` (classic) and webpack 4 keep resolving through the legacy fields and are unaffected. + +

Components

+ +

Input

+ +**`autocorrect` Property Type Changed to Boolean** + +The `autocorrect` property on `ion-input` is now a `boolean` and defaults to `false`. It was previously typed as `'on' | 'off'` with a default of `'off'`. This resolves a type conflict introduced when TypeScript 5.9 added `autocorrect: boolean` to the DOM `HTMLElement` interface. + +The string form no longer behaves the same way. Because an HTML attribute coerces to `true` for any non-empty string, `autocorrect="off"` now evaluates to `true` (autocorrect enabled). Migrate to the boolean property: + +- Remove the attribute to keep autocorrect disabled (the default). +- Use a property binding to enable it: `[autocorrect]="true"` (Angular), `autocorrect={true}` (React), or `:autocorrect="true"` (Vue). + +**Floating Label Behavior** + +Floating labels no longer automatically float when the input contains slotted content. Labels float only when the input is focused or has a value. + +**Internal DOM Structure Changes** + +The internal DOM structure has been reorganized to support floating labels with slotted content. + +Added: +- `.input-start` +- `.input-control` +- `.input-end` + +Restructured: +- `.label-text-wrapper` moved from `.input-wrapper` into `.input-control` +- `.native-wrapper` moved from `.input-wrapper` into `.input-control` +- Start slot moved from `.native-wrapper` into `.input-start` +- Clear button icon moved from `.native-wrapper` into `.input-end` +- End slot moved from `.native-wrapper` into `.input-end` +- `.input-control` now contains the label text and native `input`, while start/end content is separated into dedicated wrappers + +Update your selectors to account for these structural changes: + +```diff +-ion-input .input-wrapper .native-wrapper { } ++ion-input .input-control .native-wrapper { } + +-ion-input .input-wrapper .native-wrapper [slot="start"] { } ++ion-input .input-start [slot="start"] { } + +-ion-input .input-wrapper .native-wrapper .input-clear-icon { } ++ion-input .input-end .input-clear-icon { } + +-ion-input .input-wrapper .native-wrapper [slot="end"] { } ++ion-input .input-end [slot="end"] { } ``` -In Ionic Framework version 8, the dark palette is being distributed via css files that can be imported. Below is an example of importing a dark palette file in Angular: +

Legacy Picker

-```css -/* @import '@ionic/angular/css/palettes/dark.always.css'; */ -/* @import "@ionic/angular/css/palettes/dark.class.css"; */ -@import "@ionic/angular/css/palettes/dark.system.css"; +- `ion-picker-legacy` and `ion-picker-legacy-column` have been removed. The legacy picker component has been replaced with an inline picker component. + - Usages such as `ion-picker-legacy` or `IonPickerLegacy` should be changed to `ion-picker` and `IonPicker`, respectively. +- Remove any usages of `pickerController`. If using React, remove any usages of the `useIonPicker` hook. These controller-based APIs have been removed. Use the inline picker component instead. +- Remove any usages of the `PickerOptions`, `PickerButton`, `PickerColumn`, and `PickerColumnOption` type exports. These types were associated with the legacy picker and have been removed. + +

Modal

+ +The `handleBehavior` property on `ion-modal` now defaults to `"cycle"` instead of `"none"`. For sheet modals that display a handle, this means the handle is now focusable and activating it (by click, keyboard, or screen reader) cycles the sheet through its available breakpoints. This matches the native iOS sheet behavior and keeps sheet modals operable for assistive technology users by default. + +Sheet modals that relied on the handle being inert should set `handleBehavior="none"` to restore the previous behavior: + +```html + ``` -By importing the `dark.system.css` file, the dark palette variables will be defined like the following: +

Nav

-```css -@media (prefers-color-scheme: dark) { - :root { - /* global app variables */ - } +`ion-nav` no longer integrates with `ion-router`. It is now a standalone imperative stack navigation component, driven only through its own API (`root`, `push`, `pop`, `setRoot`, etc.) and `ion-nav-link`. - :root.ios { - /* global ios app variables */ - } +The following behaviors have been removed: - :root.md { - /* global md app variables */ - } -} +- The router no longer discovers or drives an `ion-nav`. Placing an `ion-nav` inside an `ion-router` no longer turns it into a routed outlet. +- Navigating an `ion-nav` (via `push`, `pop`, `ion-nav-link`, or the swipe-to-go-back gesture) no longer updates the URL, and the router's navigation guards no longer run for `ion-nav` transitions. +- The internal `setRouteId()` and `getRouteId()` methods and the `updateURL` nav option have been removed. + +Apps that relied on `ion-nav` to update the URL (for example, pushing components and expecting the browser URL to change) should use `ion-router-outlet` for URL-based routing. Keep the `ion-route` definitions and swap the outlet element: + +```diff + + + + + +- ++ ``` -Notice that the dark palette is now applied to the `:root` selector instead of the `body` selector. The [`:root`](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) selector represents the `` element and is identical to the selector `html`, except that its specificity is higher. +An `ion-nav` can still be used inside a routed page for local, URL-less stack navigation. It manages its own stack via `root` and `ion-nav-link`, and the URL never changes as you push and pop: + +```html + + + + +``` -While migrating to include the new dark palette files is unlikely to cause breaking changes, these new selectors can lead to unexpected overrides if custom CSS variables are being set on the `body` element. We recommend updating any instances where global application variables are set to target the `:root` selector instead. +

Router Outlet

-For more information on the new dark palette files, refer to the [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode). +`ion-router-outlet` now exposes a `swipeGesture` property that controls the swipe-to-go-back gesture per outlet. This property defaults to `true` in `"ios"` mode and `false` in `"md"` mode. -

Global Styles

+**`swipeBackEnabled` Config Behavior Change** -

Text Color

+In React and Vue, the `swipeBackEnabled` config option is now read once when the outlet mounts. Apps that dynamically toggle this config value at runtime should migrate to the `swipeGesture` property instead: -The `core.css` file has been updated to set the text color on the `body` element: +**React:** ```diff -body { -+ color: var(--ion-text-color); -} +- setupIonicReact({ swipeBackEnabled: someCondition }); ++ +``` + +**Vue:** + +```diff +- createApp(App).use(IonicVue, { swipeBackEnabled: someCondition }) ++ ``` -This allows components to inherit the color properly when used outside of Ionic Framework and is required for custom themes to work properly. However, it may have unintentional side effects in apps if the color was not expected to inherit. +**Disabling Swipe-to-Go-Back** -

Dynamic Font

+To disable the gesture on a specific outlet, set `swipeGesture` to `false`: -The `core.css` file has been updated to enable dynamic font scaling by default. +```tsx + +``` -The `--ion-default-dynamic-font` variable has been removed and replaced with `--ion-dynamic-font`. +The `swipeBackEnabled` config option is still respected as the initial default and does not need to change for apps that set it once at startup. -Developers who had previously chosen dynamic font scaling by activating it in their global stylesheets can revert to the default setting by removing their custom CSS. In doing so, their application will seamlessly continue utilizing dynamic font scaling as it did before. It's essential to note that altering the font-size of the html element should be avoided, as it may disrupt the proper functioning of dynamic font scaling. + -Developers who want to disable dynamic font scaling can set `--ion-dynamic-font: initial;` in their global stylesheets. However, this is not recommended because it may introduce accessibility challenges for users who depend on enlarged font sizes. +The `autocorrect` property on `ion-searchbar` is now a `boolean` and defaults to `false`. It was previously typed as `'on' | 'off'` with a default of `'off'`. This resolves a type conflict introduced when TypeScript 5.9 added `autocorrect: boolean` to the DOM `HTMLElement` interface. -For more information on the dynamic font, refer to the [Dynamic Font Scaling documentation](https://ionicframework.com/docs/layout/dynamic-font-scaling). +The string form no longer behaves the same way. Because an HTML attribute coerces to `true` for any non-empty string, `autocorrect="off"` now evaluates to `true` (autocorrect enabled). Migrate to the boolean property: -

Haptics

+- Remove the attribute to keep autocorrect disabled (the default). +- Use a property binding to enable it: `[autocorrect]="true"` (Angular), `autocorrect={true}` (React), or `:autocorrect="true"` (Vue). -- Support for the Cordova Haptics plugin has been removed. Components that integrate with haptics, such as `ion-picker` and `ion-toggle`, will continue to function but will no longer play haptics in Cordova environments. Developers should migrate to Capacitor to continue to have haptics in these components. +

Select

-

Components

+**`ionChange` Only Fires When the Value Changes** -

Button

+The `ionChange` event on `ion-select` now only fires when the selected value actually changes. Previously, the `alert` and `action-sheet` interfaces emitted `ionChange` every time the overlay was confirmed, even when the user chose the option that was already selected. This aligns the `alert` and `action-sheet` interfaces with the existing behavior of the `popover` and `modal` interfaces, and with the documented contract of `ionChange`. -- Button text now wraps by default. If this behavior is not desired, add the `ion-text-nowrap` class from the [CSS Utilities](https://ionicframework.com/docs/layout/css-utilities). +Apps that relied on `ionChange` firing on every confirmation (for example, to detect overlay dismissal without a value change) should listen for `ionDismiss` instead, or use the `didDismiss` event on the underlying alert or action sheet. -

Checkbox

+**Action Sheet Interface `selected` Role Removed** - The `legacy` property and support for the legacy syntax, which involved placing an `ion-checkbox` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy checkbox syntax, refer to the [Checkbox documentation](https://ionicframework.com/docs/api/checkbox#migrating-from-legacy-checkbox-syntax). +When using `interface="action-sheet"`, `ion-select` no longer assigns the `selected` role to the action sheet button for the currently selected option. This aligns the `action-sheet` interface with the `alert`, `popover`, and `modal` interfaces, none of which assign this role. This does not change the selected option's styling. -

Content

+Previously, the `selected` role was assigned only to the option matching the select's current value. Because the dismiss role mirrors the tapped button, this surfaced in just one case: re-selecting the already-selected option dismissed the action sheet with `role: "selected"` in `ionActionSheetDidDismiss`. Tapping any other option changed the value and dismissed with `role: ""`. Now that the role is no longer assigned, both cases dismiss with `role: undefined`. Apps that inspected this role to detect that a value was chosen, such as reading `role` from the underlying action sheet's `onDidDismiss` result, should listen for `ion-select`'s `ionChange` event instead, which emits the selected value when the selection changes. -- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host. +**Floating Label Behavior** -

Datetime

+Floating labels no longer automatically float when the select contains slotted content. Labels float only when the select is focused or has a value. Additionally, when using a floating label, the placeholder is only visible when the select is focused. -- The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`. - - Developers using the CSS variables available on `ion-item` will need to migrate their CSS to use CSS properties. For example: - ```diff - ion-datetime::part(month-year-button) { - - --background: red; +**Internal DOM Structure Changes** - + background: red; - } - ``` +The internal DOM structure has been reorganized to support floating labels with slotted content. This changes the structure and location of several exposed shadow parts. -

Input

+Added: +- `.select-start` β€” `part="start"` +- `.select-control` β€” `part="control"` +- `.select-end` β€” `part="end"` -- `size` has been removed from the `ion-input` component. Developers should use CSS to specify the visible width of the input. -- `accept` has been removed from the `ion-input` component. This was previously used in conjunction with the `type="file"`. However, the `file` value for `type` is not a valid value in Ionic Framework. -- The `legacy` property and support for the legacy syntax, which involved placing an `ion-input` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy input syntax, refer to the [Input documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax). +Removed: +- `.select-wrapper-inner` β€” `part="inner"` -

Item

+Restructured: +- `.label-text-wrapper` remains `part="label"` but moved from `.select-wrapper` into `.select-control` +- `.native-wrapper` remains `part="container"` but moved from `.select-wrapper-inner` into `.select-control` +- Start slot moved from `.select-wrapper-inner` into `.select-start` (`part="start"`) +- End slot moved from `.select-wrapper-inner` into `.select-end` (`part="end"`) +- `.select-icon` remains `part="icon"` but its location depends on the label state: + - With a start/end label, the icon is inside `.native-wrapper` + - With a floating/stacked label, the icon is inside `.select-end` -- The `helper` slot has been removed. Developers should use the `helperText` property on `ion-input` and `ion-textarea`. -- The `error` slot has been removed. Developers should use the `errorText` property on `ion-input` and `ion-textarea`. -- Counter functionality has been removed including the `counter` and `counterFormatter` properties. Developers should use the properties of the same name on `ion-input` and `ion-textarea`. -- The `fill` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`. -- The `shape` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`. -- Item no longer automatically delegates focus to the first focusable element. While most developers should not need to make any changes to account for this update, usages of `ion-item` with interactive elements such as form controls (inputs, textareas, etc) should be evaluated to verify that interactions still work as expected. +Update selectors that target the exposed shadow parts to account for the new structure: -
CSS variables
+If you currently target `part="inner"`, that part has been removed. Update those styles to target the new parts as appropriate. -The following deprecated CSS variables have been removed: `--highlight-height`, `--highlight-color-focused`, `--highlight-color-valid`, and `--highlight-color-invalid`. These variables were used on the bottom border highlight of an item when the form control inside of that item was focused. The form control syntax was [simplified in v7](https://ionic.io/blog/ionic-7-is-here#simplified-form-control-syntax) so that inputs, selects, and textareas would no longer be required to be used inside of an item. +If you target `part="label"`, `part="container"`, or `part="icon"`, the part names remain unchanged, but their position in the shadow DOM has changed. This may affect styles that depend on the relationship or layout of these parts. -If you have not yet migrated to the modern form control syntax, migration guides for each of the form controls that added a highlight to item can be found below: -- [Input migration documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax) -- [Select migration documentation](https://ionicframework.com/docs/api/select#migrating-from-legacy-select-syntax) -- [Textarea migration documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax) +Use the new `part="start"`, `part="control"`, and `part="end"` parts to target the new structural wrappers. -Once all form controls are using the modern syntax, the same variables can be used to customize them from the form control itself: +

Textarea

-| Name | Description | -| ----------------------------| ----------------------------------------| -| `--highlight-color-focused` | The color of the highlight when focused | -| `--highlight-color-invalid` | The color of the highlight when invalid | -| `--highlight-color-valid` | The color of the highlight when valid | -| `--highlight-height` | The height of the highlight indicator | +**Floating Label Behavior** -The following styles for item: +Floating labels no longer automatically float when the textarea contains slotted content. Labels float only when the textarea is focused or has a value. -```css -ion-item { - --highlight-color-focused: purple; - --highlight-color-valid: blue; - --highlight-color-invalid: orange; - --highlight-height: 6px; -} +**Internal DOM Structure Changes** + +The internal DOM structure has been reorganized to support floating labels with slotted content. + +Removed: `.textarea-wrapper-inner` + +Added: `.textarea-control` + +Renamed: +- `.start-slot-wrapper` β†’ `.textarea-start` +- `.end-slot-wrapper` β†’ `.textarea-end` + +Restructured: +- `.label-text-wrapper` moved from `.textarea-wrapper-inner` into `.textarea-control` +- `.native-wrapper` moved from `.textarea-wrapper-inner` into `.textarea-control` +- `.start-slot-wrapper` moved from `.textarea-wrapper-inner` to `.textarea-wrapper` and was renamed `.textarea-start` +- `.end-slot-wrapper` moved from `.textarea-wrapper-inner` to `.textarea-wrapper` and was renamed `.textarea-end` + +Update your selectors to account for these structural changes: + +```diff +-ion-textarea .textarea-wrapper-inner .native-wrapper { } ++ion-textarea .textarea-control .native-wrapper { } + +-ion-textarea .start-slot-wrapper [slot="start"] { } ++ion-textarea .textarea-start [slot="start"] { } + +-ion-textarea .end-slot-wrapper [slot="end"] { } ++ion-textarea .textarea-end [slot="end"] { } ``` -will instead be applied on the form controls: +**Minimum Height Change** + +The minimum height of textarea in Material Design (`md` mode) is now `72px`. At the default number of rows this makes textareas the same height regardless of the `fill` property or `labelPlacement`. Previously the minimum height was: + +| Fill | Label placement | Previous minimum height | +| --- | --- | --- | +| default | `start`, `end`, `fixed` | `44px` | +| default | `floating`, `stacked` | `56px` | +| `solid`, `outline` | any | `56px` | + +These were minimums, not the heights textareas actually rendered at. A textarea with content in the `start` or `end` slots was already taller than its minimum, so the change affects it differently. For example, a `fill="solid"` textarea with slotted icons and buttons previously rendered at `72px` with a `start` label and `81px` with a `floating` label. Both are now `72px`, so that floating label case is `9px` shorter than before rather than taller. + +Because `72px` is taller than two rows of text, `rows` values below `3` no longer change the height of the textarea in `md` mode: `rows="1"` and `rows="2"` both render at `72px`. + +If you were relying on the previous heights, or you need `rows` to control the height, override the minimum height back. The override has to be more specific than the component's own style, so a bare `ion-textarea` selector will not apply. Add a custom class to the textarea to increase specificity: ```css -ion-input, -ion-textarea, -ion-select { - --highlight-color-focused: purple; - --highlight-color-valid: blue; - --highlight-color-invalid: orange; - --highlight-height: 6px; +/* Add a custom class to the textarea */ +ion-textarea.custom { + min-height: 44px; } ``` -> [!NOTE] -> The input and textarea components are scoped, which means they will automatically scope their CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector. Targeting the `ion-input` or `ion-textarea` for customization will not work; therefore we recommend adding a class and customizing it that way. +

Framework Specific

+ +

Angular

+ +**Minimum Angular Version** + +Ionic 9 requires Angular 18 or later. Angular 16 and 17 are no longer supported. + +**Standalone Components Imported by Default** + +Following industry standards, Ionic 9 makes standalone components the default import path. Standalone component imports have changed from `@ionic/angular/standalone` to `@ionic/angular`. Lazy-loaded component imports have changed from `@ionic/angular` to `@ionic/angular/lazy`. + +**IonicModule Deprecation** + +`IonicModule` is deprecated in Ionic 9 and will be removed in a future major version. It remains fully functional in Ionic 9, so existing applications continue to work without changes. + +Applications should migrate to `provideIonicAngular()`, which works in both standalone and NgModule-based applications. For an NgModule-based app, replace `IonicModule.forRoot(config)` in the `imports` array with `provideIonicAngular(config)` in the `providers` array. Any config passed to `IonicModule.forRoot()` can be passed as an object to `provideIonicAngular()`. Refer to the [build options guide](https://ionicframework.com/docs/angular/build-options) for migration steps. + +**Zoneless Change Detection by Default** + +Ionic 9 defaults to zoneless change detection. Angular 21 bootstraps zoneless out of the box, so a new Ionic 9 app on Angular 21 runs without Zone.js and requires no change-detection provider. The `ng add @ionic/angular` schematic no longer registers `provideZoneChangeDetection()`. + +Because Zone.js no longer triggers change detection automatically, component state that you update from an asynchronous callback that Angular doesn't wrap (awaiting an overlay result such as `modal.onWillDismiss()`, `setTimeout`, RxJS subscriptions, `Platform` events) no longer re-renders on its own. Update a signal or call `ChangeDetectorRef.markForCheck()` in those callbacks. Template event bindings, `@HostListener`, reactive forms, and Ionic lifecycle hooks (`ionViewWillEnter`, etc.) that set state synchronously are unaffected. Refer to the [Zoneless Change Detection guide](https://ionicframework.com/docs/angular/zoneless) for the patterns. + +On Angular 18 through 20, Zone.js remains Angular's default, so those versions are unaffected and require no change. To adopt zoneless there, add `provideZonelessChangeDetection()` (named `provideExperimentalZonelessChangeDetection()` on Angular 18 and 19). + +**Keeping Zone.js on Angular 21 (optional)** + +To keep using Zone.js on Angular 21, opt back in with `provideZoneChangeDetection()` and keep `zone.js` in your polyfills. + +Standalone bootstrap: + +```diff + import { bootstrapApplication } from '@angular/platform-browser'; ++ import { provideZoneChangeDetection } from '@angular/core'; + + bootstrapApplication(AppComponent, { + providers: [ ++ provideZoneChangeDetection(), + // ...other providers + ], + }); +``` + +NgModule bootstrap: + +```diff + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; ++ import { provideZoneChangeDetection } from '@angular/core'; + + platformBrowserDynamic() +- .bootstrapModule(AppModule) ++ .bootstrapModule(AppModule, { ++ applicationProviders: [provideZoneChangeDetection()], ++ }) + .catch((err) => console.error(err)); +``` + +Angular forbids `provideZoneChangeDetection()` inside an NgModule's `providers` array, so for NgModule apps it must be passed as `applicationProviders` on the `bootstrapModule()` call. Both paths also require `zone.js` in your polyfills, which Angular 21's default scaffold omits: + +```ts +// src/polyfills.ts +import 'zone.js'; +``` + +**OnPush by Default on Angular 22** + +Angular 22 changes the default change detection strategy to `OnPush` for components that don't declare one. Combined with the zoneless default above, any component state that you mutate as a plain field from an Ionic lifecycle hook (`ionViewWillEnter`, etc.) no longer re-renders on its own. Run `ng update`, which migrates existing components to eager change detection and preserves the previous behavior, or use a signal (or `ChangeDetectorRef.markForCheck()`) for state set in those hooks. Ionic's own Angular components already declare `OnPush` explicitly and are unaffected. Angular 18 through 21 keep the eager default, so they require no change. + +**TypeScript** + +Ionic 9 supports TypeScript 5.4 or later, matching the minimum for Angular 18. Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later, per Angular's own requirements. + +**Node.js** + +Angular 22 raises the minimum Node.js version to `^22.22.3 || ^24.15.0 || ^26.0.0`. Angular 18 through 21 are unaffected. + +**Module Resolution** + +`@ionic/angular` is now published with `exports`-based subpath resolution. Apps using TypeScript `moduleResolution: "node"` (classic) can fail to resolve subpaths such as `@ionic/angular/lazy`. Set `moduleResolution` to `"bundler"` (the default for `ng new` on Angular 17 and later). Refer to [Package Exports](#version-9x-package-exports). + +**CSS Imports No Longer Use the `~` Prefix** + +Angular's current build pipeline no longer supports the webpack-loader `~` prefix in CSS `@import` statements: + +```diff +- @import '~@ionic/angular/css/core.css'; ++ @import '@ionic/angular/css/core.css'; +``` + +

React

+ +The `@ionic/react` and `@ionic/react-router` packages now require React 18 or 19. React 17 is no longer supported. + +The `@ionic/react-router` package now requires React Router v6. React Router v5 is no longer supported. + +**Minimum Version Requirements** +| Package | Supported Version | +| ---------------- | ----------------- | +| react | 18 or 19 | +| react-dom | 18 or 19 | +| react-router | 6.4.0+ | +| react-router-dom | 6.4.0+ | + +**TypeScript** + +The `@ionic/react` package now requires TypeScript 5.4 or later. Its type definitions use `NoInfer`, which TypeScript added in 5.4. This matches the minimum that `@ionic/angular` already requires. + +**Typed Overlay Hook Props** + +The `useIonModal` and `useIonPopover` hooks type `componentProps` against the component they are given, instead of accepting `any`. Props that do not match the component are a compile error, and `componentProps` is required when the component declares required props. Applications passing incorrect props will see new type errors at build time rather than failing at runtime. + +```diff + const Modal: React.FC<{ title: string }> = ({ title }) => {title}; + +- const [present, dismiss] = useIonModal(Modal, { subtitle: 'Wrong' }); ++ const [present, dismiss] = useIonModal(Modal, { title: 'Hello' }); +``` + +Props are read from the component rather than from `componentProps`, so a component declared inline needs its props annotated: + +```diff +- const [present, dismiss] = useIonModal(({ name }) =>
Hello {name}.
, { name: 'Dave' }); ++ const [present, dismiss] = useIonModal(({ name }: { name: string }) =>
Hello {name}.
, { name: 'Dave' }); +``` + +Passing a JSX element rather than a component is unchanged, and `componentProps` is not type checked in that case. + +`npx @ionic/migrate` reports the calls this affects and names what is wrong with each, but does not rewrite them, since the right fix depends on what the call was meant to do. For the inline case above, `--experimental` can annotate the parameter from the `componentProps` object literal being passed. + +React Router v6 introduces several API changes that will require updates to your application's routing configuration: + +**Route Definition Changes** + +The `component` prop has been replaced with the `element` prop, which accepts JSX: + +```diff +- ++ } /> +``` + +**Redirect Changes** + +The `` component has been replaced with ``: + +```diff +- import { Redirect } from 'react-router-dom'; ++ import { Navigate } from 'react-router-dom'; + +- ++ +``` + +**Nested Route Paths** + +Routes that contain nested routes or child `IonRouterOutlet` components need a `/*` suffix to match sub-paths: + +```diff +- } /> ++ } /> +``` + +**Accessing Route Parameters** -

Modal

+Route parameters are now accessed via the `useParams` hook instead of props: -- Detection for Capacitor <= 2 with applying status bar styles has been removed. Developers should ensure they are using Capacitor 3 or later when using the card modal presentation. +```diff +- import { RouteComponentProps } from 'react-router-dom'; ++ import { useParams } from 'react-router-dom'; + +- const MyComponent: React.FC> = ({ match }) => { +- const id = match.params.id; ++ const MyComponent: React.FC = () => { ++ const { id } = useParams<{ id: string }>(); +``` + +**RouteComponentProps Removed** + +The `RouteComponentProps` type and its `history`, `location`, and `match` props are no longer available in React Router v6. Use the equivalent hooks instead: + +- `history` -> `useNavigate` (see below) or `useIonRouter` +- `match.params` -> `useParams` (covered above) +- `location` -> `useLocation` -

Nav

+```diff +- import { RouteComponentProps } from 'react-router-dom'; ++ import { useNavigate, useLocation } from 'react-router-dom'; ++ import { useIonRouter } from '@ionic/react'; + +- const MyComponent: React.FC = ({ history, location }) => { +- history.push('/path'); +- history.replace('/path'); +- history.goBack(); +- console.log(location.pathname); ++ const MyComponent: React.FC = () => { ++ const navigate = useNavigate(); ++ const router = useIonRouter(); ++ const location = useLocation(); ++ // In an event handler or useEffect: ++ navigate('/path'); ++ navigate('/path', { replace: true }); ++ router.goBack(); ++ console.log(location.pathname); +``` + +**Exact Prop Removed** -- `getLength` returns `Promise` instead of ``. This method was not previously available in Nav's TypeScript interface, but developers could still access it by casting Nav as `any`. Developers should ensure they `await` their `getLength` call before accessing the returned value. +The `exact` prop is no longer needed. React Router v6 routes match exactly by default. To match sub-paths, use a `/*` suffix on the path: + +```diff +- ++ +``` -

Picker

+**Render Prop Removed** -- `ion-picker` and `ion-picker-column` have been renamed to `ion-picker-legacy` and `ion-picker-legacy-column`, respectively. This change was made to accommodate the new inline picker component while allowing developers to continue to use the legacy picker during this migration period. - - Only the component names have been changed. Usages such as `ion-picker` or `IonPicker` should be changed to `ion-picker-legacy` and `IonPickerLegacy`, respectively. - - Non-component usages such as `pickerController` or `useIonPicker` remain unchanged. The new picker displays inline with your page content and does not have equivalents for these non-component usages. +The `render` prop has been replaced with the `element` prop: -

Progress bar

+```diff +- } /> ++ } /> +``` -- The `--buffer-background` CSS variable has been removed. Use `--background` instead. +**Programmatic Navigation** -

Toast

+The `useHistory` hook has been replaced with `useNavigate`: -- `cssClass` has been removed from the `ToastButton` interface. This was previously used to apply a custom class to the toast buttons. Developers can use the "button" shadow part to style the buttons. +```diff +- import { useHistory } from 'react-router-dom'; ++ import { useNavigate } from 'react-router-dom'; ++ import { useIonRouter } from '@ionic/react'; -For more information on styling toast buttons, refer to the [Toast Theming documentation](https://ionicframework.com/docs/api/toast#theming). +- const history = useHistory(); ++ const navigate = useNavigate(); ++ const router = useIonRouter(); -

Radio

+- history.push('/path'); ++ navigate('/path'); -- The `legacy` property and support for the legacy syntax, which involved placing an `ion-radio` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy radio syntax, refer to the [Radio documentation](https://ionicframework.com/docs/api/radio#migrating-from-legacy-radio-syntax). +- history.replace('/path'); ++ navigate('/path', { replace: true }); -

Range

+- history.goBack(); ++ router.goBack(); +``` -- The `legacy` property and support for the legacy syntax, which involved placing an `ion-range` inside of an `ion-item` with an `ion-label`, have been removed. Ionic will also no longer attempt to automatically associate form controls with sibling `