diff --git a/.github/workflows/actions/publish-npm/action.yml b/.github/actions/publish-npm/action.yml similarity index 54% rename from .github/workflows/actions/publish-npm/action.yml rename to .github/actions/publish-npm/action.yml index 5c5b49d56c6..3e58ba9bcc6 100644 --- a/.github/workflows/actions/publish-npm/action.yml +++ b/.github/actions/publish-npm/action.yml @@ -8,48 +8,53 @@ inputs: tag: description: 'The tag to publish to on NPM.' preid: - description: 'The prerelease identifier used when doing a prerelease.' + description: "Prerelease identifier such as 'alpha', 'beta', 'rc', or 'next'. Leave blank to skip prerelease tagging." working-directory: description: 'The directory of the package.' folder: default: './' description: 'A folder containing a package.json file.' - token: - description: 'The NPM authentication token required to publish.' + node-version: + description: 'Node.js version to use when publishing.' + required: false + default: '24.x' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - name: 🟒 Configure Node for Publish + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: ${{ inputs.node-version }} + registry-url: 'https://registry.npmjs.org' + scope: '@ionic' # Provenance requires npm 9.5.0+ - - name: Install latest npm + - name: πŸ“¦ Install latest npm run: npm install -g npm@latest shell: bash # This ensures the local version of Lerna is installed # and that we do not use the global Lerna version - - name: Install root dependencies + - name: πŸ•ΈοΈ Install root dependencies run: npm ci shell: bash - - name: Install Dependencies + - name: πŸ“¦ Install Dependencies run: npx lerna@5 bootstrap --include-dependencies --scope ${{ inputs.scope }} --ignore-scripts -- --legacy-peer-deps shell: bash working-directory: ${{ inputs.working-directory }} - - name: Update Version - run: npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version --preid=${{ inputs.preid }} + - name: 🏷️ Set Version + run: | + if [ -z "${{ inputs.preid }}" ]; then + npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version + else + npx lerna@5 version ${{ inputs.version }} --yes --exact --no-changelog --no-push --no-git-tag-version --preid=${{ inputs.preid }} + fi shell: bash working-directory: ${{ inputs.working-directory }} - - name: Run Build + - name: πŸ—οΈ Run Build run: npm run build shell: bash working-directory: ${{ inputs.working-directory }} - - name: Prepare NPM Token - run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc - working-directory: ${{ inputs.working-directory }} - shell: bash - env: - NPM_TOKEN: ${{ inputs.token }} - - name: Publish to NPM + - name: πŸš€ Publish to NPM run: npm publish ${{ inputs.folder }} --tag ${{ inputs.tag }} --provenance shell: bash working-directory: ${{ inputs.working-directory }} + diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index c48d1dcb3b6..0bf8392c2b5 100644 --- a/.github/workflows/actions/build-angular-server/action.yml +++ b/.github/workflows/actions/build-angular-server/action.yml @@ -3,23 +3,23 @@ description: 'Build Ionic Angular Server' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install Angular Server Dependencies + - name: πŸ•ΈοΈ Install Angular Server Dependencies run: npm ci shell: bash working-directory: ./packages/angular-server - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/angular-server - - name: Build + - name: πŸ—οΈ Build run: npm run build.prod shell: bash working-directory: ./packages/angular-server diff --git a/.github/workflows/actions/build-angular/action.yml b/.github/workflows/actions/build-angular/action.yml index 349c6734e43..fc7496de421 100644 --- a/.github/workflows/actions/build-angular/action.yml +++ b/.github/workflows/actions/build-angular/action.yml @@ -3,31 +3,31 @@ description: 'Build Ionic Angular' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install Angular Dependencies + - name: πŸ•ΈοΈ Install Angular Dependencies run: npm ci shell: bash working-directory: ./packages/angular - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/angular - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/angular - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/angular - - name: Check Diff + - name: πŸ” Check Diff run: git diff --exit-code shell: bash working-directory: ./packages/angular diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index 070f84c4c3e..ef83ea5b4a6 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -8,20 +8,20 @@ inputs: runs: using: 'composite' steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - - name: Install Dependencies + - name: πŸ•ΈοΈ Install Dependencies run: npm ci working-directory: ./core shell: bash - - name: Install Stencil ${{ inputs.stencil-version }} + - name: πŸ“¦ Install Stencil ${{ inputs.stencil-version }} working-directory: ./core run: npm i @stencil/core@${{ inputs.stencil-version }} shell: bash - - name: Build Core + - name: πŸ—οΈ Build Core run: npm run build -- --ci --debug --verbose working-directory: ./core shell: bash diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index b0ec39decf8..f0deb2bada0 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -8,22 +8,22 @@ inputs: runs: using: 'composite' steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x - - name: Install Dependencies + node-version: 24.x + - name: πŸ•ΈοΈ Install Dependencies run: npm install working-directory: ./core shell: bash # If an Ionicons version was specified install that. # Otherwise just use the version defined in the package.json. - - name: Install Ionicons Version + - name: πŸ“¦ Install Ionicons Version if: inputs.ionicons-version != '' run: npm install ionicons@${{ inputs.ionicons-version }} working-directory: ./core shell: bash - - name: Build Core + - name: πŸ—οΈ Build Core run: npm run build -- --ci working-directory: ./core shell: bash diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index 61d5f6b2d45..fd997ea2386 100644 --- a/.github/workflows/actions/build-react-router/action.yml +++ b/.github/workflows/actions/build-react-router/action.yml @@ -3,9 +3,9 @@ description: 'Build Ionic React Router' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -16,19 +16,19 @@ runs: name: ionic-react path: ./packages/react filename: ReactBuild.zip - - name: Install Dependencies + - name: πŸ•ΈοΈ Install Dependencies run: npm ci shell: bash working-directory: ./packages/react-router - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/react-router - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/react-router - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/react-router diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index 6b8b9f74178..5de023a258f 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -3,35 +3,35 @@ description: 'Build Ionic React' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install React Dependencies + - name: πŸ•ΈοΈ Install React Dependencies run: npm ci shell: bash working-directory: ./packages/react - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/react - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/react - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/react - - name: Test Spec + - name: πŸ§ͺ Test Spec run: npm run test.spec shell: bash working-directory: ./packages/react - - name: Check Diff + - name: πŸ” Check Diff run: git diff --exit-code shell: bash working-directory: ./packages/react diff --git a/.github/workflows/actions/build-vue-router/action.yml b/.github/workflows/actions/build-vue-router/action.yml index e1c7716f5ea..b2e375bb7ae 100644 --- a/.github/workflows/actions/build-vue-router/action.yml +++ b/.github/workflows/actions/build-vue-router/action.yml @@ -3,9 +3,9 @@ description: 'Builds Ionic Vue Router' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -16,23 +16,23 @@ runs: name: ionic-vue path: ./packages/vue filename: VueBuild.zip - - name: Install Vue Router Dependencies + - name: πŸ•ΈοΈ Install Vue Router Dependencies run: npm ci shell: bash working-directory: ./packages/vue-router - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/vue-router - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/vue-router - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/vue-router - - name: Test Spec + - name: πŸ§ͺ Test Spec run: npm run test.spec shell: bash working-directory: ./packages/vue-router diff --git a/.github/workflows/actions/build-vue/action.yml b/.github/workflows/actions/build-vue/action.yml index bc8a47facc2..317f6f124ab 100644 --- a/.github/workflows/actions/build-vue/action.yml +++ b/.github/workflows/actions/build-vue/action.yml @@ -3,31 +3,31 @@ description: 'Build Ionic Vue' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install Vue Dependencies + - name: πŸ•ΈοΈ Install Vue Dependencies run: npm ci shell: bash working-directory: ./packages/vue - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/vue - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/vue - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/vue - - name: Check Diff + - name: πŸ” Check Diff run: git diff --exit-code shell: bash working-directory: ./packages/vue diff --git a/.github/workflows/actions/download-archive/action.yml b/.github/workflows/actions/download-archive/action.yml index 343e2451bce..70f201e3d67 100644 --- a/.github/workflows/actions/download-archive/action.yml +++ b/.github/workflows/actions/download-archive/action.yml @@ -10,10 +10,10 @@ inputs: runs: using: 'composite' steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: ${{ inputs.name }} path: ${{ inputs.path }} - - name: Extract Archive + - name: πŸ”Ž Extract Archive run: unzip -q -o ${{ inputs.path }}/${{ inputs.filename }} shell: bash diff --git a/.github/workflows/actions/test-angular-e2e/action.yml b/.github/workflows/actions/test-angular-e2e/action.yml index cd7ebfe0aec..0b99e99f1e2 100644 --- a/.github/workflows/actions/test-angular-e2e/action.yml +++ b/.github/workflows/actions/test-angular-e2e/action.yml @@ -6,9 +6,9 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -24,23 +24,23 @@ runs: name: ionic-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 }} diff --git a/.github/workflows/actions/test-core-clean-build/action.yml b/.github/workflows/actions/test-core-clean-build/action.yml index ea6da763fd9..1f78cfaef74 100644 --- a/.github/workflows/actions/test-core-clean-build/action.yml +++ b/.github/workflows/actions/test-core-clean-build/action.yml @@ -3,16 +3,16 @@ description: 'Test Core Clean Build' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Check Diff + - name: πŸ” Check Diff run: | git diff --exit-code || { echo -e "\033[1;31m⚠️ Error: Differences Detected ⚠️\033[0m" diff --git a/.github/workflows/actions/test-core-lint/action.yml b/.github/workflows/actions/test-core-lint/action.yml index b0e45abdaea..35fc84da48a 100644 --- a/.github/workflows/actions/test-core-lint/action.yml +++ b/.github/workflows/actions/test-core-lint/action.yml @@ -3,21 +3,21 @@ description: 'Test Core Lint' runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x - - name: Install Dependencies + node-version: 24.x + - name: πŸ•ΈοΈ Install Dependencies run: npm ci working-directory: ./core shell: bash - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./core # Lint changes should be pushed # to the branch before the branch # is merge eligible. - - name: Check Lint Results + - name: πŸ”Ž Check Lint Results run: git diff --exit-code shell: bash working-directory: ./core diff --git a/.github/workflows/actions/test-core-screenshot/action.yml b/.github/workflows/actions/test-core-screenshot/action.yml index f3d599f02ca..0c3965d632d 100644 --- a/.github/workflows/actions/test-core-screenshot/action.yml +++ b/.github/workflows/actions/test-core-screenshot/action.yml @@ -13,19 +13,19 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install Dependencies + - name: πŸ•ΈοΈ Install Dependencies run: npm install shell: bash working-directory: ./core - - name: Test + - name: πŸ§ͺ Test if: inputs.update != 'true' run: npm run test.e2e.docker.ci ${{ inputs.component }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }} shell: bash @@ -60,13 +60,13 @@ runs: fi shell: bash working-directory: ./core - - name: Archive Updated Screenshots + - name: πŸ“¦ Archive Updated Screenshots if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }} path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip - - name: Archive Test Results + - name: πŸ“¦ Archive Test Results # The always() ensures that this step # runs even if the previous step fails. # We want the test results to be archived diff --git a/.github/workflows/actions/test-core-spec/action.yml b/.github/workflows/actions/test-core-spec/action.yml index cdec48fabff..af0684d243e 100644 --- a/.github/workflows/actions/test-core-spec/action.yml +++ b/.github/workflows/actions/test-core-spec/action.yml @@ -6,14 +6,14 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x - - name: Install Dependencies + node-version: 24.x + - name: πŸ•ΈοΈ Install Dependencies run: npm ci working-directory: ./core shell: bash - - name: Install Stencil ${{ inputs.stencil-version }} + - name: πŸ“¦ Install Stencil ${{ inputs.stencil-version }} run: npm install @stencil/core@${{ inputs.stencil-version }} shell: bash working-directory: ./core @@ -23,7 +23,7 @@ runs: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Test + - name: πŸ§ͺ Test run: npm run test.spec -- --ci shell: bash working-directory: ./core diff --git a/.github/workflows/actions/test-react-e2e/action.yml b/.github/workflows/actions/test-react-e2e/action.yml index 3cf40c29b86..ad5148d9151 100644 --- a/.github/workflows/actions/test-react-e2e/action.yml +++ b/.github/workflows/actions/test-react-e2e/action.yml @@ -6,9 +6,9 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -24,23 +24,23 @@ runs: name: ionic-react-router path: ./packages/react-router filename: ReactRouterBuild.zip - - name: Create Test App + - name: πŸ§ͺ Create Test App run: ./build.sh ${{ inputs.app }} shell: bash working-directory: ./packages/react/test - - name: Install Dependencies + - name: πŸ•ΈοΈ Install Dependencies run: npm install shell: bash working-directory: ./packages/react/test/build/${{ inputs.app }} - - name: Sync Built Changes + - name: πŸ”„ Sync Built Changes run: npm run sync shell: bash working-directory: ./packages/react/test/build/${{ inputs.app }} - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/react/test/build/${{ inputs.app }} - - name: Run Tests + - name: πŸ§ͺ Run Tests run: npm run e2e shell: bash working-directory: ./packages/react/test/build/${{ inputs.app }} diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml index f1f0150de11..784e354465a 100644 --- a/.github/workflows/actions/test-react-router-e2e/action.yml +++ b/.github/workflows/actions/test-react-router-e2e/action.yml @@ -6,9 +6,9 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -24,23 +24,23 @@ runs: name: ionic-react-router path: ./packages/react-router filename: ReactRouterBuild.zip - - name: Create Test App + - name: πŸ§ͺ Create Test App run: ./build.sh ${{ inputs.app }} shell: bash working-directory: ./packages/react-router/test - - name: Install Dependencies + - name: πŸ•ΈοΈ Install Dependencies run: npm install shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} - - name: Sync Built Changes + - name: πŸ”„ Sync Built Changes run: npm run sync shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} - - name: Run Tests + - name: πŸ§ͺ Run Tests run: npm run e2e shell: bash working-directory: ./packages/react-router/test/build/${{ inputs.app }} diff --git a/.github/workflows/actions/test-vue-e2e/action.yml b/.github/workflows/actions/test-vue-e2e/action.yml index 905cb319a7f..228732067e9 100644 --- a/.github/workflows/actions/test-vue-e2e/action.yml +++ b/.github/workflows/actions/test-vue-e2e/action.yml @@ -6,9 +6,9 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core @@ -24,23 +24,23 @@ runs: name: ionic-vue-router path: ./packages/vue-router filename: VueRouterBuild.zip - - name: Create Test App + - name: πŸ§ͺ Create Test App run: ./build.sh ${{ inputs.app }} shell: bash working-directory: ./packages/vue/test - - name: Install Dependencies + - name: πŸ“¦ Install Dependencies run: npm install shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} - - name: Run Spec Tests + - name: πŸ§ͺ Run Spec Tests run: npm run test:unit shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} - - name: Run E2E Tests + - name: πŸ§ͺ Run E2E Tests run: npm run test:e2e shell: bash working-directory: ./packages/vue/test/build/${{ inputs.app }} diff --git a/.github/workflows/actions/update-reference-screenshots/action.yml b/.github/workflows/actions/update-reference-screenshots/action.yml index 95d0c7b726b..e6f0aa817a3 100644 --- a/.github/workflows/actions/update-reference-screenshots/action.yml +++ b/.github/workflows/actions/update-reference-screenshots/action.yml @@ -7,13 +7,13 @@ on: runs: using: 'composite' steps: - - uses: actions/setup-node@v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22.x - - uses: actions/download-artifact@v5 + node-version: 24.x + - uses: actions/download-artifact@v6 with: path: ./artifacts - - name: Extract Archives + - name: πŸ”Ž Extract Archives # This finds all .zip files in the ./artifacts # directory, including nested directories. # It then unzips every .zip to the root directory @@ -21,7 +21,7 @@ runs: find . -type f -name 'UpdatedScreenshots-*.zip' -exec unzip -q -o -d ../ {} \; shell: bash working-directory: ./artifacts - - name: Push Screenshots + - name: πŸ“Έ Push Screenshots # Configure user as Ionitron # and push only the changed .png snapshots # to the remote branch. diff --git a/.github/workflows/actions/upload-archive/action.yml b/.github/workflows/actions/upload-archive/action.yml index 966b80e3a00..e836e84c82d 100644 --- a/.github/workflows/actions/upload-archive/action.yml +++ b/.github/workflows/actions/upload-archive/action.yml @@ -10,10 +10,10 @@ inputs: runs: using: 'composite' steps: - - name: Create Archive + - name: πŸ—„οΈ Create Archive run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }} shell: bash - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: ${{ inputs.name }} path: ${{ inputs.output }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4800023a635..3e0ff766db6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: build-core: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-core with: ionicons-version: ${{ inputs.ionicons_npm_release_tag }} @@ -31,21 +31,21 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-clean-build test-core-lint: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-lint test-core-spec: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-spec test-core-screenshot: @@ -62,7 +62,7 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-screenshot with: shard: ${{ matrix.shard }} @@ -90,14 +90,14 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-vue build-vue-router: needs: [build-vue] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-vue-router test-vue-e2e: @@ -108,7 +108,7 @@ jobs: needs: [build-vue, build-vue-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-vue-e2e with: app: ${{ matrix.apps }} @@ -126,14 +126,14 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-angular build-angular-server: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-angular-server test-angular-e2e: @@ -144,7 +144,7 @@ jobs: needs: [build-angular, build-angular-server] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-angular-e2e with: app: ${{ matrix.apps }} @@ -162,14 +162,14 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-react build-react-router: needs: [build-react] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-react-router test-react-router-e2e: @@ -180,7 +180,7 @@ jobs: needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-react-router-e2e with: app: ${{ matrix.apps }} @@ -202,7 +202,7 @@ jobs: needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-react-e2e with: app: ${{ matrix.apps }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6ac78c8dc83..1e5f1cad4b4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,8 +14,8 @@ jobs: permissions: security-events: write steps: - - uses: actions/checkout@v5 - - uses: github/codeql-action/init@v3 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: github/codeql-action/init@v4 with: languages: javascript - - uses: github/codeql-action/analyze@v3 + - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 20746438552..15861814e9d 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -1,7 +1,11 @@ name: 'Ionic Dev Build' on: - workflow_dispatch: + workflow_call: + +permissions: + contents: read + id-token: write jobs: create-dev-hash: @@ -9,7 +13,7 @@ jobs: outputs: dev-hash: ${{ steps.create-dev-hash.outputs.DEV_HASH }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # A 1 is required before the timestamp # as lerna will fail when there is a leading 0 # See https://github.com/lerna/lerna/issues/2840 @@ -25,13 +29,12 @@ jobs: release-ionic: needs: [create-dev-hash] permissions: + contents: read id-token: write uses: ./.github/workflows/release-ionic.yml with: tag: dev version: ${{ needs.create-dev-hash.outputs.dev-hash }} - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} get-build: name: Get your dev build! diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a0f75e0db6a..e44ba2d7516 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,10 +1,11 @@ name: 'Ionic Nightly Build' on: - schedule: - # Run every Monday-Friday - # at 6:00 UTC (6:00 am UTC) - - cron: '00 06 * * 1-5' + workflow_call: + +permissions: + contents: read + id-token: write jobs: create-nightly-hash: @@ -12,7 +13,7 @@ jobs: outputs: nightly-hash: ${{ steps.create-nightly-hash.outputs.NIGHTLY_HASH }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # A 1 is required before the timestamp # as lerna will fail when there is a leading 0 # See https://github.com/lerna/lerna/issues/2840 @@ -30,10 +31,10 @@ jobs: release-ionic: needs: [create-nightly-hash] permissions: + contents: read id-token: write uses: ./.github/workflows/release-ionic.yml + secrets: inherit with: tag: nightly version: ${{ needs.create-nightly-hash.outputs.nightly-hash }} - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-ionic.yml b/.github/workflows/release-ionic.yml index 16baa3384c0..b4470041ffb 100644 --- a/.github/workflows/release-ionic.yml +++ b/.github/workflows/release-ionic.yml @@ -14,23 +14,23 @@ on: preid: description: 'The prerelease identifier used when doing a prerelease.' type: string - secrets: - NPM_TOKEN: - required: true + +permissions: + contents: read + id-token: write jobs: release-core: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: ./.github/workflows/actions/publish-npm + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: ./.github/actions/publish-npm with: scope: '@ionic/core' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'core' - token: ${{ secrets.NPM_TOKEN }} - name: Cache Built @ionic/core uses: ./.github/workflows/actions/upload-archive with: @@ -48,34 +48,33 @@ jobs: needs: [release-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/docs built cache uses: ./.github/workflows/actions/download-archive with: name: ionic-docs path: ./packages/docs filename: DocsBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/docs' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/docs' - token: ${{ secrets.NPM_TOKEN }} release-angular: needs: [release-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/angular' tag: ${{ inputs.tag }} @@ -83,7 +82,6 @@ jobs: preid: ${{ inputs.preid }} working-directory: 'packages/angular' folder: './dist' - token: ${{ secrets.NPM_TOKEN }} - name: Cache Built @ionic/angular uses: ./.github/workflows/actions/upload-archive with: @@ -95,21 +93,20 @@ jobs: needs: [release-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/react' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/react' - token: ${{ secrets.NPM_TOKEN }} - name: Cache Built @ionic/react uses: ./.github/workflows/actions/upload-archive with: @@ -121,21 +118,20 @@ jobs: needs: [release-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/vue' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/vue' - token: ${{ secrets.NPM_TOKEN }} - name: Cache Built @ionic/vue uses: ./.github/workflows/actions/upload-archive with: @@ -147,14 +143,14 @@ jobs: needs: [release-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/angular-server' tag: ${{ inputs.tag }} @@ -162,13 +158,12 @@ jobs: preid: ${{ inputs.preid }} working-directory: 'packages/angular-server' folder: './dist' - token: ${{ secrets.NPM_TOKEN }} release-react-router: needs: [release-react] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: @@ -181,20 +176,19 @@ jobs: name: ionic-react path: ./packages/react filename: ReactBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/react-router' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/react-router' - token: ${{ secrets.NPM_TOKEN }} release-vue-router: needs: [release-vue] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore @ionic/core built cache uses: ./.github/workflows/actions/download-archive with: @@ -207,11 +201,10 @@ jobs: name: ionic-vue path: ./packages/vue filename: VueBuild.zip - - uses: ./.github/workflows/actions/publish-npm + - uses: ./.github/actions/publish-npm with: scope: '@ionic/vue-router' tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} working-directory: 'packages/vue-router' - token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-orchestrator.yml b/.github/workflows/release-orchestrator.yml new file mode 100644 index 00000000000..2c41ed516f9 --- /dev/null +++ b/.github/workflows/release-orchestrator.yml @@ -0,0 +1,81 @@ +name: 'Release - Ionic Framework' + +on: + schedule: + # Run every Monday-Friday + # at 6:00 UTC (6:00 am UTC) + - cron: '00 06 * * 1-5' + workflow_dispatch: + inputs: + release-type: + description: 'Which Ionic release workflow should run?' + required: true + type: choice + default: dev + options: + - dev + - production + version: + description: 'Which version should be published? (Only for production releases)' + required: false + type: choice + options: + - patch + - minor + - major + - prepatch + - preminor + - premajor + - prerelease + tag: + description: 'Which npm tag should this be published to? (Only for production releases)' + required: false + type: choice + default: latest + options: + - latest + - next + preid: + description: 'Which prerelease identifier should be used? (Only for production releases)' + required: false + type: choice + default: '' + options: + - '' + - alpha + - beta + - rc + - next + +permissions: + contents: read + id-token: write + +jobs: + run-nightly: + if: ${{ github.event_name == 'schedule' }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/nightly.yml + secrets: inherit + + run-dev: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'dev' }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/dev-build.yml + secrets: inherit + + run-production: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'production' }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/release.yml + secrets: inherit + with: + version: ${{ inputs.version }} + tag: ${{ inputs.tag }} + preid: ${{ inputs.preid }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 084c8af3077..650486bb981 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,54 +1,61 @@ name: 'Ionic Production Release' on: - workflow_dispatch: + workflow_call: inputs: version: + description: 'Which version should be published?' required: true - type: choice - description: Which version should be published? - options: - - patch - - minor - - major - - prepatch - - preminor - - premajor - - prerelease + type: string tag: + description: 'Which npm tag should this be published to?' required: true - type: choice - description: Which npm tag should this be published to? - options: - - latest - - next + type: string preid: - type: choice - description: Which prerelease identifier should be used? This is only needed when version is "prepatch", "preminor", "premajor", or "prerelease". - options: - - '' - - alpha - - beta - - rc - - next + description: 'Which prerelease identifier should be used? This is only needed when version is "prepatch", "preminor", "premajor", or "prerelease".' + required: false + type: string + +permissions: + contents: read + id-token: write jobs: + validate_version: + name: βœ… Validate Version Input + runs-on: ubuntu-latest + steps: + - name: πŸ”Ž Ensure version is allowed + env: + VERSION: ${{ inputs.version }} + run: | + case "$VERSION" in + patch|minor|major|prepatch|preminor|premajor|prerelease) + exit 0 + ;; + *) + echo "::error::Invalid version input: '$VERSION'. Allowed values: patch, minor, major, prepatch, preminor, premajor, prerelease." + exit 1 + ;; + esac + shell: bash + release-ionic: + needs: [validate_version] permissions: + contents: read id-token: write uses: ./.github/workflows/release-ionic.yml with: tag: ${{ inputs.tag }} version: ${{ inputs.version }} preid: ${{ inputs.preid }} - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} finalize-release: needs: [release-ionic] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: token: ${{ secrets.IONITRON_TOKEN }} fetch-depth: 0 @@ -76,7 +83,7 @@ jobs: needs: [finalize-release] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Pull the latest version of the reference # branch instead of the revision that triggered # the workflow otherwise we won't get the commit diff --git a/.github/workflows/stencil-nightly.yml b/.github/workflows/stencil-nightly.yml index 6b24a9d69bb..653d4060637 100644 --- a/.github/workflows/stencil-nightly.yml +++ b/.github/workflows/stencil-nightly.yml @@ -26,7 +26,7 @@ jobs: build-core-with-stencil-nightly: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-core-stencil-prerelease with: stencil-version: ${{ inputs.npm_release_tag || 'nightly' }} @@ -35,21 +35,21 @@ jobs: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-clean-build test-core-lint: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-lint test-core-spec: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-spec with: stencil-version: ${{ inputs.npm_release_tag || 'nightly' }} @@ -72,7 +72,7 @@ jobs: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-screenshot with: shard: ${{ matrix.shard }} @@ -100,14 +100,14 @@ jobs: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-vue build-vue-router: needs: [build-vue] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-vue-router test-vue-e2e: @@ -118,7 +118,7 @@ jobs: needs: [build-vue, build-vue-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-vue-e2e with: app: ${{ matrix.apps }} @@ -136,14 +136,14 @@ jobs: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-angular build-angular-server: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-angular-server test-angular-e2e: @@ -154,7 +154,7 @@ jobs: needs: [build-angular, build-angular-server] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-angular-e2e with: app: ${{ matrix.apps }} @@ -172,14 +172,14 @@ jobs: needs: [build-core-with-stencil-nightly] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-react build-react-router: needs: [build-react] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-react-router test-react-router-e2e: @@ -190,7 +190,7 @@ jobs: needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-react-router-e2e with: app: ${{ matrix.apps }} @@ -212,7 +212,7 @@ jobs: needs: [build-react, build-react-router] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-react-e2e with: app: ${{ matrix.apps }} @@ -225,3 +225,35 @@ jobs: - name: Check build matrix status if: ${{ needs.test-react-e2e.result != 'success' }} run: exit 1 + + send-success-messages: + needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e] + runs-on: ubuntu-latest + if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }} + steps: + - name: Notify success on Discord + run: | + curl -H "Content-Type:application/json" \ + -d '{"embeds": [{"title": "βœ… Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "color": 65280, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \ + ${{secrets.DISCORD_NOTIFY_WEBHOOK}} + - name: Notify success on Slack + run: | + curl -H "Content-Type:application/json" \ + -d '{"title": "βœ… Workflow ${{github.workflow}} #${{github.run_number}} finished successfully", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \ + ${{secrets.SLACK_NOTIFY_SUCCESS_WEBHOOK}} + + send-failure-messages: + needs: [test-core-clean-build, test-core-lint, test-core-spec, verify-screenshots, verify-test-vue-e2e, verify-test-angular-e2e, verify-test-react-router-e2e, verify-test-react-e2e] + runs-on: ubuntu-latest + if: ${{ !cancelled() && contains(needs.*.result, 'failure') }} + steps: + - name: Notify failure on Discord + run: | + curl -H "Content-Type:application/json" \ + -d '{"content": "Alerting <@&1347593178580254761>!", "embeds": [{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "color": 16711680, "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}]}' \ + ${{secrets.DISCORD_NOTIFY_WEBHOOK}} + - name: Notify failure on Slack + run: | + curl -H "Content-Type:application/json" \ + -d '{"title": "❌ Workflow ${{github.workflow}} #${{github.run_number}} failed", "url": "${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}"}' \ + ${{secrets.SLACK_NOTIFY_FAILURE_WEBHOOK}} diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index ef5dcf31347..967fd3965c4 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -26,7 +26,7 @@ jobs: build-core: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/build-core test-core-screenshot: @@ -47,7 +47,7 @@ jobs: needs: [build-core] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: ./.github/workflows/actions/test-core-screenshot with: shard: ${{ matrix.shard }} @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest needs: [test-core-screenshot] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Normally, we could just push with the # default GITHUB_TOKEN, but that will # not cause the build workflow diff --git a/CHANGELOG.md b/CHANGELOG.md index 47210c4bf27..5aca45ac369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,63 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + + +### Bug Fixes + +* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2)) + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + + +### Bug Fixes + +* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613) + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + + +### Bug Fixes + +* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab)) + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + + +### Bug Fixes + +* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b)) +* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326) +* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367)) + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + + +### Bug Fixes + +* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611) + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 5be7265795b..d19d8deea27 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,63 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + + +### Bug Fixes + +* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2)) + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + + +### Bug Fixes + +* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613) + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + + +### Bug Fixes + +* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab)) + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + + +### Bug Fixes + +* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b)) +* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326) +* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367)) + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + + +### Bug Fixes + +* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611) + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) diff --git a/core/Dockerfile b/core/Dockerfile index 095cde63a4a..50a4687701c 100644 --- a/core/Dockerfile +++ b/core/Dockerfile @@ -1,5 +1,5 @@ # Get Playwright -FROM mcr.microsoft.com/playwright:v1.55.1 +FROM mcr.microsoft.com/playwright:v1.56.1 # Set the working directory WORKDIR /ionic diff --git a/core/package-lock.json b/core/package-lock.json index 92433ef97d7..17febb498f2 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { "@phosphor-icons/core": "^2.1.1", @@ -15,7 +15,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@axe-core/playwright": "^4.10.2", + "@axe-core/playwright": "^4.11.0", "@capacitor/core": "^7.0.0", "@capacitor/haptics": "^7.0.0", "@capacitor/keyboard": "^7.0.0", @@ -23,7 +23,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.55.1", + "@playwright/test": "^1.56.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", "@stencil/angular-output-target": "^0.10.0", @@ -59,12 +59,13 @@ "dev": true }, "node_modules/@axe-core/playwright": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz", - "integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz", + "integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==", "dev": true, + "license": "MPL-2.0", "dependencies": { - "axe-core": "~4.10.3" + "axe-core": "~4.11.0" }, "peerDependencies": { "playwright-core": ">= 1.0.0" @@ -96,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.8", @@ -746,6 +748,7 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.0.tgz", "integrity": "sha512-P6NnjoHyobZgTjynlZSn27d0SUj6j38inlNxFnKZr9qwU7/r6+0Sg2nWkGkIH/pMmXHsvGD8zVe6KUq1UncIjA==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -806,6 +809,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -822,6 +826,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -838,6 +843,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -854,6 +860,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -886,6 +893,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -902,6 +910,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -918,6 +927,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -934,6 +944,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -950,6 +961,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -966,6 +978,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -982,6 +995,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -998,6 +1012,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1014,6 +1029,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1030,6 +1046,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1046,6 +1063,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1062,6 +1080,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1078,6 +1097,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1094,6 +1114,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1110,6 +1131,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1126,6 +1148,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1142,6 +1165,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -1158,6 +1182,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -1174,6 +1199,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1190,6 +1216,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1206,6 +1233,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1412,6 +1440,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -2351,13 +2380,13 @@ "integrity": "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==" }, "node_modules/@playwright/test": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", - "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -2555,6 +2584,7 @@ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", + "peer": true, "bin": { "stencil": "bin/stencil" }, @@ -3022,6 +3052,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.2.tgz", "integrity": "sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.7.2", "@typescript-eslint/types": "6.7.2", @@ -3258,7 +3289,6 @@ "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.25.3", "@vue/shared": "3.5.13", @@ -3273,7 +3303,6 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -3286,8 +3315,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vue/compiler-dom": { "version": "3.5.13", @@ -3295,7 +3323,6 @@ "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-core": "3.5.13", "@vue/shared": "3.5.13" @@ -3307,7 +3334,6 @@ "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.25.3", "@vue/compiler-core": "3.5.13", @@ -3325,8 +3351,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vue/compiler-sfc/node_modules/postcss": { "version": "8.5.3", @@ -3348,7 +3373,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -3364,7 +3388,6 @@ "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13" @@ -3376,7 +3399,6 @@ "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.13" } @@ -3387,7 +3409,6 @@ "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.13", "@vue/shared": "3.5.13" @@ -3399,7 +3420,6 @@ "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.13", "@vue/runtime-core": "3.5.13", @@ -3413,7 +3433,6 @@ "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13" @@ -3427,8 +3446,7 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@zeit/schemas": { "version": "2.21.0", @@ -3465,6 +3483,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3713,10 +3732,11 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -4852,8 +4872,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "2.6.9", @@ -5287,6 +5306,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -8988,7 +9008,6 @@ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -9376,7 +9395,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -9903,13 +9921,13 @@ } }, "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -9922,11 +9940,12 @@ } }, "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -9948,6 +9967,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", "dev": true, + "peer": true, "dependencies": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -10063,6 +10083,7 @@ "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", "dev": true, + "peer": true, "peerDependencies": { "postcss": ">=5.0.0" } @@ -10116,6 +10137,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -10530,6 +10552,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.35.1.tgz", "integrity": "sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==", "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -10864,7 +10887,6 @@ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11667,7 +11689,8 @@ "node_modules/tslib": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/core/package.json b/core/package.json index 3956fcfaf94..0abffb958bc 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.7.5", + "version": "8.7.10", "description": "Base components for Ionic", "keywords": [ "ionic", @@ -37,7 +37,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@axe-core/playwright": "^4.10.2", + "@axe-core/playwright": "^4.11.0", "@capacitor/core": "^7.0.0", "@capacitor/haptics": "^7.0.0", "@capacitor/keyboard": "^7.0.0", @@ -45,7 +45,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.55.1", + "@playwright/test": "^1.56.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", "@stencil/angular-output-target": "^0.10.0", diff --git a/core/src/components/accordion/accordion.tsx b/core/src/components/accordion/accordion.tsx index 6321c581851..4f76c035e7f 100644 --- a/core/src/components/accordion/accordion.tsx +++ b/core/src/components/accordion/accordion.tsx @@ -42,7 +42,40 @@ const enum AccordionState { }) export class Accordion implements ComponentInterface { private accordionGroupEl?: HTMLIonAccordionGroupElement | null; - private updateListener = () => this.updateState(false); + private accordionGroupUpdateHandler = () => { + /** + * Determine if this update will cause an actual state change. + * We only want to mark as "interacted" if the state is changing. + */ + const accordionGroup = this.accordionGroupEl; + if (accordionGroup) { + const value = accordionGroup.value; + const accordionValue = this.value; + const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue; + const isExpanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding; + const stateWillChange = shouldExpand !== isExpanded; + + /** + * Only mark as interacted if: + * 1. This is not the first update we've received with a defined value + * 2. The state is actually changing (prevents redundant updates from enabling animations) + */ + if (this.hasReceivedFirstUpdate && stateWillChange) { + this.hasInteracted = true; + } + + /** + * Only count this as the first update if the group value is defined. + * This prevents the initial undefined value from the group's componentDidLoad + * from being treated as the first real update. + */ + if (value !== undefined) { + this.hasReceivedFirstUpdate = true; + } + } + + this.updateState(); + }; private contentEl: HTMLDivElement | undefined; private contentElWrapper: HTMLDivElement | undefined; private headerEl: HTMLDivElement | undefined; @@ -54,6 +87,25 @@ export class Accordion implements ComponentInterface { @State() state: AccordionState = AccordionState.Collapsed; @State() isNext = false; @State() isPrevious = false; + /** + * Tracks whether a user-initiated interaction has occurred. + * Animations are disabled until the first interaction happens. + * This prevents the accordion from animating when it's programmatically + * set to an expanded or collapsed state on initial load. + */ + @State() hasInteracted = false; + + /** + * Tracks if this accordion has ever been expanded. + * Used to prevent the first expansion from animating. + */ + private hasEverBeenExpanded = false; + + /** + * Tracks if this accordion has received its first update from the group. + * Used to distinguish initial programmatic sets from user interactions. + */ + private hasReceivedFirstUpdate = false; /** * The value of the accordion. Defaults to an autogenerated @@ -92,15 +144,15 @@ export class Accordion implements ComponentInterface { connectedCallback() { const accordionGroupEl = (this.accordionGroupEl = this.el?.closest('ion-accordion-group')); if (accordionGroupEl) { - this.updateState(true); - addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener); + this.updateState(); + addEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler); } } disconnectedCallback() { const accordionGroupEl = this.accordionGroupEl; if (accordionGroupEl) { - removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener); + removeEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler); } } @@ -237,10 +289,16 @@ export class Accordion implements ComponentInterface { ionItem.appendChild(iconEl); }; - private expandAccordion = (initialUpdate = false) => { + private expandAccordion = () => { const { contentEl, contentElWrapper } = this; - if (initialUpdate || contentEl === undefined || contentElWrapper === undefined) { + + /** + * If the content elements aren't available yet, just set the state. + * This happens on initial render before the DOM is ready. + */ + if (contentEl === undefined || contentElWrapper === undefined) { this.state = AccordionState.Expanded; + this.hasEverBeenExpanded = true; return; } @@ -252,6 +310,12 @@ export class Accordion implements ComponentInterface { cancelAnimationFrame(this.currentRaf); } + /** + * Mark that this accordion has been expanded at least once. + * This allows subsequent expansions to animate. + */ + this.hasEverBeenExpanded = true; + if (this.shouldAnimate()) { raf(() => { this.state = AccordionState.Expanding; @@ -272,9 +336,14 @@ export class Accordion implements ComponentInterface { } }; - private collapseAccordion = (initialUpdate = false) => { + private collapseAccordion = () => { const { contentEl } = this; - if (initialUpdate || contentEl === undefined) { + + /** + * If the content element isn't available yet, just set the state. + * This happens on initial render before the DOM is ready. + */ + if (contentEl === undefined) { this.state = AccordionState.Collapsed; return; } @@ -316,6 +385,19 @@ export class Accordion implements ComponentInterface { * of what is set in the config. */ private shouldAnimate = () => { + /** + * Don't animate until after the first user interaction. + * This prevents animations on initial load when accordions + * start in an expanded or collapsed state programmatically. + * + * Additionally, don't animate the very first expansion even if + * hasInteracted is true. This handles edge cases like React StrictMode + * where effects run twice and might incorrectly mark as interacted. + */ + if (!this.hasInteracted || !this.hasEverBeenExpanded) { + return false; + } + if (typeof (window as any) === 'undefined') { return false; } @@ -337,7 +419,7 @@ export class Accordion implements ComponentInterface { return true; }; - private updateState = async (initialUpdate = false) => { + private updateState = async () => { const accordionGroup = this.accordionGroupEl; const accordionValue = this.value; @@ -350,10 +432,10 @@ export class Accordion implements ComponentInterface { const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue; if (shouldExpand) { - this.expandAccordion(initialUpdate); + this.expandAccordion(); this.isNext = this.isPrevious = false; } else { - this.collapseAccordion(initialUpdate); + this.collapseAccordion(); /** * When using popout or inset, @@ -403,6 +485,12 @@ export class Accordion implements ComponentInterface { if (disabled || readonly) return; + /** + * Mark that the user has interacted with the accordion. + * This enables animations for all future state changes. + */ + this.hasInteracted = true; + if (accordionGroupEl) { /** * Because the accordion group may or may diff --git a/core/src/components/accordion/test/accordion.spec.ts b/core/src/components/accordion/test/accordion.spec.ts index 54883002dbf..e10fdc9d279 100644 --- a/core/src/components/accordion/test/accordion.spec.ts +++ b/core/src/components/accordion/test/accordion.spec.ts @@ -200,6 +200,87 @@ it('should set default values if not provided', async () => { expect(accordion.classList.contains('accordion-collapsed')).toEqual(false); }); +it('should not animate when initial value is set before load', async () => { + const page = await newSpecPage({ + components: [Item, Accordion, AccordionGroup], + }); + + const accordionGroup = page.doc.createElement('ion-accordion-group'); + accordionGroup.innerHTML = ` + + Label +
Content
+
+ + Label +
Content
+
+ `; + + 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 = ` + + Label +
Content
+
+ + Label +
Content
+
+ `; + + 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: ` + + + Label +
Content
+
+
+ `, + }); + + 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({ diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index ccf20ba012d..77ed273c432 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -399,11 +399,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf target, }; let fill = this.fill; - /** - * We check both undefined and null to - * work around https://github.com/ionic-team/stencil/issues/3586. - */ - if (fill == null) { + if (fill === undefined) { fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid'; } diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 39793837454..e33eb885177 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -36,8 +37,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; @@ -133,6 +134,13 @@ export class Checkbox implements ComponentInterface { */ @Prop() size?: 'small'; + /** + * 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. @@ -151,18 +159,69 @@ export class Checkbox implements ComponentInterface { */ @Event() ionBlur!: EventEmitter; + connectedCallback() { + const { el } = this; + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextId = this.getHintTextId(); + }); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(el); + } + componentWillLoad() { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), }; + + this.hintTextId = this.getHintTextId(); + } + + disconnectedCallback() { + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } /** @internal */ @Method() async setFocus() { - if (this.focusEl) { - this.focusEl.focus(); - } + this.el.focus(); } /** @@ -182,7 +241,6 @@ export class Checkbox implements ComponentInterface { private toggleChecked = (ev: Event) => { ev.preventDefault(); - this.setFocus(); this.setChecked(!this.checked); this.indeterminate = false; }; @@ -220,10 +278,10 @@ export class Checkbox implements ComponentInterface { ev.stopPropagation(); }; - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + private getHintTextId(): string | undefined { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -239,7 +297,7 @@ export class Checkbox implements ComponentInterface { * This element should only be rendered if hint text is set. */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; /** * undefined and empty string values should @@ -252,11 +310,11 @@ export class Checkbox implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); @@ -293,13 +351,17 @@ export class Checkbox implements ComponentInterface {
+ + diff --git a/core/src/components/checkbox/test/validation/index.html b/core/src/components/checkbox/test/validation/index.html new file mode 100644 index 00000000000..e3f4bce5601 --- /dev/null +++ b/core/src/components/checkbox/test/validation/index.html @@ -0,0 +1,184 @@ + + + + + Checkbox - Validation + + + + + + + + + + + + + + Checkbox - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ I agree to the terms and conditions +
+ +
+

Optional Field (No Validation)

+ Optional Checkbox +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss index a48fd5b2286..7dd79991a59 100644 --- a/core/src/components/header/header.ios.scss +++ b/core/src/components/header/header.ios.scss @@ -39,6 +39,15 @@ --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 { @@ -65,8 +74,6 @@ * 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; } @@ -92,6 +99,28 @@ 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; diff --git a/core/src/components/header/header.tsx b/core/src/components/header/header.tsx index 69c43335aaa..e922a388605 100644 --- a/core/src/components/header/header.tsx +++ b/core/src/components/header/header.tsx @@ -16,6 +16,7 @@ import { handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, + getRoleType, } from './header.utils'; /** @@ -219,9 +220,10 @@ export class Header implements ComponentInterface { const { translucent, inheritedAttributes, divider } = this; const theme = getIonTheme(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 = hostContext('ion-menu', this.el) ? 'none' : 'banner'; + const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, theme); return ( { const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl); if (active) { + headerEl.setAttribute('role', ROLE_BANNER); headerEl.classList.remove('header-collapse-condense-inactive'); ionTitles.forEach((ionTitle) => { @@ -179,6 +182,16 @@ 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'); /** @@ -244,3 +257,28 @@ 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 theme The current theme. + * @returns 'none' if inside ion-menu or if condensed in md + * theme, otherwise 'banner'. + */ +export const getRoleType = (isInsideMenu: boolean, isCondensed: boolean, theme: 'ios' | 'md' | 'ionic') => { + // 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` & `ionic` theme condensed headers + * since the large header is never shown. + */ + if (isCondensed && theme !== 'ios') { + return ROLE_NONE; + } + // Default to banner role. + return ROLE_BANNER; +}; diff --git a/core/src/components/header/test/condense/header.e2e.ts b/core/src/components/header/test/condense/header.e2e.ts index b57d1ee58f7..c416532973e 100644 --- a/core/src/components/header/test/condense/header.e2e.ts +++ b/core/src/components/header/test/condense/header.e2e.ts @@ -40,5 +40,45 @@ 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'); + }); }); }); diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 08c4cbdc607..94829b29bdd 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -15,7 +15,7 @@ import { h, } from '@stencil/core'; import type { NotchController } from '@utils/forms'; -import { createNotchController } from '@utils/forms'; +import { createNotchController, checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; @@ -425,16 +425,6 @@ export class Input implements ComponentInterface { } } - /** - * Checks if the input is in an invalid state based on Ionic validation classes - */ - private checkInvalidState(): boolean { - const hasIonTouched = this.el.classList.contains('ion-touched'); - const hasIonInvalid = this.el.classList.contains('ion-invalid'); - - return hasIonTouched && hasIonInvalid; - } - connectedCallback() { const { el } = this; @@ -448,7 +438,7 @@ export class Input implements ComponentInterface { // Watch for class changes to update validation state if (Build.isBrowser && typeof MutationObserver !== 'undefined') { this.validationObserver = new MutationObserver(() => { - const newIsInvalid = this.checkInvalidState(); + const newIsInvalid = checkInvalidState(el); if (this.isInvalid !== newIsInvalid) { this.isInvalid = newIsInvalid; // Force a re-render to update aria-describedby immediately @@ -463,7 +453,7 @@ export class Input implements ComponentInterface { } // Always set initial state - this.isInvalid = this.checkInvalidState(); + this.isInvalid = checkInvalidState(el); this.debounceChanged(); if (Build.isBrowser) { diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index 71ee98b0ce7..ae95316c25c 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import { renderHiddenInput } from '@utils/helpers'; import { getIonTheme } from '../../global/ionic-global'; @@ -24,9 +25,17 @@ export class RadioGroup implements ComponentInterface { private errorTextId = `${this.inputId}-error-text`; private labelId = `${this.inputId}-lbl`; private label?: HTMLIonLabelElement | null; + private validationObserver?: MutationObserver; @Element() el!: HTMLElement; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextId?: string; + /** * If `true`, the radios can be deselected. */ @@ -126,6 +135,57 @@ export class RadioGroup implements ComponentInterface { this.labelId = label.id = this.name + '-lbl'; } } + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(this.el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextId = this.getHintTextId(); + }); + } + }); + + this.validationObserver.observe(this.el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(this.el); + } + + componentWillLoad() { + this.hintTextId = this.getHintTextId(); + } + + disconnectedCallback() { + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } private getRadios(): HTMLIonRadioElement[] { @@ -249,7 +309,7 @@ export class RadioGroup implements ComponentInterface { * Renders the helper text or error text values */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; const hasHintText = !!helperText || !!errorText; if (!hasHintText) { @@ -258,20 +318,20 @@ export class RadioGroup implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); } - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + private getHintTextId(): string | undefined { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -295,8 +355,8 @@ export class RadioGroup implements ComponentInterface { }} role="radiogroup" aria-labelledby={label ? labelId : null} - aria-describedby={this.getHintTextID()} - aria-invalid={this.getHintTextID() === this.errorTextId} + aria-describedby={this.hintTextId} + aria-invalid={this.isInvalid ? 'true' : undefined} onClick={this.onClick} > {this.renderHintText()} diff --git a/core/src/components/radio-group/test/validation/index.html b/core/src/components/radio-group/test/validation/index.html new file mode 100644 index 00000000000..49fdbf1ebbc --- /dev/null +++ b/core/src/components/radio-group/test/validation/index.html @@ -0,0 +1,194 @@ + + + + + Radio Group - Validation + + + + + + + + + + + + + + Radio Group - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ + Grapes
+ Strawberries +
+
+ +
+

Optional Field (No Validation)

+ + Cucumbers
+ Tomatoes +
+
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 6857860adde..a3d6761251d 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -1,8 +1,8 @@ import caretDownRegular from '@phosphor-icons/core/assets/regular/caret-down.svg'; import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core'; import type { NotchController } from '@utils/forms'; -import { compareOptions, createNotchController, isOptionSelected } from '@utils/forms'; +import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from '@utils/forms'; import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; @@ -68,6 +68,7 @@ export class Select implements ComponentInterface { private inheritedAttributes: Attributes = {}; private nativeWrapperEl: HTMLElement | undefined; private notchSpacerEl: HTMLElement | undefined; + private validationObserver?: MutationObserver; private notchController?: NotchController; @@ -85,6 +86,13 @@ export class Select implements ComponentInterface { */ @State() hasFocus = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextId?: string; + /** * The text to display on the cancel button. */ @@ -314,10 +322,51 @@ export class Select implements ComponentInterface { */ forceUpdate(this); }); + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(this.el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextId = this.getHintTextId(); + }); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(this.el); } componentWillLoad() { this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + + this.hintTextId = this.getHintTextId(); } componentDidLoad() { @@ -344,6 +393,12 @@ export class Select implements ComponentInterface { this.notchController.destroy(); this.notchController = undefined; } + + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } /** @@ -1084,8 +1139,8 @@ export class Select implements ComponentInterface { aria-label={this.ariaLabel} aria-haspopup="dialog" aria-expanded={`${isExpanded}`} - aria-describedby={this.getHintTextID()} - aria-invalid={this.getHintTextID() === this.errorTextId} + aria-describedby={this.hintTextId} + aria-invalid={this.isInvalid ? 'true' : undefined} aria-required={`${required}`} onFocus={this.onFocus} onBlur={this.onBlur} @@ -1161,10 +1216,10 @@ export class Select implements ComponentInterface { return config.get('selectCollapsedIcon', defaultIcon); } - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + private getHintTextId(): string | undefined { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -1179,14 +1234,14 @@ export class Select implements ComponentInterface { * Renders the helper text or error text values */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; return [ -
- {helperText} +
+ {!isInvalid ? helperText : null}
, -
- {errorText} + , ]; } diff --git a/core/src/components/select/test/validation/index.html b/core/src/components/select/test/validation/index.html new file mode 100644 index 00000000000..74d0586bd77 --- /dev/null +++ b/core/src/components/select/test/validation/index.html @@ -0,0 +1,200 @@ + + + + + Select - Validation + + + + + + + + + + + + + + Select - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ + Apples + Oranges + Pears + +
+ +
+

Optional Field (No Validation)

+ + Red + Blue + Green + +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx index 02bf05082d8..0717172204a 100644 --- a/core/src/components/tab-bar/tab-bar.tsx +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -24,6 +24,7 @@ import type { TabBarChangedEventDetail } from './tab-bar-interface'; }) export class TabBar implements ComponentInterface { private keyboardCtrl: KeyboardController | null = null; + private didLoad = false; @Element() el!: HTMLElement; @@ -42,6 +43,12 @@ export class TabBar implements ComponentInterface { @Prop() selectedTab?: string; @Watch('selectedTab') selectedTabChanged() { + // Skip the initial watcher call that happens during component load + // We handle that in componentDidLoad to ensure children are ready + if (!this.didLoad) { + return; + } + if (this.selectedTab !== undefined) { this.ionTabBarChanged.emit({ tab: this.selectedTab, @@ -87,8 +94,19 @@ export class TabBar implements ComponentInterface { */ @Event() ionTabBarLoaded!: EventEmitter; - componentWillLoad() { - this.selectedTabChanged(); + componentDidLoad() { + this.ionTabBarLoaded.emit(); + // Set the flag to indicate the component has loaded + // This allows the watcher to emit changes from this point forward + this.didLoad = true; + + // Emit the initial selected tab after the component is fully loaded + // This ensures all child components (ion-tab-button) are ready + if (this.selectedTab !== undefined) { + this.ionTabBarChanged.emit({ + tab: this.selectedTab, + }); + } } async connectedCallback() { @@ -112,10 +130,6 @@ export class TabBar implements ComponentInterface { } } - componentDidLoad() { - this.ionTabBarLoaded.emit(); - } - private getShape(): string | undefined { const theme = getIonTheme(this); const { shape } = this; diff --git a/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-ltr-light-Mobile-Firefox-linux.png index ea08431df02..5faf81c5e3b 100644 Binary files a/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-ltr-light-Mobile-Firefox-linux.png and b/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-rtl-light-Mobile-Firefox-linux.png index 6ca62bdd107..f30e8a41c9f 100644 Binary files a/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-rtl-light-Mobile-Firefox-linux.png and b/core/src/components/tab-bar/test/basic/tab-bar.e2e.ts-snapshots/tab-bar-default-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Chrome-linux.png index 23aa68062ec..e451765985d 100644 Binary files a/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Safari-linux.png b/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Safari-linux.png index 8b93748ae5c..6ca8470ec8c 100644 Binary files a/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/tab-bar/test/translucent/tab-bar.e2e.ts-snapshots/tab-bar-translucent-container-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/tabs/tabs.tsx b/core/src/components/tabs/tabs.tsx index 2456d0d6197..fcb66642563 100644 --- a/core/src/components/tabs/tabs.tsx +++ b/core/src/components/tabs/tabs.tsx @@ -68,32 +68,33 @@ export class Tabs implements NavOutlet { this.ionNavWillLoad.emit(); } - componentWillRender() { + componentDidLoad() { + this.updateTabBar(); + } + + componentDidUpdate() { + this.updateTabBar(); + } + + private updateTabBar() { const tabBar = this.el.querySelector('ion-tab-bar'); - if (tabBar) { - let tab = this.selectedTab ? this.selectedTab.tab : undefined; - - // Fallback: if no selectedTab is set but we're using router mode, - // determine the active tab from the current URL. This works around - // timing issues in React Router integration where setRouteId may not - // be called in time for the initial render. - // TODO(FW-6724): Remove this with React Router upgrade - if (!tab && this.useRouter && typeof window !== 'undefined') { - const currentPath = window.location.pathname; - const tabButtons = this.el.querySelectorAll('ion-tab-button'); - - // Look for a tab button that matches the current path pattern - for (const tabButton of tabButtons) { - const tabId = tabButton.getAttribute('tab'); - if (tabId && currentPath.includes(tabId)) { - tab = tabId; - break; - } - } - } + if (!tabBar) { + return; + } - tabBar.selectedTab = tab; + const tab = this.selectedTab ? this.selectedTab.tab : undefined; + + // If tabs has no selected tab but tab-bar already has a selected-tab set, + // don't overwrite it. This handles cases where tab-bar is used without ion-tab elements. + if (tab === undefined) { + return; + } + + if (tabBar.selectedTab === tab) { + return; } + + tabBar.selectedTab = tab; } /** @@ -165,6 +166,7 @@ export class Tabs implements NavOutlet { this.selectedTab = selectedTab; this.ionTabsWillChange.emit({ tab: selectedTab.tab }); selectedTab.active = true; + this.updateTabBar(); return Promise.resolve(); } diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index 6a6d51f3c4b..272b21f3077 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -15,7 +15,7 @@ import { writeTask, } from '@stencil/core'; import type { NotchController } from '@utils/forms'; -import { createNotchController } from '@utils/forms'; +import { createNotchController, checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers'; import { createSlotMutationController } from '@utils/slot-mutation-controller'; @@ -348,16 +348,6 @@ export class Textarea implements ComponentInterface { } } - /** - * Checks if the textarea is in an invalid state based on Ionic validation classes - */ - private checkValidationState(): boolean { - const hasIonTouched = this.el.classList.contains('ion-touched'); - const hasIonInvalid = this.el.classList.contains('ion-invalid'); - - return hasIonTouched && hasIonInvalid; - } - connectedCallback() { const { el } = this; this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this)); @@ -370,7 +360,7 @@ export class Textarea implements ComponentInterface { // Watch for class changes to update validation state if (Build.isBrowser && typeof MutationObserver !== 'undefined') { this.validationObserver = new MutationObserver(() => { - const newIsInvalid = this.checkValidationState(); + const newIsInvalid = checkInvalidState(this.el); if (this.isInvalid !== newIsInvalid) { this.isInvalid = newIsInvalid; // Force a re-render to update aria-describedby immediately @@ -385,7 +375,7 @@ export class Textarea implements ComponentInterface { } // Always set initial state - this.isInvalid = this.checkValidationState(); + this.isInvalid = checkInvalidState(this.el); this.debounceChanged(); if (Build.isBrowser) { diff --git a/core/src/components/toggle/test/basic/index.html b/core/src/components/toggle/test/basic/index.html index e8e286b7a73..a127f5c5af6 100644 --- a/core/src/components/toggle/test/basic/index.html +++ b/core/src/components/toggle/test/basic/index.html @@ -45,6 +45,20 @@ Full-width
Long Label Long Label Long Label Long Label Long Label Long Label
+ + diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts b/core/src/components/toggle/test/basic/toggle.e2e.ts index 0cdd76792a4..9502df83b55 100644 --- a/core/src/components/toggle/test/basic/toggle.e2e.ts +++ b/core/src/components/toggle/test/basic/toggle.e2e.ts @@ -1,7 +1,65 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('toggle: ionChange'), () => { + test('should fire ionChange when interacting with toggle', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const ionChange = await page.spyOnEvent('ionChange'); + const toggle = page.locator('ion-toggle'); + + await toggle.click(); + expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true }); + + await toggle.click(); + expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false }); + }); + + test('should fire ionChange when interacting with toggle in item', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + const ionChange = await page.spyOnEvent('ionChange'); + const item = page.locator('ion-item'); + + await item.click(); + expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true }); + + await item.click(); + expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false }); + }); + + test('should not fire when programmatically setting a value', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const ionChange = await page.spyOnEvent('ionChange'); + const toggle = page.locator('ion-toggle'); + + await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true)); + expect(ionChange).not.toHaveReceivedEvent(); + }); + }); + test.describe(title('toggle: click'), () => { test('should trigger onclick only once when clicking the label', async ({ page }, testInfo) => { testInfo.annotations.push({ @@ -35,4 +93,195 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(clickCount).toBe(1); }); }); + + test.describe(title('toggle: ionFocus'), () => { + test('should not have visual regressions', async ({ page, pageUtils }) => { + await page.setContent( + ` + + +
+ Unchecked +
+ `, + config + ); + + await pageUtils.pressKeys('Tab'); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`toggle-focus`)); + }); + + test('should not have visual regressions when interacting with toggle in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + Unchecked + + `, + config + ); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`toggle-in-item-focus`)); + }); + + test('should fire ionFocus when toggle is focused', async ({ page, pageUtils }) => { + await page.setContent( + ` + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + expect(ionFocus).toHaveReceivedEventTimes(1); + + // Reset focus + const toggle = page.locator('ion-toggle'); + const toggleBoundingBox = (await toggle.boundingBox())!; + await page.mouse.click(0, toggleBoundingBox.height + 1); + + // Test focus with click + await toggle.click(); + + expect(ionFocus).toHaveReceivedEventTimes(2); + }); + + test('should fire ionFocus when interacting with toggle in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + expect(ionFocus).toHaveReceivedEventTimes(1); + + // Verify that the event target is the toggle and not the item + const eventByKeyboard = ionFocus.events[0]; + expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle'); + + // Reset focus + const toggle = page.locator('ion-toggle'); + const toggleBoundingBox = (await toggle.boundingBox())!; + await page.mouse.click(0, toggleBoundingBox.height + 1); + + // Test focus with click + const item = page.locator('ion-item'); + await item.click(); + + expect(ionFocus).toHaveReceivedEventTimes(2); + + // Verify that the event target is the toggle and not the item + const eventByClick = ionFocus.events[0]; + expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle'); + }); + + test('should not fire when programmatically setting a value', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + const toggle = page.locator('ion-toggle'); + + await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true)); + expect(ionFocus).not.toHaveReceivedEvent(); + }); + }); + + test.describe(title('toggle: ionBlur'), () => { + test('should fire ionBlur when toggle is blurred', async ({ page, pageUtils }) => { + await page.setContent( + ` + + `, + config + ); + + const ionBlur = await page.spyOnEvent('ionBlur'); + + // Test blur with keyboard navigation + // Focus the toggle + await pageUtils.pressKeys('Tab'); + // Blur the toggle + await pageUtils.pressKeys('Tab'); + + expect(ionBlur).toHaveReceivedEventTimes(1); + + // Test blur with click + const toggle = page.locator('ion-toggle'); + // Focus the toggle + await toggle.click(); + // Blur the toggle by clicking outside of it + const toggleBoundingBox = (await toggle.boundingBox())!; + await page.mouse.click(0, toggleBoundingBox.height + 1); + + expect(ionBlur).toHaveReceivedEventTimes(2); + }); + + test('should fire ionBlur after interacting with toggle in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + + `, + config + ); + + const ionBlur = await page.spyOnEvent('ionBlur'); + + // Test blur with keyboard navigation + // Focus the toggle + await pageUtils.pressKeys('Tab'); + // Blur the toggle + await pageUtils.pressKeys('Tab'); + + expect(ionBlur).toHaveReceivedEventTimes(1); + + // Verify that the event target is the toggle and not the item + const event = ionBlur.events[0]; + expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle'); + + // Test blur with click + const item = page.locator('ion-item'); + await item.click(); + // Blur the toggle by clicking outside of it + const itemBoundingBox = (await item.boundingBox())!; + await page.mouse.click(0, itemBoundingBox.height + 1); + + expect(ionBlur).toHaveReceivedEventTimes(2); + + // Verify that the event target is the toggle and not the item + const eventByClick = ionBlur.events[0]; + expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle'); + }); + }); }); diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0d7cbc11195 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..bb21be2862e Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..07a370b9311 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ee7a99593cd Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a54d1caeb87 Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f9e16f871ef Binary files /dev/null and b/core/src/components/toggle/test/basic/toggle.e2e.ts-snapshots/toggle-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/toggle/test/item/index.html b/core/src/components/toggle/test/item/index.html index caeda79d077..b872bcf3ea3 100644 --- a/core/src/components/toggle/test/item/index.html +++ b/core/src/components/toggle/test/item/index.html @@ -223,6 +223,20 @@

Multiline Label

+ + diff --git a/core/src/components/toggle/test/validation/index.html b/core/src/components/toggle/test/validation/index.html new file mode 100644 index 00000000000..54932edeb08 --- /dev/null +++ b/core/src/components/toggle/test/validation/index.html @@ -0,0 +1,184 @@ + + + + + Toggle - Validation + + + + + + + + + + + + + + Toggle - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ Tap to turn on +
+ +
+

Optional Field (No Validation)

+ Optional Toggle +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index 17db54623dc..9f416ae88e1 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -1,7 +1,8 @@ import checkRegular from '@phosphor-icons/core/assets/regular/check.svg'; import minusRegular from '@phosphor-icons/core/assets/regular/minus.svg'; import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import { renderHiddenInput, inheritAriaAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { hapticSelection } from '@utils/native/haptic'; @@ -44,16 +45,23 @@ export class Toggle implements ComponentInterface { private helperTextId = `${this.inputId}-helper-text`; private errorTextId = `${this.inputId}-error-text`; private gesture?: Gesture; - private focusEl?: HTMLElement; private lastDrag = 0; private inheritedAttributes: Attributes = {}; private toggleTrack?: HTMLElement; private didLoad = false; + private validationObserver?: MutationObserver; @Element() el!: HTMLIonToggleElement; @State() activated = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextId?: string; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -166,7 +174,6 @@ export class Toggle implements ComponentInterface { const isNowChecked = !checked; this.checked = isNowChecked; - this.setFocus(); this.ionChange.emit({ checked: isNowChecked, value, @@ -174,15 +181,56 @@ export class Toggle implements ComponentInterface { } async connectedCallback() { + const { didLoad, el } = this; + /** * If we have not yet rendered * ion-toggle, then toggleTrack is not defined. * But if we are moving ion-toggle via appendChild, * then toggleTrack will be defined. */ - if (this.didLoad) { + if (didLoad) { this.setupGesture(); } + + // 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); } componentDidLoad() { @@ -213,12 +261,20 @@ export class Toggle implements ComponentInterface { this.gesture.destroy(); this.gesture = undefined; } + + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } componentWillLoad() { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), }; + + this.hintTextId = this.getHintTextId(); } private onStart() { @@ -247,9 +303,7 @@ export class Toggle implements ComponentInterface { } private setFocus() { - if (this.focusEl) { - this.focusEl.focus(); - } + this.el.focus(); } private onKeyDown = (ev: KeyboardEvent) => { @@ -397,10 +451,10 @@ export class Toggle implements ComponentInterface { return this.el.textContent !== ''; } - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + private getHintTextId(): string | undefined { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -416,7 +470,7 @@ export class Toggle implements ComponentInterface { * This element should only be rendered if hint text is set. */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; /** * undefined and empty string values should @@ -429,11 +483,11 @@ export class Toggle implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); @@ -447,7 +501,6 @@ export class Toggle implements ComponentInterface { color, disabled, el, - errorTextId, hasLabel, inheritedAttributes, inputId, @@ -468,14 +521,17 @@ export class Toggle implements ComponentInterface { this.onFocus()} - onBlur={() => this.onBlur()} - ref={(focusEl) => (this.focusEl = focusEl)} required={required} {...inheritedAttributes} /> diff --git a/core/src/utils/forms/index.ts b/core/src/utils/forms/index.ts index d24bddfaa77..682811ed643 100644 --- a/core/src/utils/forms/index.ts +++ b/core/src/utils/forms/index.ts @@ -1,2 +1,3 @@ export * from './notch-controller'; export * from './compare-with-utils'; +export * from './validity'; diff --git a/core/src/utils/forms/validity.ts b/core/src/utils/forms/validity.ts new file mode 100644 index 00000000000..5c2adabe675 --- /dev/null +++ b/core/src/utils/forms/validity.ts @@ -0,0 +1,21 @@ +type FormElement = + | HTMLIonInputElement + | HTMLIonTextareaElement + | HTMLIonSelectElement + | HTMLIonCheckboxElement + | HTMLIonToggleElement + | HTMLElement; + +/** + * Checks if the form element is in an invalid state based on + * Ionic validation classes. + * + * @param el The form element to check. + * @returns `true` if the element is invalid, `false` otherwise. + */ +export const checkInvalidState = (el: FormElement): boolean => { + const hasIonTouched = el.classList.contains('ion-touched'); + const hasIonInvalid = el.classList.contains('ion-invalid'); + + return hasIonTouched && hasIonInvalid; +}; diff --git a/core/src/utils/test/playwright/page/utils/spy-on-event.ts b/core/src/utils/test/playwright/page/utils/spy-on-event.ts index a34f7194027..bc6891dc1cd 100644 --- a/core/src/utils/test/playwright/page/utils/spy-on-event.ts +++ b/core/src/utils/test/playwright/page/utils/spy-on-event.ts @@ -2,6 +2,40 @@ import type { E2EPage } from '../../playwright-declarations'; import { addE2EListener, EventSpy } from '../event-spy'; export const spyOnEvent = async (page: E2EPage, eventName: string): Promise => { + /** + * Tabbing out of the page boundary can lead to unreliable `ionBlur events, + * particularly in Firefox. + * + * This occurs because Playwright may incorrectly maintain focus state on the + * last element, even after a Tab press attempts to shift focus outside the + * viewport. To reliably trigger the necessary blur event, add a visually + * hidden, focusable element at the end of the page to receive focus instead of + * the browser. + * + * Playwright issue reference: + * https://github.com/microsoft/playwright/issues/32269 + */ + if (eventName === 'ionBlur') { + const hiddenInput = await page.$('#hidden-input-for-ion-blur'); + if (!hiddenInput) { + await page.evaluate(() => { + const input = document.createElement('input'); + input.id = 'hidden-input-for-ion-blur'; + input.style.position = 'absolute'; + input.style.opacity = '0'; + input.style.height = '0'; + input.style.width = '0'; + input.style.pointerEvents = 'none'; + document.body.appendChild(input); + + // Clean up the element when the page is unloaded. + window.addEventListener('unload', () => { + input.remove(); + }); + }); + } + } + const spy = new EventSpy(eventName); const handle = await page.evaluateHandle(() => window); diff --git a/core/src/utils/transition/index.ts b/core/src/utils/transition/index.ts index 69789bf862d..e74e7318c82 100644 --- a/core/src/utils/transition/index.ts +++ b/core/src/utils/transition/index.ts @@ -18,34 +18,51 @@ const focusController = createFocusController(); // TODO(FW-2832): types +/** + * Executes the main page transition. + * It also manages the lifecycle of header visibility (if any) + * to prevent visual flickering in iOS. The flickering only + * occurs for a condensed header that is placed above the content. + * + * @param opts Options for the transition. + * @returns A promise that resolves when the transition is complete. + */ export const transition = (opts: TransitionOptions): Promise => { return new Promise((resolve, reject) => { writeTask(() => { - beforeTransition(opts); - runTransition(opts).then( - (result) => { - if (result.animation) { - result.animation.destroy(); + const transitioningInactiveHeader = getIosIonHeader(opts); + beforeTransition(opts, transitioningInactiveHeader); + runTransition(opts) + .then( + (result) => { + if (result.animation) { + result.animation.destroy(); + } + afterTransition(opts); + resolve(result); + }, + (error) => { + afterTransition(opts); + reject(error); } - afterTransition(opts); - resolve(result); - }, - (error) => { - afterTransition(opts); - reject(error); - } - ); + ) + .finally(() => { + // Ensure that the header is restored to its original state. + setHeaderTransitionClass(transitioningInactiveHeader, false); + }); }); }); }; -const beforeTransition = (opts: TransitionOptions) => { +const beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => { const enteringEl = opts.enteringEl; const leavingEl = opts.leavingEl; focusController.saveViewFocus(leavingEl); setZIndex(enteringEl, leavingEl, opts.direction); + // Prevent flickering of the header by adding a class. + setHeaderTransitionClass(transitioningInactiveHeader, true); if (opts.showGoBack) { enteringEl.classList.add('can-go-back'); @@ -278,6 +295,40 @@ const setZIndex = ( } }; +/** + * Add a class to ensure that the header (if any) + * does not flicker during the transition. By adding the + * transitioning class, we ensure that the header has + * the necessary styles to prevent the following flickers: + * 1. When entering a page with a condensed header, the + * header should never be visible. However, + * it briefly renders the background color while + * the transition is occurring. + * 2. When leaving a page with a condensed header, the + * header has an opacity of 0 and the pages + * have a z-index which causes the entering page to + * briefly show it's content underneath the leaving page. + * 3. When entering a page or leaving a page with a fade + * header, the header should not have a background color. + * However, it briefly shows the background color while + * the transition is occurring. + * + * @param header The header element to modify. + * @param isTransitioning Whether the transition is occurring. + */ +const setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => { + if (!header) { + return; + } + + const transitionClass = 'header-transitioning'; + if (isTransitioning) { + header.classList.add(transitionClass); + } else { + header.classList.remove(transitionClass); + } +}; + export const getIonPageElement = (element: HTMLElement) => { if (element.classList.contains('ion-page')) { return element; @@ -291,6 +342,32 @@ export const getIonPageElement = (element: HTMLElement) => { return element; }; +/** + * Retrieves the ion-header element from a page based on the + * direction of the transition. + * + * @param opts Options for the transition. + * @returns The ion-header element or null if not found or not in 'ios' mode. + */ +const getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => { + const enteringEl = opts.enteringEl; + const leavingEl = opts.leavingEl; + const direction = opts.direction; + const mode = opts.mode; + + if (mode !== 'ios') { + return null; + } + + const element = direction === 'back' ? leavingEl : enteringEl; + + if (!element) { + return null; + } + + return element.querySelector('ion-header'); +}; + export interface TransitionOptions extends NavOptions { progressCallback?: (ani: Animation | undefined) => void; baseEl: any; diff --git a/lerna.json b/lerna.json index 283bc5a42ef..e871041f8ba 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "core", "packages/*" ], - "version": "8.7.5" + "version": "8.7.10" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index 2286ed5513c..50ea1a4d74d 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/angular-server + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/angular-server + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/angular-server + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/angular-server + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 58d98508f35..fd80d3a6af7 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.5" + "@ionic/core": "^8.7.10" }, "devDependencies": { "@angular-eslint/eslint-plugin": "^16.0.0", @@ -1031,12 +1031,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -1386,9 +1386,9 @@ ] }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -7306,11 +7306,11 @@ "dev": true }, "@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "requires": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -7529,9 +7529,9 @@ "optional": true }, "@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "requires": { "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index f17037d02ba..f27038921ca 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "8.7.5", + "version": "8.7.10", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -62,6 +62,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^8.7.5" + "@ionic/core": "^8.7.10" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index 79e9b64e6d3..9d20850477a 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,52 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + + +### Bug Fixes + +* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2)) + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/angular + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/angular + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + + +### Bug Fixes + +* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367)) + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/angular + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index e840282dae0..d9d320ab3aa 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -32,7 +32,7 @@ "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", "@schematics/angular": "^17.0.0", - "@types/node": "12.12.5", + "@types/node": "20.19.25", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.0", @@ -41,7 +41,7 @@ "ng-packagr": "^16.0.0", "prettier": "^2.4.1", "rxjs": "~7.5.0", - "typescript": "~4.9.3", + "typescript": "~5.0.2", "typescript-eslint-language-service": "^5.0.0", "zone.js": "~0.13.0" }, @@ -1398,12 +1398,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -2308,9 +2308,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -2402,9 +2402,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "12.12.5", + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -8719,16 +8724,17 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", + "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/typescript-eslint-language-service": { @@ -8756,6 +8762,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", diff --git a/packages/angular/package.json b/packages/angular/package.json index bfdef31fd6e..cfc96d53dd6 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "8.7.5", + "version": "8.7.10", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -48,7 +48,7 @@ } }, "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -78,7 +78,7 @@ "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", "@schematics/angular": "^17.0.0", - "@types/node": "12.12.5", + "@types/node": "20.19.25", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.0", @@ -87,7 +87,7 @@ "ng-packagr": "^16.0.0", "prettier": "^2.4.1", "rxjs": "~7.5.0", - "typescript": "~4.9.3", + "typescript": "~5.0.2", "typescript-eslint-language-service": "^5.0.0", "zone.js": "~0.13.0" }, diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html index d33aa4ae1e5..870f53872f2 100644 --- a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html +++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html @@ -77,6 +77,105 @@

MinLength Errors: {{minLengthField.errors | json}}

+ + + + + Option 1 + Option 2 + + + + + + +

Select Touched: {{selectField.touched}}

+

Select Invalid: {{selectField.invalid}}

+

Select Errors: {{selectField.errors | json}}

+
+
+ + + + + I agree to the terms and conditions + + + + + + +

Checkbox Touched: {{checkboxField.touched}}

+

Checkbox Invalid: {{checkboxField.invalid}}

+

Checkbox Errors: {{checkboxField.errors | json}}

+
+
+ + + + + Tap to turn on + + + + + + +

Toggle Touched: {{toggleField.touched}}

+

Toggle Invalid: {{toggleField.invalid}}

+

Toggle Errors: {{toggleField.errors | json}}

+
+
+ + + + + Grapes
+ Strawberries +
+
+ + + + +

Radio Group Touched: {{radioGroupField.touched}}

+

Radio Group Invalid: {{radioGroupField.invalid}}

+

Radio Group Errors: {{radioGroupField.errors | json}}

+
+
diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts index 1ecdaa5e5d0..d26e1a6cc4a 100644 --- a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts +++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts @@ -9,6 +9,10 @@ export class TemplateFormComponent { inputValue = ''; textareaValue = ''; minLengthValue = ''; + selectValue = ''; + checkboxValue = false; + toggleValue = false; + radioGroupValue = ''; // Track if form has been submitted submitted = false; diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index ed9628ae7c9..007743f905f 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -47,6 +47,10 @@ export const routes: Routes = [ children: [ { path: 'input-validation', loadComponent: () => import('../validation/input-validation/input-validation.component').then(c => c.InputValidationComponent) }, { path: 'textarea-validation', loadComponent: () => import('../validation/textarea-validation/textarea-validation.component').then(c => c.TextareaValidationComponent) }, + { path: 'select-validation', loadComponent: () => import('../validation/select-validation/select-validation.component').then(c => c.SelectValidationComponent) }, + { path: 'checkbox-validation', loadComponent: () => import('../validation/checkbox-validation/checkbox-validation.component').then(c => c.CheckboxValidationComponent) }, + { path: 'toggle-validation', loadComponent: () => import('../validation/toggle-validation/toggle-validation.component').then(c => c.ToggleValidationComponent) }, + { path: 'radio-group-validation', loadComponent: () => import('../validation/radio-group-validation/radio-group-validation.component').then(c => c.RadioGroupValidationComponent) }, { path: '**', redirectTo: 'input-validation' } ] }, diff --git a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html index fd6ae409a3b..7ac9c619180 100644 --- a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html +++ b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html @@ -121,16 +121,36 @@ Validation Tests + + + Checkbox Validation Test + + Input Validation Test + + + Radio Group Validation Test + + + + + Select Validation Test + + Textarea Validation Test + + + Toggle Validation Test + + diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html new file mode 100644 index 00000000000..86a8425e0cb --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html @@ -0,0 +1,53 @@ + + + Checkbox - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.terms.label }} + +
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.label }} + +
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss new file mode 100644 index 00000000000..d8b2a267e5a --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 50px; + grid-column-gap: 50px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts new file mode 100644 index 00000000000..48bf8a935f1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonCheckbox, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-checkbox-validation', + templateUrl: './checkbox-validation.component.html', + styleUrls: ['./checkbox-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonCheckbox, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class CheckboxValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + terms: { + label: 'I agree to the terms and conditions', + helperText: "You must agree to continue", + errorText: 'This field is required' + }, + optional: { + label: 'Optional Checkbox', + helperText: 'You can skip this field', + errorText: '' + } + }; + + form = this.fb.group({ + terms: [false, Validators.requiredTrue], + optional: [false] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html new file mode 100644 index 00000000000..b7c90ba1ce6 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html @@ -0,0 +1,57 @@ + + + Radio Group - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.fruits.firstRadio }}
+ {{ fieldMetadata.fruits.secondRadio }} +
+
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.firstRadio }}
+ {{ fieldMetadata.optional.secondRadio }} +
+
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss new file mode 100644 index 00000000000..add228ccab1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 20px; + grid-column-gap: 20px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts new file mode 100644 index 00000000000..aa4ee109a50 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonRadioGroup, + IonRadio, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-radio-group-validation', + templateUrl: './radio-group-validation.component.html', + styleUrls: ['./radio-group-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonRadioGroup, + IonRadio, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class RadioGroupValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + fruits: { + helperText: "You must select one to continue", + errorText: 'This field is required', + firstRadio: "Grapes", + secondRadio: "Strawberries" + }, + optional: { + label: 'Optional Radio', + helperText: 'You can skip this field', + errorText: '', + firstRadio: "Option A", + secondRadio: "Option B" + } + }; + + form = this.fb.group({ + fruits: ['', Validators.required], + optional: [''] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.html new file mode 100644 index 00000000000..15993edc803 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.html @@ -0,0 +1,63 @@ + + + Select - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + Apples + Oranges + Pears + +
+ +
+

Optional Field (No Validation)

+ + Red + Blue + Green + +
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.scss new file mode 100644 index 00000000000..add228ccab1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 20px; + grid-column-gap: 20px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.ts new file mode 100644 index 00000000000..1ae4a239ef4 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/select-validation/select-validation.component.ts @@ -0,0 +1,63 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonSelect, + IonSelectOption, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-select-validation', + templateUrl: './select-validation.component.html', + styleUrls: ['./select-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonSelect, + IonSelectOption, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class SelectValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + fruits: { + label: 'Fruits', + helperText: "Select an option", + errorText: 'This field is required' + }, + optional: { + label: 'Colors', + helperText: 'You can skip this field', + errorText: '' + } + }; + + form = this.fb.group({ + fruits: ['', Validators.required], + optional: [''] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html new file mode 100644 index 00000000000..1bf6ac67e75 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html @@ -0,0 +1,54 @@ + + + Toggle - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.on.label }} + +
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.label }} + + +
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss new file mode 100644 index 00000000000..add228ccab1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 20px; + grid-column-gap: 20px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts new file mode 100644 index 00000000000..d756ac150be --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonToggle, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-toggle-validation', + templateUrl: './toggle-validation.component.html', + styleUrls: ['./toggle-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonToggle, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class ToggleValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + on: { + label: 'Tap to turn on', + helperText: "You must turn on to continue", + errorText: 'This field is required' + }, + optional: { + label: 'Optional Toggle', + helperText: 'You can skip this field', + errorText: '' + } + }; + + form = this.fb.group({ + on: [false, Validators.requiredTrue], + optional: [false] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index 942e8829315..2b184082db4 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/docs + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/docs + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/docs + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/docs + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 0ea9a4f7398..7b500894ce0 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/docs", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT" } } diff --git a/packages/docs/package.json b/packages/docs/package.json index 3b1eec2020f..702437e7270 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "8.7.5", + "version": "8.7.10", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index dcff3c6e956..6d2f0c247fd 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/react-router + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/react-router + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/react-router + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/react-router + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/react-router + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) **Note:** Version bump only for package @ionic/react-router diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index b0abc70ecd2..9a29e5dc1ec 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react-router", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/react": "^8.7.5", + "@ionic/react": "^8.7.10", "tslib": "*" }, "devDependencies": { @@ -238,12 +238,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -415,12 +415,12 @@ } }, "node_modules/@ionic/react": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz", - "integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.10.tgz", + "integrity": "sha512-FNGQCmkAjteOBz+qR9Qa/dl8AjdP4UXandmWWEECTzdrQKBpt5fFdu8vE3i7FBx60R4tr/zBxlila48EzVtqsQ==", "license": "MIT", "dependencies": { - "@ionic/core": "8.7.5", + "@ionic/core": "8.7.10", "ionicons": "^8.0.13", "tslib": "*" }, @@ -669,9 +669,9 @@ ] }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -4175,11 +4175,11 @@ "dev": true }, "@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "requires": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -4281,11 +4281,11 @@ "requires": {} }, "@ionic/react": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz", - "integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.10.tgz", + "integrity": "sha512-FNGQCmkAjteOBz+qR9Qa/dl8AjdP4UXandmWWEECTzdrQKBpt5fFdu8vE3i7FBx60R4tr/zBxlila48EzVtqsQ==", "requires": { - "@ionic/core": "8.7.5", + "@ionic/core": "8.7.10", "ionicons": "^8.0.13", "tslib": "*" } @@ -4422,9 +4422,9 @@ "optional": true }, "@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "requires": { "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 840cfe017a6..2d0ca34a77d 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "8.7.5", + "version": "8.7.10", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -36,7 +36,7 @@ "dist/" ], "dependencies": { - "@ionic/react": "^8.7.5", + "@ionic/react": "^8.7.10", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 6c7b05c0a33..bc0b6e7ffa5 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,49 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/react + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + + +### Bug Fixes + +* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613) + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/react + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/react + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/react + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index 68c8d93aab3..1d413e6139f 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "ionicons": "^8.0.13", "tslib": "*" }, @@ -736,12 +736,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -1726,9 +1726,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" diff --git a/packages/react/package.json b/packages/react/package.json index bfb5516c651..b09d4b153a8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "8.7.5", + "version": "8.7.10", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -40,7 +40,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "ionicons": "^8.0.13", "tslib": "*" }, diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx index 2f7f4a63ded..634af89f075 100644 --- a/packages/react/test/base/src/App.tsx +++ b/packages/react/test/base/src/App.tsx @@ -37,6 +37,7 @@ import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted' import OverlayComponents from './pages/overlay-components/OverlayComponents'; import OverlayHooks from './pages/overlay-hooks/OverlayHooks'; import ReorderGroup from './pages/ReorderGroup'; +import AccordionGroup from './pages/AccordionGroup'; setupIonicReact(); @@ -69,6 +70,7 @@ const App: React.FC = () => ( + diff --git a/packages/react/test/base/src/pages/AccordionGroup.tsx b/packages/react/test/base/src/pages/AccordionGroup.tsx new file mode 100644 index 00000000000..ffcfaca8bd2 --- /dev/null +++ b/packages/react/test/base/src/pages/AccordionGroup.tsx @@ -0,0 +1,54 @@ +import { IonHeader, IonTitle, IonToolbar, IonPage, IonContent, IonAccordionGroup, IonAccordion, IonItem, IonLabel } from '@ionic/react'; +import { useEffect, useRef } from 'react'; + +const AccordionGroup: React.FC = () => { + const accordionGroup = useRef(null); + + useEffect(() => { + if (!accordionGroup.current) { + return; + } + + accordionGroup.current.value = ['first', 'third']; + }, []); + + return ( + + + + Accordion Group + + + + + + + First Accordion + +
+ First Content +
+
+ + + Second Accordion + +
+ Second Content +
+
+ + + Third Accordion + +
+ Third Content +
+
+
+
+
+ ); +}; + +export default AccordionGroup; diff --git a/packages/react/test/base/src/pages/Main.tsx b/packages/react/test/base/src/pages/Main.tsx index dd87350d9be..3873cd3d5b5 100644 --- a/packages/react/test/base/src/pages/Main.tsx +++ b/packages/react/test/base/src/pages/Main.tsx @@ -22,6 +22,9 @@ const Main: React.FC = () => { + + Accordion Group + Overlay Hooks diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index cf6cdfedb4a..7a499dd8cbf 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/vue-router + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/vue-router + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/vue-router + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/vue-router + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/vue-router + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) **Note:** Version bump only for package @ionic/vue-router diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 5c9e6036d55..0907e5e65c3 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/vue": "^8.7.5" + "@ionic/vue": "^8.7.10" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", @@ -673,12 +673,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -865,12 +865,12 @@ } }, "node_modules/@ionic/vue": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.5.tgz", - "integrity": "sha512-wx7o+ABDDTWLM47CIjxueoZtKbvMQ9AolqGY4/2JvAJds/JlSs4kOEes/AzQ/1dREEp+4sOapmTtJnyauErY3A==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.10.tgz", + "integrity": "sha512-5nhzBZC1VASQmNGz36UXsxVsMXAUmhKhpAfWaiwqfoHy/8YLV6d5dwCQPBwQrX7K4I/k76eyho8xQ4YhQFZXng==", "license": "MIT", "dependencies": { - "@ionic/core": "8.7.5", + "@ionic/core": "8.7.10", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" } @@ -1523,9 +1523,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -8041,11 +8041,11 @@ "dev": true }, "@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "requires": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -8156,11 +8156,11 @@ "requires": {} }, "@ionic/vue": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.5.tgz", - "integrity": "sha512-wx7o+ABDDTWLM47CIjxueoZtKbvMQ9AolqGY4/2JvAJds/JlSs4kOEes/AzQ/1dREEp+4sOapmTtJnyauErY3A==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.10.tgz", + "integrity": "sha512-5nhzBZC1VASQmNGz36UXsxVsMXAUmhKhpAfWaiwqfoHy/8YLV6d5dwCQPBwQrX7K4I/k76eyho8xQ4YhQFZXng==", "requires": { - "@ionic/core": "8.7.5", + "@ionic/core": "8.7.10", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" } @@ -8624,9 +8624,9 @@ } }, "@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "requires": { "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index ba2858d5e3d..e8f7e2bcb86 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "8.7.5", + "version": "8.7.10", "description": "Vue Router integration for @ionic/vue", "scripts": { "test.spec": "jest", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^8.7.5" + "@ionic/vue": "^8.7.10" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index d0850bccee8..47d14f10a59 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + +**Note:** Version bump only for package @ionic/vue + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + +**Note:** Version bump only for package @ionic/vue + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + +**Note:** Version bump only for package @ionic/vue + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + +**Note:** Version bump only for package @ionic/vue + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + +**Note:** Version bump only for package @ionic/vue + + + + + ## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 29f867b7894..9efb270a1b2 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "8.7.5", + "version": "8.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "8.7.5", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, @@ -222,12 +222,12 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz", - "integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==", + "version": "8.7.10", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.10.tgz", + "integrity": "sha512-auDIGVQCwh/gc69WwbR/DFzZPx4O5EpYTBjS2cRzZXKK7yS1ZMey2VLflqbdpQFye+tyBCJvfcOEHgUo1vuVFA==", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "tslib": "^2.1.0" } @@ -674,9 +674,9 @@ ] }, "node_modules/@stencil/core": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", - "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" diff --git a/packages/vue/package.json b/packages/vue/package.json index d3469d121f5..d3a35214b0f 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "8.7.5", + "version": "8.7.10", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -68,7 +68,7 @@ "vue-router": "^4.0.16" }, "dependencies": { - "@ionic/core": "^8.7.5", + "@ionic/core": "^8.7.10", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, diff --git a/renovate.json5 b/renovate.json5 index 672f8fc461d..a14bdb1860c 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -8,12 +8,19 @@ groupName: "Download + Upload Artifacts" }, { - matchPackagePatterns: ["@stencil/core", "@stencil/angular-output-target", "@stencil/react-output-target", "@stencil/sass", "@stencil/vue-output-target"], + matchPackagePatterns: ["@stencil/core", "@stencil/sass"], groupName: "stencil", matchFileNames: [ "core/package.json" ] }, + { + matchPackagePatterns: ["@stencil/angular-output-target", "@stencil/react-output-target", "@stencil/vue-output-target"], + groupName: "stencil-output-targets", + matchFileNames: [ + "core/package.json" + ] + }, // TODO(ROU-11172): unpin React output target version { matchPackageNames: ["@stencil/react-output-target"],