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 6bd557db3b9..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@v4 + - 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/ionic-issue-bot.yml b/.github/ionic-issue-bot.yml index 25fab64cb6e..c1e02a5f068 100644 --- a/.github/ionic-issue-bot.yml +++ b/.github/ionic-issue-bot.yml @@ -56,14 +56,6 @@ closeAndLock: bug reports and feature requests. Please use our [forum](https://forum.ionicframework.com) for questions about the framework. - Thank you for using Ionic! - - label: "ionitron: appflow" - message: > - Thanks for the issue! This issue appears to be related to Ionic Appflow. We use this issue tracker exclusively for - bug reports and feature requests. Please use the [Ionic Appflow Support Forum](https://ionic.zendesk.com/hc/en-us/requests/new) - to report this issue. - - Thank you for using Ionic! - label: "ionitron: missing template" message: > @@ -145,65 +137,6 @@ noReproduction: lock: true dryRun: false -wrongRepo: - repos: - - label: "ionitron: capacitor" - repo: capacitor - message: > - Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests - associated with the Ionic Framework. It appears that this issue is associated with Capacitor. - I am moving this issue to the Capacitor repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: v3" - repo: ionic-v3 - message: > - Thanks for the issue! We have moved the source code and issues for Ionic 3 into a separate repository. - I am moving this issue to the repository for Ionic 3. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: cli" - repo: ionic-cli - message: > - Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests - associated with the Ionic Framework. It appears that this issue is associated with the Ionic CLI. - I am moving this issue to the Ionic CLI repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: docs" - repo: ionic-docs - message: > - Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests - associated with the Ionic Framework. It appears that this issue is associated with the Ionic Documentation. - I am moving this issue to the Ionic Docs repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: stencil" - repo: stencil - message: > - Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests - associated with the Ionic Framework. It appears that this issue is associated with Stencil. - I am moving this issue to the Stencil repository. Please track this issue over there. - - - Thank you for using Ionic! - - label: "ionitron: native" - repo: ionic-native - message: > - Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests - associated with the Ionic Framework. It appears that this issue is associated with Ionic Native. - I am moving this issue to the Ionic Native repository. Please track this issue over there. - - - Thank you for using Ionic! - close: true - lock: true - dryRun: false - screenshot: appId: 18001 checkName: "build" diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index b530d300788..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@v4 + - 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 80da1c353d7..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@v4 + - uses: actions/setup-node@v6 with: - node-version: 22.x + node-version: 24.x - uses: ./.github/workflows/actions/download-archive with: name: ionic-core path: ./core filename: CoreBuild.zip - - name: Install Angular Dependencies + - name: πŸ•ΈοΈ Install Angular Dependencies run: npm ci shell: bash working-directory: ./packages/angular - - name: Sync + - name: πŸ”„ Sync run: npm run sync shell: bash working-directory: ./packages/angular - - name: Lint + - name: πŸ–ŒοΈ Lint run: npm run lint shell: bash working-directory: ./packages/angular - - name: Build + - name: πŸ—οΈ Build run: npm run build shell: bash working-directory: ./packages/angular - - name: Check Diff + - name: πŸ” Check Diff run: git diff --exit-code shell: bash working-directory: ./packages/angular diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index de2a0ee98c5..d77adf74004 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@v4 - - uses: actions/setup-node@v4 + - 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 28006742feb..e430d563ae9 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@v4 - - uses: actions/setup-node@v4 + - 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 @@ -31,4 +31,6 @@ runs: with: name: ionic-core output: core/CoreBuild.zip - paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt + # Include generated proxy files from Stencil output targets so + # framework builds can detect when they need to be updated + paths: core/dist core/components core/css core/hydrate core/loader core/src/components.d.ts core/api.txt packages/angular/src/directives/proxies.ts packages/angular/src/directives/proxies-list.ts packages/angular/standalone/src/directives/proxies.ts packages/vue/src/proxies.ts packages/react/src/components/proxies.ts packages/react/src/components/inner-proxies.ts packages/react/src/components/routing-proxies.ts diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index 390378cb12d..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@v4 + - 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 3ea565d62e3..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@v4 + - 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 623bdc4c7a1..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@v4 + - 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 f2be91e1090..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@v4 + - 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 705d6e27cf4..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@v4 + - 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 d822e69468d..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@v4 + - 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 a4298c2c0a1..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@v4 + - 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 588c310b462..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@v4 + - 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 f6246664066..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@v4 + - 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 ab056ac667b..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@v4 + - 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 cf71e4da5aa..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@v4 + - 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 93a21db7855..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@v4 + - 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 d454b2e2a32..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@v4 + - 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/assign-issues.yml b/.github/workflows/assign-issues.yml index 4608d2323dd..d06c1f52e10 100644 --- a/.github/workflows/assign-issues.yml +++ b/.github/workflows/assign-issues.yml @@ -13,6 +13,6 @@ jobs: - name: 'Auto-assign issue' uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0 with: - assignees: brandyscarney, ShaneK + assignees: brandyscarney, thetaPC, ShaneK numOfAssignee: 1 allowSelfAssign: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a34ce912eb..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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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 fbb39d4d5d6..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@v4 - - 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/conventional-commit.yml b/.github/workflows/conventional-commit.yml index f044cbd1db0..8c0bbdebb37 100644 --- a/.github/workflows/conventional-commit.yml +++ b/.github/workflows/conventional-commit.yml @@ -12,7 +12,7 @@ jobs: if: | !contains(github.event.pull_request.title, 'release') && !contains(github.event.pull_request.title, 'chore') - uses: amannn/action-semantic-pull-request@v5 + uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 48dc911c622..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@v4 + - 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/label.yml b/.github/workflows/label.yml index a4e35060dfb..7f5d8de9787 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -13,7 +13,7 @@ jobs: triage: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6f812a02f6c..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@v4 + - 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 4be91285a4a..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@v4 - - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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 e5cc26855be..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@v4 + - 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@v4 + - 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 7c085f1b848..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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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@v4 + - 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 5bb35c5905a..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@v4 + - 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@v4 + - 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@v4 + - 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 0ba2f3e326c..5aca45ac369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,100 @@ 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) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) +* **vue:** emit component-specific overlay events ([#30688](https://github.com/ionic-team/ionic-framework/issues/30688)) ([024d090](https://github.com/ionic-team/ionic-framework/commit/024d090122548e26ec2cdcfae4637dde8f288278)), closes [#30641](https://github.com/ionic-team/ionic-framework/issues/30641) + + + + + +## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) + + +### Bug Fixes + +* **input:** improve error text accessibility ([#30635](https://github.com/ionic-team/ionic-framework/issues/30635)) ([c339bc3](https://github.com/ionic-team/ionic-framework/commit/c339bc36827b62ef871325869a9a5db9b17ac785)) +* **overlays,picker:** remove invalid aria-hidden attribute ([#30563](https://github.com/ionic-team/ionic-framework/issues/30563)) ([49f96d7](https://github.com/ionic-team/ionic-framework/commit/49f96d7f1e9050a95e3e33a821c0467ecc0bed64)), closes [#30040](https://github.com/ionic-team/ionic-framework/issues/30040) +* **segment-view:** scroll and select the right item when the component is in RTL context; ([#30675](https://github.com/ionic-team/ionic-framework/issues/30675)) ([66f517d](https://github.com/ionic-team/ionic-framework/commit/66f517d5b2154fff00b294a78f4107f057a580c6)), closes [#30079](https://github.com/ionic-team/ionic-framework/issues/30079) + + + + + +## [8.7.3](https://github.com/ionic-team/ionic-framework/compare/v8.7.2...v8.7.3) (2025-08-20) + + +### Bug Fixes + +* **checkbox:** add aria attributes to ignore checkbox icon ([#30633](https://github.com/ionic-team/ionic-framework/issues/30633)) ([e9e6605](https://github.com/ionic-team/ionic-framework/commit/e9e6605862a05a46d26c26a144ed1cf22133a2b7)), closes [#30231](https://github.com/ionic-team/ionic-framework/issues/30231) +* **refresher:** prevent focus-related scroll jumps on refresh ([#30636](https://github.com/ionic-team/ionic-framework/issues/30636)) ([1899b49](https://github.com/ionic-team/ionic-framework/commit/1899b49d252abc6003f763cea8db2a51efa941ec)) + + + + + ## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index ebbd0421168..d19d8deea27 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,99 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.10](https://github.com/ionic-team/ionic-framework/compare/v8.7.9...v8.7.10) (2025-11-19) + + +### Bug Fixes + +* **checkbox, toggle, radio-group:** improve screen reader announcement timing for validation errors ([#30714](https://github.com/ionic-team/ionic-framework/issues/30714)) ([92db364](https://github.com/ionic-team/ionic-framework/commit/92db36489cca944caf1593dbd518a1f025a171a2)) + + + + + +## [8.7.9](https://github.com/ionic-team/ionic-framework/compare/v8.7.8...v8.7.9) (2025-11-05) + + +### Bug Fixes + +* **accordion-group:** skip initial animation ([#30729](https://github.com/ionic-team/ionic-framework/issues/30729)) ([58d5638](https://github.com/ionic-team/ionic-framework/commit/58d563805fca1db88caeeb40a8f710ac30416d93)), closes [#30613](https://github.com/ionic-team/ionic-framework/issues/30613) + + + + + +## [8.7.8](https://github.com/ionic-team/ionic-framework/compare/v8.7.7...v8.7.8) (2025-10-29) + + +### Bug Fixes + +* **checkbox, toggle:** fire ionFocus and ionBlur ([#30733](https://github.com/ionic-team/ionic-framework/issues/30733)) ([54a1c86](https://github.com/ionic-team/ionic-framework/commit/54a1c86d6a5d533b0c8c2d18edc62454a7c17bab)) + + + + + +## [8.7.7](https://github.com/ionic-team/ionic-framework/compare/v8.7.6...v8.7.7) (2025-10-15) + + +### Bug Fixes + +* **header:** ensure one banner role in condensed header ([#30718](https://github.com/ionic-team/ionic-framework/issues/30718)) ([12084af](https://github.com/ionic-team/ionic-framework/commit/12084af163ed811b9c6bda3c7850fc0c53c60c7b)) +* **header:** prevent flickering during iOS page transitions ([#30705](https://github.com/ionic-team/ionic-framework/issues/30705)) ([820fa28](https://github.com/ionic-team/ionic-framework/commit/820fa2854331722d22efd0e38a1936117477967a)), closes [#25326](https://github.com/ionic-team/ionic-framework/issues/25326) +* **select:** improve screen reader announcement timing for validation errors ([#30723](https://github.com/ionic-team/ionic-framework/issues/30723)) ([03303d7](https://github.com/ionic-team/ionic-framework/commit/03303d73f0bfe2380ced7931525fc52fd8576367)) + + + + + +## [8.7.6](https://github.com/ionic-team/ionic-framework/compare/v8.7.5...v8.7.6) (2025-10-08) + + +### Bug Fixes + +* **tabs:** respect stencil lifecycle order for tab selection ([#30702](https://github.com/ionic-team/ionic-framework/issues/30702)) ([7bb9535](https://github.com/ionic-team/ionic-framework/commit/7bb9535f601d2469ce60687a9c03f8b1cfe4aba4)), closes [#30611](https://github.com/ionic-team/ionic-framework/issues/30611) + + + + + +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) + + + + + +## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) + + +### Bug Fixes + +* **input:** improve error text accessibility ([#30635](https://github.com/ionic-team/ionic-framework/issues/30635)) ([c339bc3](https://github.com/ionic-team/ionic-framework/commit/c339bc36827b62ef871325869a9a5db9b17ac785)) +* **overlays,picker:** remove invalid aria-hidden attribute ([#30563](https://github.com/ionic-team/ionic-framework/issues/30563)) ([49f96d7](https://github.com/ionic-team/ionic-framework/commit/49f96d7f1e9050a95e3e33a821c0467ecc0bed64)), closes [#30040](https://github.com/ionic-team/ionic-framework/issues/30040) +* **segment-view:** scroll and select the right item when the component is in RTL context; ([#30675](https://github.com/ionic-team/ionic-framework/issues/30675)) ([66f517d](https://github.com/ionic-team/ionic-framework/commit/66f517d5b2154fff00b294a78f4107f057a580c6)), closes [#30079](https://github.com/ionic-team/ionic-framework/issues/30079) + + + + + +## [8.7.3](https://github.com/ionic-team/ionic-framework/compare/v8.7.2...v8.7.3) (2025-08-20) + + +### Bug Fixes + +* **checkbox:** add aria attributes to ignore checkbox icon ([#30633](https://github.com/ionic-team/ionic-framework/issues/30633)) ([e9e6605](https://github.com/ionic-team/ionic-framework/commit/e9e6605862a05a46d26c26a144ed1cf22133a2b7)), closes [#30231](https://github.com/ionic-team/ionic-framework/issues/30231) +* **refresher:** prevent focus-related scroll jumps on refresh ([#30636](https://github.com/ionic-team/ionic-framework/issues/30636)) ([1899b49](https://github.com/ionic-team/ionic-framework/commit/1899b49d252abc6003f763cea8db2a51efa941ec)) + + + + + ## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06) diff --git a/core/Dockerfile b/core/Dockerfile index ab667d50f97..50a4687701c 100644 --- a/core/Dockerfile +++ b/core/Dockerfile @@ -1,5 +1,5 @@ # Get Playwright -FROM mcr.microsoft.com/playwright:v1.54.2 +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 1fd9f61c2c9..8d6417b6bae 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,20 +1,20 @@ { "name": "@ionic/core", - "version": "8.7.2", + "version": "8.7.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.7.2", + "version": "8.7.10", "license": "MIT", "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "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", @@ -22,7 +22,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.54.2", + "@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", @@ -30,7 +30,7 @@ "@stencil/sass": "^3.0.9", "@stencil/vue-output-target": "0.10.8", "@types/jest": "^29.5.6", - "@types/node": "^14.6.0", + "@types/node": "^16.18.126", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", "chalk": "^5.3.0", @@ -57,12 +57,12 @@ "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, "dependencies": { - "axe-core": "~4.10.3" + "axe-core": "~4.11.0" }, "peerDependencies": { "playwright-core": ">= 1.0.0" @@ -663,9 +663,9 @@ "dev": true }, "node_modules/@capacitor/core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz", - "integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz", + "integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==", "dev": true, "dependencies": { "tslib": "^2.1.0" @@ -681,18 +681,18 @@ } }, "node_modules/@capacitor/keyboard": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz", - "integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz", + "integrity": "sha512-BIBKjmky5rOYNhvYhNeDi0MMvjwYZ6YF9JoCYcGKvKY+XLJKtezsEL78XfOlgWZBkbfR8uq3tzktY6PqgoYLKA==", "dev": true, "peerDependencies": { "@capacitor/core": ">=7.0.0" } }, "node_modules/@capacitor/status-bar": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz", - "integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.3.tgz", + "integrity": "sha512-JyRpVnKwHij9hgPWolF6PK+HT3e2HSPjN11/h2OmKxq8GAdPGARFLv+97eZl0pvuvm0Kka/LpiLb5whXISBg7Q==", "dev": true, "peerDependencies": { "@capacitor/core": ">=7.0.0" @@ -1715,12 +1715,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "dependencies": { - "playwright": "1.54.2" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -1914,10 +1914,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==", - "license": "MIT", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", + "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "bin": { "stencil": "bin/stencil" }, @@ -2112,10 +2111,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.14.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", - "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==", - "dev": true + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", @@ -3034,9 +3034,9 @@ } }, "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, "engines": { "node": ">=4" @@ -3474,9 +3474,9 @@ ] }, "node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -8593,12 +8593,12 @@ } }, "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "dependencies": { - "playwright-core": "1.54.2" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -8611,9 +8611,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "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, "bin": { "playwright-core": "cli.js" @@ -10653,12 +10653,12 @@ }, "dependencies": { "@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, "requires": { - "axe-core": "~4.10.3" + "axe-core": "~4.11.0" } }, "@babel/code-frame": { @@ -11102,9 +11102,9 @@ "dev": true }, "@capacitor/core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz", - "integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz", + "integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -11118,16 +11118,16 @@ "requires": {} }, "@capacitor/keyboard": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz", - "integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz", + "integrity": "sha512-BIBKjmky5rOYNhvYhNeDi0MMvjwYZ6YF9JoCYcGKvKY+XLJKtezsEL78XfOlgWZBkbfR8uq3tzktY6PqgoYLKA==", "dev": true, "requires": {} }, "@capacitor/status-bar": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz", - "integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.3.tgz", + "integrity": "sha512-JyRpVnKwHij9hgPWolF6PK+HT3e2HSPjN11/h2OmKxq8GAdPGARFLv+97eZl0pvuvm0Kka/LpiLb5whXISBg7Q==", "dev": true, "requires": {} }, @@ -11863,12 +11863,12 @@ } }, "@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "requires": { - "playwright": "1.54.2" + "playwright": "1.56.1" } }, "@rollup/plugin-node-resolve": { @@ -11984,9 +11984,9 @@ "requires": {} }, "@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", @@ -12146,9 +12146,9 @@ "dev": true }, "@types/node": { - "version": "14.14.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", - "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==", + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", "dev": true }, "@types/normalize-package-data": { @@ -12771,9 +12771,9 @@ } }, "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 }, "babel-jest": { @@ -13076,9 +13076,9 @@ "dev": true }, "chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true }, "chalk-template": { @@ -16812,19 +16812,19 @@ } }, "playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.54.2" + "playwright-core": "1.56.1" } }, "playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "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 }, "postcss": { diff --git a/core/package.json b/core/package.json index 7a09435adbe..8ac9d3c7620 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.7.2", + "version": "8.7.10", "description": "Base components for Ionic", "keywords": [ "ionic", @@ -31,12 +31,12 @@ "loader/" ], "dependencies": { - "@stencil/core": "4.36.2", + "@stencil/core": "4.38.0", "ionicons": "^8.0.13", "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", @@ -44,7 +44,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.54.2", + "@playwright/test": "^1.56.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", "@stencil/angular-output-target": "^0.10.0", @@ -52,7 +52,7 @@ "@stencil/sass": "^3.0.9", "@stencil/vue-output-target": "0.10.8", "@types/jest": "^29.5.6", - "@types/node": "^14.6.0", + "@types/node": "^16.18.126", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", "chalk": "^5.3.0", diff --git a/core/src/components/accordion/accordion.tsx b/core/src/components/accordion/accordion.tsx index 92d28848d20..6facef87a46 100644 --- a/core/src/components/accordion/accordion.tsx +++ b/core/src/components/accordion/accordion.tsx @@ -38,7 +38,40 @@ const enum AccordionState { }) export class Accordion implements ComponentInterface { private accordionGroupEl?: HTMLIonAccordionGroupElement | null; - private updateListener = () => this.updateState(false); + private accordionGroupUpdateHandler = () => { + /** + * Determine if this update will cause an actual state change. + * We only want to mark as "interacted" if the state is changing. + */ + const accordionGroup = this.accordionGroupEl; + if (accordionGroup) { + const value = accordionGroup.value; + const accordionValue = this.value; + const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue; + const isExpanded = this.state === AccordionState.Expanded || this.state === AccordionState.Expanding; + const stateWillChange = shouldExpand !== isExpanded; + + /** + * Only mark as interacted if: + * 1. This is not the first update we've received with a defined value + * 2. The state is actually changing (prevents redundant updates from enabling animations) + */ + if (this.hasReceivedFirstUpdate && stateWillChange) { + this.hasInteracted = true; + } + + /** + * Only count this as the first update if the group value is defined. + * This prevents the initial undefined value from the group's componentDidLoad + * from being treated as the first real update. + */ + if (value !== undefined) { + this.hasReceivedFirstUpdate = true; + } + } + + this.updateState(); + }; private contentEl: HTMLDivElement | undefined; private contentElWrapper: HTMLDivElement | undefined; private headerEl: HTMLDivElement | undefined; @@ -50,6 +83,25 @@ export class Accordion implements ComponentInterface { @State() state: AccordionState = AccordionState.Collapsed; @State() isNext = false; @State() isPrevious = false; + /** + * Tracks whether a user-initiated interaction has occurred. + * Animations are disabled until the first interaction happens. + * This prevents the accordion from animating when it's programmatically + * set to an expanded or collapsed state on initial load. + */ + @State() hasInteracted = false; + + /** + * Tracks if this accordion has ever been expanded. + * Used to prevent the first expansion from animating. + */ + private hasEverBeenExpanded = false; + + /** + * Tracks if this accordion has received its first update from the group. + * Used to distinguish initial programmatic sets from user interactions. + */ + private hasReceivedFirstUpdate = false; /** * The value of the accordion. Defaults to an autogenerated @@ -88,15 +140,15 @@ export class Accordion implements ComponentInterface { connectedCallback() { const accordionGroupEl = (this.accordionGroupEl = this.el?.closest('ion-accordion-group')); if (accordionGroupEl) { - this.updateState(true); - addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener); + this.updateState(); + addEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler); } } disconnectedCallback() { const accordionGroupEl = this.accordionGroupEl; if (accordionGroupEl) { - removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener); + removeEventListener(accordionGroupEl, 'ionValueChange', this.accordionGroupUpdateHandler); } } @@ -212,10 +264,16 @@ export class Accordion implements ComponentInterface { ionItem.appendChild(iconEl); }; - private expandAccordion = (initialUpdate = false) => { + private expandAccordion = () => { const { contentEl, contentElWrapper } = this; - if (initialUpdate || contentEl === undefined || contentElWrapper === undefined) { + + /** + * If the content elements aren't available yet, just set the state. + * This happens on initial render before the DOM is ready. + */ + if (contentEl === undefined || contentElWrapper === undefined) { this.state = AccordionState.Expanded; + this.hasEverBeenExpanded = true; return; } @@ -227,6 +285,12 @@ export class Accordion implements ComponentInterface { cancelAnimationFrame(this.currentRaf); } + /** + * Mark that this accordion has been expanded at least once. + * This allows subsequent expansions to animate. + */ + this.hasEverBeenExpanded = true; + if (this.shouldAnimate()) { raf(() => { this.state = AccordionState.Expanding; @@ -247,9 +311,14 @@ export class Accordion implements ComponentInterface { } }; - private collapseAccordion = (initialUpdate = false) => { + private collapseAccordion = () => { const { contentEl } = this; - if (initialUpdate || contentEl === undefined) { + + /** + * If the content element isn't available yet, just set the state. + * This happens on initial render before the DOM is ready. + */ + if (contentEl === undefined) { this.state = AccordionState.Collapsed; return; } @@ -291,6 +360,19 @@ export class Accordion implements ComponentInterface { * of what is set in the config. */ private shouldAnimate = () => { + /** + * Don't animate until after the first user interaction. + * This prevents animations on initial load when accordions + * start in an expanded or collapsed state programmatically. + * + * Additionally, don't animate the very first expansion even if + * hasInteracted is true. This handles edge cases like React StrictMode + * where effects run twice and might incorrectly mark as interacted. + */ + if (!this.hasInteracted || !this.hasEverBeenExpanded) { + return false; + } + if (typeof (window as any) === 'undefined') { return false; } @@ -312,7 +394,7 @@ export class Accordion implements ComponentInterface { return true; }; - private updateState = async (initialUpdate = false) => { + private updateState = async () => { const accordionGroup = this.accordionGroupEl; const accordionValue = this.value; @@ -325,10 +407,10 @@ export class Accordion implements ComponentInterface { const shouldExpand = Array.isArray(value) ? value.includes(accordionValue) : value === accordionValue; if (shouldExpand) { - this.expandAccordion(initialUpdate); + this.expandAccordion(); this.isNext = this.isPrevious = false; } else { - this.collapseAccordion(initialUpdate); + this.collapseAccordion(); /** * When using popout or inset, @@ -386,6 +468,12 @@ export class Accordion implements ComponentInterface { if (disabled || readonly) return; + /** + * Mark that the user has interacted with the accordion. + * This enables animations for all future state changes. + */ + this.hasInteracted = true; + if (accordionGroupEl) { /** * Because the accordion group may or may 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/action-sheet/test/a11y/action-sheet.e2e.ts-snapshots/action-sheet-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts-snapshots/action-sheet-scale-md-ltr-Mobile-Chrome-linux.png index 8a152f330e4..094a219aa3c 100644 Binary files a/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts-snapshots/action-sheet-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts-snapshots/action-sheet-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png index 4e8222cdfe2..82f5a675ff2 100644 Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png index ef47a09dfc6..fbab7b3c127 100644 Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/back-button/test/a11y/back-button.e2e.ts-snapshots/back-button-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/back-button/test/a11y/back-button.e2e.ts-snapshots/back-button-scale-md-ltr-Mobile-Chrome-linux.png index 2b6d0f39bd2..f2cf349aaf3 100644 Binary files a/core/src/components/back-button/test/a11y/back-button.e2e.ts-snapshots/back-button-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/back-button/test/a11y/back-button.e2e.ts-snapshots/back-button-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 47326abfea8..9eecd0d2c62 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -361,11 +361,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf target, }; let fill = this.fill; - /** - * We check both undefined and null to - * work around https://github.com/ionic-team/stencil/issues/3586. - */ - if (fill == null) { + if (fill === undefined) { fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid'; } diff --git a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-clear-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-clear-scale-md-ltr-Mobile-Chrome-linux.png index 13dc91e2521..edf6210af61 100644 Binary files a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-clear-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-clear-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-default-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-default-scale-md-ltr-Mobile-Chrome-linux.png index 9d9e9a85d17..7b06b904fce 100644 Binary files a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-default-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-default-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-small-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-small-scale-md-ltr-Mobile-Chrome-linux.png index fd39f0f8a10..8f8583f4a11 100644 Binary files a/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-small-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/button/test/a11y/button.e2e.ts-snapshots/button-small-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-clear-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-clear-scale-md-ltr-Mobile-Chrome-linux.png index d68ecf91bf5..20394301b2e 100644 Binary files a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-clear-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-clear-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-default-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-default-scale-md-ltr-Mobile-Chrome-linux.png index 88dec93214c..df4464bb826 100644 Binary files a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-default-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-default-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-icon-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-icon-scale-md-ltr-Mobile-Chrome-linux.png index b9b88ff84ef..a556b1b9abe 100644 Binary files a/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-icon-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/buttons/test/a11y/buttons.e2e.ts-snapshots/buttons-icon-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/card/test/a11y/card.e2e.ts-snapshots/card-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/card/test/a11y/card.e2e.ts-snapshots/card-scale-md-ltr-Mobile-Chrome-linux.png index aada6cb715d..87dcc890334 100644 Binary files a/core/src/components/card/test/a11y/card.e2e.ts-snapshots/card-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/card/test/a11y/card.e2e.ts-snapshots/card-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 4a9132665fd..abd289c2635 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'; @@ -34,8 +35,8 @@ export class Checkbox implements ComponentInterface { private inputLabelId = `${this.inputId}-lbl`; private helperTextId = `${this.inputId}-helper-text`; private errorTextId = `${this.inputId}-error-text`; - private focusEl?: HTMLElement; private inheritedAttributes: Attributes = {}; + private validationObserver?: MutationObserver; @Element() el!: HTMLIonCheckboxElement; @@ -121,6 +122,13 @@ export class Checkbox implements ComponentInterface { */ @Prop() required = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextId?: string; + /** * Emitted when the checked property has changed as a result of a user action such as a click. * @@ -138,18 +146,69 @@ export class Checkbox implements ComponentInterface { */ @Event() ionBlur!: EventEmitter; + connectedCallback() { + const { el } = this; + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextId = this.getHintTextId(); + }); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(el); + } + componentWillLoad() { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), }; + + this.hintTextId = this.getHintTextId(); + } + + disconnectedCallback() { + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } /** @internal */ @Method() async setFocus() { - if (this.focusEl) { - this.focusEl.focus(); - } + this.el.focus(); } /** @@ -169,7 +228,6 @@ export class Checkbox implements ComponentInterface { private toggleChecked = (ev: Event) => { ev.preventDefault(); - this.setFocus(); this.setChecked(!this.checked); this.indeterminate = false; }; @@ -207,10 +265,10 @@ export class Checkbox implements ComponentInterface { ev.stopPropagation(); }; - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + private getHintTextId(): string | undefined { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -226,7 +284,7 @@ export class Checkbox implements ComponentInterface { * This element should only be rendered if hint text is set. */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; /** * undefined and empty string values should @@ -239,11 +297,11 @@ export class Checkbox implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); @@ -278,13 +336,17 @@ export class Checkbox implements ComponentInterface {
- +
diff --git a/core/src/components/checkbox/test/a11y/checkbox.e2e.ts-snapshots/checkbox-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/a11y/checkbox.e2e.ts-snapshots/checkbox-scale-md-ltr-Mobile-Chrome-linux.png index fbede37e147..90d597d391b 100644 Binary files a/core/src/components/checkbox/test/a11y/checkbox.e2e.ts-snapshots/checkbox-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/checkbox/test/a11y/checkbox.e2e.ts-snapshots/checkbox-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts index 159e86f9a30..733f229e4b6 100644 --- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts @@ -44,7 +44,10 @@ configs().forEach(({ title, screenshot, config }) => { }); }); -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { test.describe(title('checkbox: ionChange'), () => { test('should fire ionChange when interacting with checkbox', async ({ page }) => { await page.setContent( @@ -133,4 +136,195 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(clickCount).toBe(1); }); }); + + test.describe(title('checkbox: ionFocus'), () => { + test('should not have visual regressions', async ({ page, pageUtils }) => { + await page.setContent( + ` + + +
+ Unchecked +
+ `, + config + ); + + await pageUtils.pressKeys('Tab'); + + const container = page.locator('#container'); + + await expect(container).toHaveScreenshot(screenshot(`checkbox-focus`)); + }); + + test('should not have visual regressions when interacting with checkbox in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + Unchecked + + `, + config + ); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + const item = page.locator('ion-item'); + + await expect(item).toHaveScreenshot(screenshot(`checkbox-in-item-focus`)); + }); + + test('should fire ionFocus when checkbox is focused', async ({ page, pageUtils }) => { + await page.setContent( + ` + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + expect(ionFocus).toHaveReceivedEventTimes(1); + + // Reset focus + const checkbox = page.locator('ion-checkbox'); + const checkboxBoundingBox = (await checkbox.boundingBox())!; + await page.mouse.click(0, checkboxBoundingBox.height + 1); + + // Test focus with click + await checkbox.click(); + + expect(ionFocus).toHaveReceivedEventTimes(2); + }); + + test('should fire ionFocus when interacting with checkbox in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + + // Test focus with keyboard navigation + await pageUtils.pressKeys('Tab'); + + expect(ionFocus).toHaveReceivedEventTimes(1); + + // Verify that the event target is the checkbox and not the item + const eventByKeyboard = ionFocus.events[0]; + expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox'); + + // Reset focus + const checkbox = page.locator('ion-checkbox'); + const checkboxBoundingBox = (await checkbox.boundingBox())!; + await page.mouse.click(0, checkboxBoundingBox.height + 1); + + // Test focus with click + const item = page.locator('ion-item'); + await item.click(); + + expect(ionFocus).toHaveReceivedEventTimes(2); + + // Verify that the event target is the checkbox and not the item + const eventByClick = ionFocus.events[0]; + expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox'); + }); + + test('should not fire when programmatically setting a value', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const ionFocus = await page.spyOnEvent('ionFocus'); + const checkbox = page.locator('ion-checkbox'); + + await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true)); + expect(ionFocus).not.toHaveReceivedEvent(); + }); + }); + + test.describe(title('checkbox: ionBlur'), () => { + test('should fire ionBlur when checkbox is blurred', async ({ page, pageUtils }) => { + await page.setContent( + ` + + `, + config + ); + + const ionBlur = await page.spyOnEvent('ionBlur'); + + // Test blur with keyboard navigation + // Focus the checkbox + await pageUtils.pressKeys('Tab'); + // Blur the checkbox + await pageUtils.pressKeys('Tab'); + + expect(ionBlur).toHaveReceivedEventTimes(1); + + // Test blur with click + const checkbox = page.locator('ion-checkbox'); + // Focus the checkbox + await checkbox.click(); + // Blur the checkbox by clicking outside of it + const checkboxBoundingBox = (await checkbox.boundingBox())!; + await page.mouse.click(0, checkboxBoundingBox.height + 1); + + expect(ionBlur).toHaveReceivedEventTimes(2); + }); + + test('should fire ionBlur after interacting with checkbox in item', async ({ page, pageUtils }) => { + await page.setContent( + ` + + + + `, + config + ); + + const ionBlur = await page.spyOnEvent('ionBlur'); + + // Test blur with keyboard navigation + // Focus the checkbox + await pageUtils.pressKeys('Tab'); + // Blur the checkbox + await pageUtils.pressKeys('Tab'); + + expect(ionBlur).toHaveReceivedEventTimes(1); + + // Verify that the event target is the checkbox and not the item + const event = ionBlur.events[0]; + expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox'); + + // Test blur with click + const item = page.locator('ion-item'); + await item.click(); + // Blur the checkbox by clicking outside of it + const itemBoundingBox = (await item.boundingBox())!; + await page.mouse.click(0, itemBoundingBox.height + 1); + + expect(ionBlur).toHaveReceivedEventTimes(2); + + // Verify that the event target is the checkbox and not the item + const eventByClick = ionBlur.events[0]; + expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox'); + }); + }); }); diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..45885364c12 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..305f4d89e1b Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..23f542f7ec4 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2c0dcdc0baa Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..9a511961639 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..8b59702aa81 Binary files /dev/null and b/core/src/components/checkbox/test/basic/checkbox.e2e.ts-snapshots/checkbox-in-item-focus-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/checkbox/test/basic/index.html b/core/src/components/checkbox/test/basic/index.html index 3380f82ddd1..54bc33c3db2 100644 --- a/core/src/components/checkbox/test/basic/index.html +++ b/core/src/components/checkbox/test/basic/index.html @@ -50,6 +50,20 @@ Specified width
Full-width
+ + diff --git a/core/src/components/checkbox/test/item/index.html b/core/src/components/checkbox/test/item/index.html index b2fa0bdccd1..2548cbfaad0 100644 --- a/core/src/components/checkbox/test/item/index.html +++ b/core/src/components/checkbox/test/item/index.html @@ -246,6 +246,20 @@

Multiline Label

+ + 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/chip/test/a11y/chip.e2e.ts-snapshots/chip-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/chip/test/a11y/chip.e2e.ts-snapshots/chip-scale-md-ltr-Mobile-Chrome-linux.png index c0fb06ff67b..0086d4a5bda 100644 Binary files a/core/src/components/chip/test/a11y/chip.e2e.ts-snapshots/chip-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/chip/test/a11y/chip.e2e.ts-snapshots/chip-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/content/test/standalone/content.e2e.ts-snapshots/content-standalone-md-ltr-Mobile-Chrome-linux.png b/core/src/components/content/test/standalone/content.e2e.ts-snapshots/content-standalone-md-ltr-Mobile-Chrome-linux.png index 148717952ab..0f5e28fd3ab 100644 Binary files a/core/src/components/content/test/standalone/content.e2e.ts-snapshots/content-standalone-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/content/test/standalone/content.e2e.ts-snapshots/content-standalone-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts index 76d6cd2d8aa..a626dd90855 100644 --- a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts +++ b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts @@ -1,7 +1,7 @@ -import { expect } from '@playwright/test'; import type { Locator } from '@playwright/test'; -import { configs, test } from '@utils/test/playwright'; +import { expect } from '@playwright/test'; import type { EventSpy } from '@utils/test/playwright'; +import { configs, test } from '@utils/test/playwright'; /** * This behavior does not vary across directions. @@ -176,5 +176,34 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await ionModalDidPresent.next(); await expect(datetime).toBeVisible(); }); + test('should set datetime ready state and keep calendar interactive when reopening modal', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30706', + }); + + const openAndInteract = async () => { + await page.click('#date-button'); + await ionModalDidPresent.next(); + + await page.locator('ion-datetime.datetime-ready').waitFor(); + + const calendarBody = datetime.locator('.calendar-body'); + await expect(calendarBody).toBeVisible(); + }; + + await openAndInteract(); + + const firstEnabledDay = datetime.locator('.calendar-day:not([disabled])').first(); + await firstEnabledDay.click(); + await page.waitForChanges(); + + await modal.evaluate((el: HTMLIonModalElement) => el.dismiss()); + await ionModalDidDismiss.next(); + + await openAndInteract(); + }); }); }); diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 2e3e111e56d..3411f28eb57 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -3,6 +3,7 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTa import { startFocusVisible } from '@utils/focus-visible'; import { getElementRoot, raf, renderHiddenInput } from '@utils/helpers'; import { printIonError, printIonWarning } from '@utils/logging'; +import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays'; import { isRTL } from '@utils/rtl'; import { createColorClasses } from '@utils/theme'; import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward } from 'ionicons/icons'; @@ -1100,6 +1101,32 @@ export class Datetime implements ComponentInterface { this.initializeKeyboardListeners(); } + /** + * TODO(FW-6931): Remove this fallback upon solving the root cause + * Fallback to ensure the datetime becomes ready even if + * IntersectionObserver never reports it as intersecting. + * + * This is primarily used in environments where the observer + * might not fire as expected, such as when running under + * synthetic tests that stub IntersectionObserver. + */ + private ensureReadyIfVisible = () => { + if (this.el.classList.contains('datetime-ready')) { + return; + } + + const rect = this.el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + return; + } + + this.initializeListeners(); + + writeTask(() => { + this.el.classList.add('datetime-ready'); + }); + }; + componentDidLoad() { const { el, intersectionTrackerRef } = this; @@ -1140,6 +1167,18 @@ export class Datetime implements ComponentInterface { */ raf(() => visibleIO?.observe(intersectionTrackerRef!)); + /** + * TODO(FW-6931): Remove this fallback upon solving the root cause + * Fallback: If IntersectionObserver never reports that the + * datetime is visible but the host clearly has layout, ensure + * we still initialize listeners and mark the component as ready. + * + * We schedule this after everything has had a chance to run. + */ + setTimeout(() => { + this.ensureReadyIfVisible(); + }, 100); + /** * We need to clean up listeners when the datetime is hidden * in a popover/modal so that we can properly scroll containers @@ -1598,7 +1637,7 @@ export class Datetime implements ComponentInterface { forcePresentation === 'time-date' ? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)] : [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)]; - return {renderArray}; + return {renderArray}; } private renderDatePickerColumns(forcePresentation: string) { diff --git a/core/src/components/datetime/test/a11y/datetime.e2e.ts-snapshots/datetime-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime/test/a11y/datetime.e2e.ts-snapshots/datetime-scale-md-ltr-Mobile-Chrome-linux.png index 173af1a0774..58771820175 100644 Binary files a/core/src/components/datetime/test/a11y/datetime.e2e.ts-snapshots/datetime-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/datetime/test/a11y/datetime.e2e.ts-snapshots/datetime-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime/test/basic/datetime.e2e.ts b/core/src/components/datetime/test/basic/datetime.e2e.ts index 67812b6d020..6104d0014cf 100644 --- a/core/src/components/datetime/test/basic/datetime.e2e.ts +++ b/core/src/components/datetime/test/basic/datetime.e2e.ts @@ -394,6 +394,61 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => }); }); +/** + * Synthetic IntersectionObserver fallback behavior. + * + * This test stubs IntersectionObserver so that the callback + * never reports an intersecting entry. The datetime should + * still become ready via its internal fallback logic. + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('datetime: IO fallback'), () => { + test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30706', + }); + + await page.addInitScript(() => { + const OriginalIO = window.IntersectionObserver; + (window as any).IntersectionObserver = function (callback: any, options: any) { + const instance = new OriginalIO(() => {}, options); + const originalObserve = instance.observe.bind(instance); + + instance.observe = (target: Element) => { + originalObserve(target); + callback([ + { + isIntersecting: false, + target, + } as IntersectionObserverEntry, + ]); + }; + + return instance; + } as any; + }); + + await page.setContent( + ` + + `, + config + ); + + const datetime = page.locator('ion-datetime'); + + // Give the fallback a short amount of time to run + await page.waitForTimeout(100); + + await expect(datetime).toHaveClass(/datetime-ready/); + + const calendarBody = datetime.locator('.calendar-body'); + await expect(calendarBody).toBeVisible(); + }); + }); +}); + /** * We are setting RTL on the component * instead, so we don't need to test diff --git a/core/src/components/datetime/test/locale/datetime.e2e.ts-snapshots/datetime-locale-ja-JP-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/datetime/test/locale/datetime.e2e.ts-snapshots/datetime-locale-ja-JP-diff-ios-ltr-Mobile-Safari-linux.png index 3437e3bc6db..2770cb15bb3 100644 Binary files a/core/src/components/datetime/test/locale/datetime.e2e.ts-snapshots/datetime-locale-ja-JP-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/datetime/test/locale/datetime.e2e.ts-snapshots/datetime-locale-ja-JP-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss index cda084da7c4..0c0f2007d42 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; } @@ -93,6 +100,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 6b2102a7db3..ab93fada783 100644 --- a/core/src/components/header/header.tsx +++ b/core/src/components/header/header.tsx @@ -15,6 +15,7 @@ import { handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, + getRoleType, } from './header.utils'; /** @@ -208,9 +209,10 @@ export class Header implements ComponentInterface { const { translucent, inheritedAttributes } = this; const mode = getIonMode(this); const collapse = this.collapse || 'none'; + const isCondensed = collapse === 'condense'; // banner role must be at top level, so remove role if inside a menu - const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner'; + const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode); 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 mode The current mode. + * @returns 'none' if inside ion-menu or if condensed in md + * mode, otherwise 'banner'. + */ +export const getRoleType = (isInsideMenu: boolean, isCondensed: boolean, mode: 'ios' | 'md') => { + // If the header is inside a menu, it should not have the banner role. + if (isInsideMenu) { + return ROLE_NONE; + } + /** + * Only apply role="none" to `md` mode condensed headers + * since the large header is never shown. + */ + if (isCondensed && mode === 'md') { + return ROLE_NONE; + } + // Default to banner role. + return ROLE_BANNER; +}; 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 69281c5e899..19c5a9d406f 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -14,7 +14,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 { createSlotMutationController } from '@utils/slot-mutation-controller'; @@ -79,8 +79,15 @@ export class Input implements ComponentInterface { */ @State() hasFocus = false; + /** + * Track validation state for proper aria-live announcements + */ + @State() isInvalid = false; + @Element() el!: HTMLIonInputElement; + private validationObserver?: MutationObserver; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -406,6 +413,26 @@ export class Input implements ComponentInterface { () => this.labelSlot ); + // Watch for class changes to update validation state + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + // Force a re-render to update aria-describedby immediately + forceUpdate(this); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(el); + this.debounceChanged(); if (Build.isBrowser) { document.dispatchEvent( @@ -451,6 +478,12 @@ export class Input implements ComponentInterface { this.notchController.destroy(); this.notchController = undefined; } + + // Clean up validation observer to prevent memory leaks + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } /** @@ -626,22 +659,22 @@ export class Input 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} + , ]; } private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -864,7 +897,7 @@ export class Input implements ComponentInterface { onCompositionstart={this.onCompositionStart} onCompositionend={this.onCompositionEnd} aria-describedby={this.getHintTextID()} - aria-invalid={this.getHintTextID() === this.errorTextId} + aria-invalid={this.isInvalid ? 'true' : undefined} {...this.inheritedAttributes} /> {this.clearInput && !readonly && !disabled && ( diff --git a/core/src/components/input/test/a11y/input.e2e.ts-snapshots/input-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input/test/a11y/input.e2e.ts-snapshots/input-scale-md-ltr-Mobile-Chrome-linux.png index cd5c32698ab..f9c0034e922 100644 Binary files a/core/src/components/input/test/a11y/input.e2e.ts-snapshots/input-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/input/test/a11y/input.e2e.ts-snapshots/input-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png index f210e56d1ce..84852e16478 100644 Binary files a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png index f210e56d1ce..84852e16478 100644 Binary files a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/validation/index.html b/core/src/components/input/test/validation/index.html new file mode 100644 index 00000000000..2a6ad89e13f --- /dev/null +++ b/core/src/components/input/test/validation/index.html @@ -0,0 +1,284 @@ + + + + + Input - Validation + + + + + + + + + + + + + + Input - 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 Email Field

+ +
+ +
+

Required Name Field

+ +
+ +
+

Phone Number (Pattern Validation)

+ +
+ +
+

Password (Min Length)

+ +
+ +
+

Age (Number Range)

+ +
+ +
+

Optional Field (No Validation)

+ +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-headings-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-headings-scale-md-ltr-Mobile-Chrome-linux.png index cf8be874a28..6efdef8be40 100644 Binary files a/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-headings-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-headings-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-scale-md-ltr-Mobile-Chrome-linux.png index 167558ec8ed..36a4b4672a5 100644 Binary files a/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item-divider/test/a11y/item-divider.e2e.ts-snapshots/item-divider-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item-sliding/test/a11y/item-sliding.e2e.ts-snapshots/item-sliding-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item-sliding/test/a11y/item-sliding.e2e.ts-snapshots/item-sliding-scale-md-ltr-Mobile-Chrome-linux.png index fd8582ceaf4..b15e8bf05cd 100644 Binary files a/core/src/components/item-sliding/test/a11y/item-sliding.e2e.ts-snapshots/item-sliding-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item-sliding/test/a11y/item-sliding.e2e.ts-snapshots/item-sliding-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-buttons-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-buttons-scale-md-ltr-Mobile-Chrome-linux.png index 8998c48f8a4..9350d2910f2 100644 Binary files a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-buttons-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-buttons-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-detail-icon-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-detail-icon-scale-md-ltr-Mobile-Chrome-linux.png index 8f386636a08..5d6765747e1 100644 Binary files a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-detail-icon-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-detail-icon-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-icons-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-icons-scale-md-ltr-Mobile-Chrome-linux.png index 75552763262..92cfcf4649c 100644 Binary files a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-icons-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-icons-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-scale-md-ltr-Mobile-Chrome-linux.png index 0a9b23a5ddc..e3ac84cde13 100644 Binary files a/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/a11y/item.e2e.ts-snapshots/item-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-headings-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-headings-scale-md-ltr-Mobile-Chrome-linux.png index 0d03b926f8d..20826fa6040 100644 Binary files a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-headings-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-headings-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-stacked-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-stacked-scale-md-ltr-Mobile-Chrome-linux.png index 577a599c7d8..b958ef4ce54 100644 Binary files a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-stacked-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-stacked-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-wrap-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-wrap-scale-md-ltr-Mobile-Chrome-linux.png index ce4cc8f935b..da520d869d6 100644 Binary files a/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-wrap-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/label/test/a11y/label.e2e.ts-snapshots/label-wrap-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/list-header/test/a11y/list-header.e2e.ts-snapshots/list-header-default-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/list-header/test/a11y/list-header.e2e.ts-snapshots/list-header-default-scale-md-ltr-Mobile-Chrome-linux.png index 05816fc84af..7c2a4571fde 100644 Binary files a/core/src/components/list-header/test/a11y/list-header.e2e.ts-snapshots/list-header-default-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/list-header/test/a11y/list-header.e2e.ts-snapshots/list-header-default-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/loading/test/a11y/loading.e2e.ts-snapshots/loading-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/loading/test/a11y/loading.e2e.ts-snapshots/loading-scale-md-ltr-Mobile-Chrome-linux.png index 59d1c981010..45c10031cae 100644 Binary files a/core/src/components/loading/test/a11y/loading.e2e.ts-snapshots/loading-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/loading/test/a11y/loading.e2e.ts-snapshots/loading-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 6306801ce50..4769f609914 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -418,6 +418,9 @@ export class Menu implements ComponentInterface, MenuI { */ @Method() setOpen(shouldOpen: boolean, animated = true, role?: string): Promise { + // Blur the active element to prevent it from being kept focused inside an element that will be set with aria-hidden="true" + (document.activeElement as HTMLElement)?.blur(); + return menuController._setOpen(this, shouldOpen, animated, role); } diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 219769e4879..afac8f3d3ed 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -95,6 +95,12 @@ export const createSheetGesture = ( const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation'); const enableBackdrop = () => { + // Respect explicit opt-out of focus trapping/backdrop interactions + // If focusTrap is false or showBackdrop is false, do not enable the backdrop or re-enable focus trap + const el = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean }; + if (el.focusTrap === false || el.showBackdrop === false) { + return; + } baseEl.style.setProperty('pointer-events', 'auto'); backdropEl.style.setProperty('pointer-events', 'auto'); @@ -235,7 +241,10 @@ export const createSheetGesture = ( * ion-backdrop and .modal-wrapper always have pointer-events: auto * applied, so the modal content can still be interacted with. */ - const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint; + const shouldEnableBackdrop = + currentBreakpoint > backdropBreakpoint && + (baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false && + (baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false; if (shouldEnableBackdrop) { enableBackdrop(); } else { @@ -582,7 +591,10 @@ export const createSheetGesture = ( * Backdrop should become enabled * after the backdropBreakpoint value */ - const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint; + const shouldEnableBackdrop = + currentBreakpoint > backdropBreakpoint && + (baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false && + (baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false; if (shouldEnableBackdrop) { enableBackdrop(); } else { diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png index 5fa19d7ac60..5b93a9766fc 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-scale-md-ltr-Mobile-Chrome-linux.png index 2dffcda2d60..836195d2734 100644 Binary files a/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-wrapping-label-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-wrapping-label-scale-md-ltr-Mobile-Chrome-linux.png index e70662216b3..a48900f259f 100644 Binary files a/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-wrapping-label-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/note/test/a11y/note.e2e.ts-snapshots/note-wrapping-label-scale-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/picker-column-option.scss b/core/src/components/picker-column-option/picker-column-option.scss index a414a3580ed..e34a301d77d 100644 --- a/core/src/components/picker-column-option/picker-column-option.scss +++ b/core/src/components/picker-column-option/picker-column-option.scss @@ -3,7 +3,7 @@ // Picker Column // -------------------------------------------------- -button { +.picker-column-option-button { @include padding(0); @include margin(0); @@ -40,6 +40,6 @@ button { opacity: 0.4; } -:host(.option-disabled) button { +:host(.option-disabled) .picker-column-option-button { cursor: default; } diff --git a/core/src/components/picker-column-option/picker-column-option.tsx b/core/src/components/picker-column-option/picker-column-option.tsx index 87b828574f8..86bed9bf3eb 100644 --- a/core/src/components/picker-column-option/picker-column-option.tsx +++ b/core/src/components/picker-column-option/picker-column-option.tsx @@ -124,9 +124,9 @@ export class PickerColumnOption implements ComponentInterface { ['option-disabled']: disabled, })} > - +
); } diff --git a/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts b/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts index aef2df2c7e9..6ba18db56a8 100644 --- a/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts +++ b/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts @@ -10,7 +10,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => { test('should not have accessibility violations', async ({ page }) => { await page.goto(`/src/components/picker-column-option/test/a11y`, config); - const results = await new AxeBuilder({ page }).analyze(); + const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze(); expect(results.violations).toEqual([]); }); diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png index 8d03c748200..df0b9ca392e 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png index 41ff9101e06..61755c3da00 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Safari-linux.png index 8f8266fcb1f..30e2f7d4d1b 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Chrome-linux.png index 3c980dc1823..8c07722126e 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Firefox-linux.png index 731292dc074..5b674c5ee64 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Safari-linux.png index 4b07f2ccd11..d6f6de0778c 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/active-picker-column-option-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png index 1dbd4a7aeff..564e9b8e209 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png index 605008bbe31..e271489c570 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Safari-linux.png index bde6e1f8260..30e2f7d4d1b 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Chrome-linux.png index f775d8efde1..cfb5ad0b7b2 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Firefox-linux.png index 77afbe2d8c0..5a976495500 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Safari-linux.png index 2da927e1a8c..d6f6de0778c 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-active-picker-column-option-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Chrome-linux.png index 1dbd4a7aeff..564e9b8e209 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Firefox-linux.png index 605008bbe31..e271489c570 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Safari-linux.png index bde6e1f8260..30e2f7d4d1b 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Chrome-linux.png index 111fc55cc64..3dd7bd4974b 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Firefox-linux.png index 27505e326f4..3be42705447 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Safari-linux.png index 540367aac65..3a2ad242b85 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/disabled-picker-column-option-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Chrome-linux.png index 8d03c748200..df0b9ca392e 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Firefox-linux.png index 41ff9101e06..61755c3da00 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Safari-linux.png index 8f8266fcb1f..30e2f7d4d1b 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Chrome-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Chrome-linux.png index da4362bd859..e540ce119a8 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Firefox-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Firefox-linux.png index 1c3f1143046..df1d4a1d9e3 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Safari-linux.png b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Safari-linux.png index ca81bc41179..3a2ad242b85 100644 Binary files a/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Safari-linux.png and b/core/src/components/picker-column-option/test/basic/picker-column-option.e2e.ts-snapshots/picker-column-option-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/picker-column-option/test/picker-column-option.spec.ts b/core/src/components/picker-column-option/test/picker-column-option.spec.ts index 2f33a3359a4..e60f60522df 100644 --- a/core/src/components/picker-column-option/test/picker-column-option.spec.ts +++ b/core/src/components/picker-column-option/test/picker-column-option.spec.ts @@ -3,7 +3,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { PickerColumnOption } from '../picker-column-option'; describe('picker column option', () => { - it('button should be enabled by default', async () => { + it('should be enabled by default', async () => { const page = await newSpecPage({ components: [PickerColumnOption], html: ` @@ -12,12 +12,11 @@ describe('picker column option', () => { }); const option = page.body.querySelector('ion-picker-column-option')!; - const button = option.shadowRoot!.querySelector('button')!; - await expect(button.hasAttribute('disabled')).toEqual(false); + await expect(option.classList.contains('option-disabled')).toEqual(false); }); - it('button should be disabled if specified', async () => { + it('should be disabled if specified', async () => { const page = await newSpecPage({ components: [PickerColumnOption], html: ` @@ -26,8 +25,7 @@ describe('picker column option', () => { }); const option = page.body.querySelector('ion-picker-column-option')!; - const button = option.shadowRoot!.querySelector('button')!; - await expect(button.hasAttribute('disabled')).toEqual(true); + await expect(option.classList.contains('option-disabled')).toEqual(true); }); }); diff --git a/core/src/components/picker-column/picker-column.tsx b/core/src/components/picker-column/picker-column.tsx index 3393cadf6e0..905ba8cf81e 100644 --- a/core/src/components/picker-column/picker-column.tsx +++ b/core/src/components/picker-column/picker-column.tsx @@ -653,39 +653,6 @@ export class PickerColumn implements ComponentInterface { return el ? el.getAttribute('aria-label') ?? el.innerText : ''; }; - /** - * Render an element that overlays the column. This element is for assistive - * tech to allow users to navigate the column up/down. This element should receive - * focus as it listens for synthesized keyboard events as required by the - * slider role: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/slider_role - */ - private renderAssistiveFocusable = () => { - const { activeItem } = this; - const valueText = this.getOptionValueText(activeItem); - - /** - * When using the picker, the valuetext provides important context that valuenow - * does not. Additionally, using non-zero valuemin/valuemax values can cause - * WebKit to incorrectly announce numeric valuetext values (such as a year - * like "2024") as percentages: https://bugs.webkit.org/show_bug.cgi?id=273126 - */ - return ( -
(this.assistiveFocusable = el)} - class="assistive-focusable" - role="slider" - tabindex={this.disabled ? undefined : 0} - aria-label={this.ariaLabel} - aria-valuemin={0} - aria-valuemax={0} - aria-valuenow={0} - aria-valuetext={valueText} - aria-orientation="vertical" - onKeyDown={(ev) => this.onKeyDown(ev)} - >
- ); - }; - render() { const { color, disabled, isActive, numericInput } = this; const mode = getIonMode(this); @@ -699,33 +666,21 @@ export class PickerColumn implements ComponentInterface { ['picker-column-disabled']: disabled, })} > - {this.renderAssistiveFocusable()}