diff --git a/.cspell.json b/.cspell.json index 498ef18b64..263351d4a1 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,4 +1,4 @@ { "import": "node_modules/@skyux/dev-infra-private/.cspell-shared.json", - "words": ["novalidate", "tabindex", "interactable", "fetchpriority"] + "words": ["novalidate", "tabindex", "interactable", "fetchpriority", "xlink"] } diff --git a/.eslintrc-overrides.json b/.eslintrc-overrides.json index 23197c1147..897244ae8d 100644 --- a/.eslintrc-overrides.json +++ b/.eslintrc-overrides.json @@ -38,7 +38,26 @@ { "files": ["*.html"], "rules": { - "@angular-eslint/template/button-has-type": ["error"] + "@angular-eslint/template/alt-text": ["warn"], + "@angular-eslint/template/attributes-order": ["warn"], + "@angular-eslint/template/button-has-type": ["error"], + "@angular-eslint/template/conditional-complexity": ["warn"], + "@angular-eslint/template/cyclomatic-complexity": ["warn"], + "@angular-eslint/template/elements-content": ["error"], + "@angular-eslint/template/interactive-supports-focus": ["warn"], + "@angular-eslint/template/label-has-associated-control": ["warn"], + "@angular-eslint/template/no-any": ["error"], + "@angular-eslint/template/no-call-expression": ["warn"], + "@angular-eslint/template/no-distracting-elements": ["warn"], + "@angular-eslint/template/no-inline-styles": ["warn"], + "@angular-eslint/template/no-interpolation-in-attributes": ["warn"], + "@angular-eslint/template/no-positive-tabindex": ["warn"], + "@angular-eslint/template/prefer-control-flow": ["warn"], + "@angular-eslint/template/prefer-ngsrc": ["warn"], + "@angular-eslint/template/prefer-self-closing-tags": ["warn"], + "@angular-eslint/template/role-has-required-aria": ["error"], + "@angular-eslint/template/use-track-by-function": ["warn"], + "@angular-eslint/template/valid-aria": ["error"] } } ] diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 95f84ab94a..ebe0c638c7 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -4,7 +4,7 @@ on: types: - closed branches: - - 7.x.x + - 10.x.x env: TARGET_BRANCH: main @@ -38,6 +38,7 @@ jobs: run: npm ci - name: Cherry pick + id: cherry-pick run: | # Set the git user to the author of the merge commit. git config user.name "$(git log -1 --pretty=format:'%an' ${{ github.event.pull_request.merge_commit_sha }})" @@ -72,6 +73,8 @@ jobs: echo "CHERRY_PICK_RESULT=failed" >> $GITHUB_ENV exit 0 fi + + echo "commit_message=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} @@ -81,6 +84,7 @@ jobs: github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} script: | const pr = context.payload.pull_request; + const title = ${{ toJson(steps.cherry-pick.outputs.commit_message) }}; let body = `:cherries: Cherry picked from #${pr.number} [${pr.title}](${pr.html_url})` const prAzureBoardLink = pr.body?.match(/(?<=\[)AB#\d+(?=])/g); if (prAzureBoardLink) { @@ -91,7 +95,7 @@ jobs: repo: context.repo.repo, head: process.env.CHERRY_PICK_BRANCH, base: process.env.TARGET_BRANCH, - title: `${pr.title} (#${pr.number})`, + title, body }).then(result => { console.log(`Created PR #${result.data.number}: ${result.data.html_url}`); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12344fb043..0b11d717fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,14 @@ on: pull_request: push: branches: - - main + - 10.x.x + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: - NX_CLOUD_DISTRIBUTED_EXECUTION: true + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} jobs: install-deps: @@ -39,8 +43,6 @@ jobs: - name: npm install if: steps.cache.outputs.cache-hit != 'true' run: npm ci - - name: Start Nx Cloud CI run - run: npx nx-cloud start-ci-run agents: name: Bootup Nx Cloud agent @@ -48,7 +50,7 @@ jobs: needs: install-deps strategy: matrix: - agent: [1, 2, 3, 4, 5, 6, 7, 8] + agent: [1, 2, 3] steps: - uses: actions/checkout@v4 - name: Retrieve node_modules cache @@ -59,33 +61,71 @@ jobs: - name: Start Nx Agent ${{ matrix.agent }} run: npx nx-cloud start-agent - lint: - name: Lint - needs: install-deps + build-coverage-lint: + name: Build, code coverage unit tests, and linting + if: ${{ !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} runs-on: ubuntu-latest + needs: install-deps steps: - uses: actions/checkout@v4 with: - # We need to fetch all branches and commits so that Nx affected has a base to compare against. fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - # Rebase must happen before installing dependencies. - - name: Rebase current branch - run: node ./scripts/rebase-pr.js - - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v4 + - run: npx nx-cloud start-ci-run --agent-count=3 --distribute-on="manual" - name: Retrieve node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} + - uses: nrwl/nx-set-shas@v4 + # Rebase must happen before installing dependencies. + - name: Rebase current branch + run: node ./scripts/rebase-pr.js + - name: Run build, lint, test, and posttest for npm packages + run: | + npx nx affected --target build lint test posttest --configuration ci --parallel 1 --exclude '*,!tag:npm' + - name: Run lint, test, and posttest for non-npm packages + run: | + npx nx affected --target lint test posttest --configuration ci --parallel 1 --exclude 'tag:npm' + - name: Stop + if: ${{ always() }} + run: npx nx-cloud stop-all-agents + + build: + name: Build + needs: build-coverage-lint + runs-on: ubuntu-latest + if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} + steps: + - name: Run build + run: | + [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Built. || false + + coverage: + name: Code coverage + needs: build-coverage-lint + runs-on: ubuntu-latest + if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} + steps: + - name: Run code coverage + run: | + [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Code covered. || false + + lint: + name: Lint + needs: build-coverage-lint + runs-on: ubuntu-latest + if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} + steps: - name: Run lint - run: npx nx affected:lint --quiet --silent --parallel=5 + run: | + [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Linted. || false check-workspace: name: Check dependencies and resources + if: ${{ !cancelled() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} needs: install-deps runs-on: ubuntu-latest steps: @@ -100,7 +140,7 @@ jobs: - name: Rebase current branch run: node ./scripts/rebase-pr.js - name: Retrieve node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} @@ -129,40 +169,13 @@ jobs: - name: Derive appropriate SHAs for base and head for `nx affected` commands uses: nrwl/nx-set-shas@v4 - name: Retrieve node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} - name: Check code formatting run: npx nx format:check - build: - name: Build - needs: install-deps - runs-on: ubuntu-latest - if: ${{ github.event_name == 'pull_request' }} - steps: - - uses: actions/checkout@v4 - with: - # We need to fetch all branches and commits so that Nx affected has a base to compare against. - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - # Rebase must happen before installing dependencies. - - name: Rebase current branch - run: node ./scripts/rebase-pr.js - - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v4 - - name: Retrieve node_modules cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} - - name: Build - run: | - npx nx affected --target=build --parallel=5 --exclude '*,!tag:npm' - build-dist: name: Build packages distribution needs: install-deps @@ -170,17 +183,11 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - uses: actions/checkout@v4 - with: - # We need to fetch all branches and commits so that Nx affected has a base to compare against. - fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - # Rebase must happen before installing dependencies. - - name: Rebase current branch - run: node ./scripts/rebase-pr.js - name: Retrieve node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} @@ -199,45 +206,18 @@ jobs: SLACK_CHANNEL: C01GY7ZP4HM SLACK_FOOTER: 'Blackbaud Sky Build User' - coverage: - name: Code coverage - needs: install-deps - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - # We need to fetch all branches and commits so that Nx affected has a base to compare against. - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - # Rebase must happen before installing dependencies. - - name: Rebase current branch - run: node ./scripts/rebase-pr.js - - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v4 - - name: Retrieve node_modules cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} - - name: Code coverage - run: | - npx nx affected --target=test,posttest --parallel=5 --configuration=ci - stop-agents: name: Stop Nx Cloud agents runs-on: ubuntu-latest if: ${{ !cancelled() }} - needs: - [install-deps, check-workspace, format, lint, build, build-dist, coverage] + needs: build-coverage-lint steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - name: Retrieve node_modules cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fa94842ff0..532ed53969 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,12 +6,17 @@ on: pull_request_target: push: branches: - - main + - 10.x.x workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name || github.event.ref }} + cancel-in-progress: false + env: CYPRESS_VERIFY_TIMEOUT: 120000 GH_PAGES_OWNER: blackbaud + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium PERCY_COMMIT: ${{ github.sha }} PERCY_SKIP_UPDATE_CHECK: 'true' @@ -93,6 +98,7 @@ jobs: strategy: # If one build fails, do not cancel other builds. fail-fast: false + max-parallel: 3 matrix: project: ${{ fromJSON(needs.install-deps.outputs.parameters).projects }} steps: @@ -119,7 +125,7 @@ jobs: key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} if: ${{ matrix.project != 'skip' }} - name: Build ${{ matrix.project }} - run: npx nx run ${{ matrix.project }}:build-storybook:ci + run: npx nx run ${{ matrix.project }}:build-storybook:ci --no-agents if: ${{ matrix.project != 'skip' }} - name: Upload storybook artifact uses: actions/upload-artifact@v4 @@ -175,7 +181,9 @@ jobs: if: ${{ fromJson(needs.install-deps.outputs.parameters).ghPagesRepo == 'skyux-pr-preview' }} - name: Build ${{ matrix.app }} run: | - npx nx build ${{ matrix.app }} --baseHref="https://blackbaud.github.io/skyux-pr-preview/${{ needs.install-deps.outputs.pr-number }}/${{ matrix.app }}/" + npx nx build ${{ matrix.app }} \ + --baseHref="https://blackbaud.github.io/skyux-pr-preview/${{ needs.install-deps.outputs.pr-number }}/${{ matrix.app }}/" \ + --no-agents if: ${{ fromJson(needs.install-deps.outputs.parameters).ghPagesRepo == 'skyux-pr-preview' && matrix.app != 'dep-graph' }} - name: Build ${{ matrix.app }} run: npx nx dep-graph --file=dist/apps/dep-graph/index.html @@ -248,7 +256,7 @@ jobs: --projectsJson='${{ fromJson(needs.install-deps.outputs.parameters).projectsJson }}' \ --baseUrl='../${{ fromJson(needs.install-deps.outputs.parameters).storybooksPath }}' - name: Build Storybook Composition - run: npx nx run storybook:build-storybook:ci --outputDir=dist/storybook + run: npx nx run storybook:build-storybook:ci --outputDir=dist/storybook --no-agents - name: Checkout ${{ fromJson(needs.install-deps.outputs.parameters).ghPagesRepo }} uses: actions/checkout@v4 with: @@ -272,9 +280,10 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.job }}--${{ matrix.project }}-${{ github.head_ref || format('{0}-{1}', github.run_id, github.run_attempt) }} cancel-in-progress: true - needs: install-deps + needs: [install-deps, build-storybook] strategy: fail-fast: false + max-parallel: 3 matrix: include: ${{ fromJSON(needs.install-deps.outputs.parameters).e2eTargets }} steps: @@ -322,7 +331,7 @@ jobs: echo '# PERCY_TARGET_COMMIT' $PERCY_TARGET_COMMIT # Timing setting recommended by https://docs.percy.io/docs/cypress#missing-assets - npx percy exec -t 350 -- nx e2e ${{ matrix.project }} -c ci 2>&1 | tee ${{ runner.temp }}/percy-${{ matrix.project }}.log + npx percy exec -t 350 -- nx e2e ${{ matrix.project }} -c ci --no-agents 2>&1 | tee ${{ runner.temp }}/percy-${{ matrix.project }}.log RESULT=$? if [ $RESULT -ne 0 ]; then echo "Percy failed with exit code $RESULT" @@ -330,7 +339,7 @@ jobs: if [ $RETRY -gt 0 ]; then echo "Percy client error. Retrying..." set -eo pipefail - npx percy exec -t 350 -- nx e2e ${{ matrix.project }} -c ci 2>&1 | tee ${{ runner.temp }}/percy-${{ matrix.project }}.log + npx percy exec -t 350 -- nx e2e ${{ matrix.project }} -c ci --no-agents 2>&1 | tee ${{ runner.temp }}/percy-${{ matrix.project }}.log else exit 1 fi @@ -412,7 +421,7 @@ jobs: /home/runner/.cache/Cypress key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} - name: Build e2e-schematics - run: npx nx build e2e-schematics + run: npx nx build e2e-schematics --no-agents - name: Download Percy Build Numbers uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index dfcde4497b..20a7f3b761 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: push: branches: - - main + - 10.x.x env: # Set to 'alpha', 'beta', or 'rc' to create a prerelease. PRERELEASE: 'false' diff --git a/.gitignore b/.gitignore index 2fe258590b..b93ca0dc6f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ Thumbs.db .nx/cache .nx/workspace-data +nx-cloud.env diff --git a/.skyuxdev.json b/.skyuxdev.json index 40267b8336..c020624a26 100644 --- a/.skyuxdev.json +++ b/.skyuxdev.json @@ -1,5 +1,5 @@ { - "baseBranch": "main", + "baseBranch": "10.x.x", "documentationExcludeProjects": [ "animations", "assets", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4195a3cacc..1fcce32f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,206 @@ # Changelog +## [10.42.0](https://github.com/blackbaud/skyux/compare/10.41.5...10.42.0) (2024-09-06) + + +### Features + +* **components/theme:** add support for @skyux/icons@7.8.0 ([#2678](https://github.com/blackbaud/skyux/issues/2678)) ([1997de3](https://github.com/blackbaud/skyux/commit/1997de3beb4b586a66dc78fc47f95c0fea74a885)) + + +### Bug Fixes + +* **components/forms:** stop requiring labelText in field group ([#2674](https://github.com/blackbaud/skyux/issues/2674)) ([a245e9a](https://github.com/blackbaud/skyux/commit/a245e9a71a7512ec9503a273c7dce1534c2dcdee)) +* **components/icon:** variant input is respected for SVG-based icons ([#2688](https://github.com/blackbaud/skyux/issues/2688)) ([d5681a6](https://github.com/blackbaud/skyux/commit/d5681a627f6da97c1fb1b2b314f3b4ce04dfd187)) + +## [10.41.5](https://github.com/blackbaud/skyux/compare/10.41.4...10.41.5) (2024-08-26) + + +### Bug Fixes + +* **components/lists:** selectable repeater items do not log a checkbox `label` deprecation warning ([#2641](https://github.com/blackbaud/skyux/issues/2641)) ([110bfdf](https://github.com/blackbaud/skyux/commit/110bfdfeaa2c79417b9970b7fbee2c821febe660)) + +## [10.41.4](https://github.com/blackbaud/skyux/compare/10.41.3...10.41.4) (2024-08-22) + + +### Bug Fixes + +* **code-examples:** update lookup code examples to all use the `searchAsync` event ([#2621](https://github.com/blackbaud/skyux/issues/2621)) ([041dc29](https://github.com/blackbaud/skyux/commit/041dc29197b625106a4874c72fb396fa2424c11d)) + + +### Deprecations + +* **components/lookup:** lookup component's `data` input has been deprecated and consumers should use the `searchAsync` event instead ([#2617](https://github.com/blackbaud/skyux/issues/2617)) ([e28887f](https://github.com/blackbaud/skyux/commit/e28887ffabf7831870501dc61749446e45be4e22)) + +## [10.41.3](https://github.com/blackbaud/skyux/compare/10.41.2...10.41.3) (2024-08-16) + + +### Bug Fixes + +* `sky-form-errors` is no longer created and destroyed when form errors are present ([#2596](https://github.com/blackbaud/skyux/issues/2596)) ([416f1ea](https://github.com/blackbaud/skyux/commit/416f1ea017d57627a07491b046440a7d065ca690)) +* **components/lookup:** autocomplete and lookup show wait in dropdown when using an async search when no dropdown was previously open ([#2610](https://github.com/blackbaud/skyux/issues/2610)) ([7250a20](https://github.com/blackbaud/skyux/commit/7250a207568ab2cf04b73f949ff84bdb4c57c906)) +* **components/phone-field:** phone numbers validate when selected country is changed programmatically ([#2593](https://github.com/blackbaud/skyux/issues/2593)) ([6da7bb8](https://github.com/blackbaud/skyux/commit/6da7bb8d05c0645f3cc6c65386d6428d3d85b9e8)) + +## [10.41.2](https://github.com/blackbaud/skyux/compare/10.41.1...10.41.2) (2024-08-08) + + +### Bug Fixes + +* **components/colorpicker:** match label text to easy mode label styling ([#2591](https://github.com/blackbaud/skyux/issues/2591)) ([6abe29c](https://github.com/blackbaud/skyux/commit/6abe29cbe423d7a276adfa69093725150b88ef36)) +* **components/forms:** fix field group `labelText` inputs to work with async pipe ([#2590](https://github.com/blackbaud/skyux/issues/2590)) ([43cd58b](https://github.com/blackbaud/skyux/commit/43cd58bd22231f139d6bb5c609b0f757fedb841a)) +* **components/pages:** use CSP_NONCE when creating style elements ([#2599](https://github.com/blackbaud/skyux/issues/2599)) ([cb8e620](https://github.com/blackbaud/skyux/commit/cb8e62082934f2fd352ac9709f15f4068637657b)) + +## [10.41.1](https://github.com/blackbaud/skyux/compare/10.41.0...10.41.1) (2024-08-02) + + +### Bug Fixes + +* **components/router:** fix `SkyHrefHarness` to find elements when `skyHref` is bound to a template variable ([#2580](https://github.com/blackbaud/skyux/issues/2580)) ([79ed2d0](https://github.com/blackbaud/skyux/commit/79ed2d0ed802a23a2f2089e1e3944240f39a16d9)) + +## [10.41.0](https://github.com/blackbaud/skyux/compare/10.40.0...10.41.0) (2024-07-31) + + +### Features + +* move help inline features out of developer preview for key info, box, and description list ([#2574](https://github.com/blackbaud/skyux/issues/2574)) ([1cd7b1e](https://github.com/blackbaud/skyux/commit/1cd7b1e2052f23a0a88b5bfdace41b36c852330a)) + + +### Bug Fixes + +* **components/layout:** fix key info display in page summary ([#2576](https://github.com/blackbaud/skyux/issues/2576)) ([944c329](https://github.com/blackbaud/skyux/commit/944c3299c608e616475c4b1c0e3bd4ce6694311c)) +* **components/modals:** `SkyModalTestingController.closeTopModal` passes `reason` and `result` to the modal instance ([#2565](https://github.com/blackbaud/skyux/issues/2565)) ([0337f78](https://github.com/blackbaud/skyux/commit/0337f7825431699609f668d3408d353b59ba6896)) + +## [10.40.0](https://github.com/blackbaud/skyux/compare/10.39.0...10.40.0) (2024-07-26) + + +### Features + +* **components/forms:** improve file attachments error messaging for incorrect file types ([#2553](https://github.com/blackbaud/skyux/issues/2553)) ([93404bf](https://github.com/blackbaud/skyux/commit/93404bf9a97eddcc9955139d374d7218d1f678ea)) + + +### Bug Fixes + +* **components/lists:** repeater focus styles show on focus-visible in modern theme ([#2554](https://github.com/blackbaud/skyux/issues/2554)) ([4c6e357](https://github.com/blackbaud/skyux/commit/4c6e357d3f9d8d426b7f08d93e6d982c59789f31)) + +## [10.39.0](https://github.com/blackbaud/skyux/compare/10.38.0...10.39.0) (2024-07-25) + + +### Features + +* **components/icon:** upgrade icons library to 7.5.0 ([#2549](https://github.com/blackbaud/skyux/issues/2549)) ([3c8b6b7](https://github.com/blackbaud/skyux/commit/3c8b6b7eacd69c95ed8e1d33c28ccdad6a82c266)) + + +### Bug Fixes + +* **components/help-inline:** set `aria-controls` on help inline button when widget element exists ([#2541](https://github.com/blackbaud/skyux/issues/2541)) ([55e7030](https://github.com/blackbaud/skyux/commit/55e7030cbd5dc7fd19e69917ab9ba7ef6f06de22)) +* **components/lists:** remove `::ng-deep` from sort styles ([#2538](https://github.com/blackbaud/skyux/issues/2538)) ([deab22a](https://github.com/blackbaud/skyux/commit/deab22a1352bdb10e813087c3e2129926acd4695)) +* **components/lookup:** lookup aria labels are now set appropriately when using the input box `labelText` input ([#2548](https://github.com/blackbaud/skyux/issues/2548)) ([73f9e68](https://github.com/blackbaud/skyux/commit/73f9e68d5a0804651df76349618c0c0b17e36bfb)) + +## [10.38.0](https://github.com/blackbaud/skyux/compare/10.37.4...10.38.0) (2024-07-23) + + +### Features + +* **components/core:** add scroll shadow directive ([#2537](https://github.com/blackbaud/skyux/issues/2537)) ([db223ad](https://github.com/blackbaud/skyux/commit/db223adfcb20767ffc48f9392c2a7722ce109c79)) + +## [10.37.4](https://github.com/blackbaud/skyux/compare/10.37.3...10.37.4) (2024-07-22) + + +### Bug Fixes + +* **components/indicators:** hide illustration while loading ([#2526](https://github.com/blackbaud/skyux/issues/2526)) ([bc029bd](https://github.com/blackbaud/skyux/commit/bc029bd098e98d62123295b5c82ab15fe780ff9d)) +* **components/tiles:** remove `::ng-deep` from tile styles ([#2527](https://github.com/blackbaud/skyux/issues/2527)) ([09092d7](https://github.com/blackbaud/skyux/commit/09092d7b8d7eae39950cbc3209ff8dae5727251e)) + +## [10.37.3](https://github.com/blackbaud/skyux/compare/10.37.2...10.37.3) (2024-07-17) + + +### Bug Fixes + +* **components/layout:** remove onpush change detection from description list term to dynamically detect inline help inputs from description list content ([#2515](https://github.com/blackbaud/skyux/issues/2515)) ([2a5a1d1](https://github.com/blackbaud/skyux/commit/2a5a1d153eb801fe196d85ea359bb6ba144f8a2d)) + +## [10.37.2](https://github.com/blackbaud/skyux/compare/10.37.1...10.37.2) (2024-07-17) + + +### Bug Fixes + +* **components/packages:** additional error handling for AG Grid schematic ([#2509](https://github.com/blackbaud/skyux/issues/2509)) ([27498a2](https://github.com/blackbaud/skyux/commit/27498a2ba227cf8ff98026c1f3990a0eb5f756ac)) + +## [10.37.1](https://github.com/blackbaud/skyux/compare/10.37.0...10.37.1) (2024-07-16) + + +### Bug Fixes + +* **components/help-inline:** add schematic for missing popovers peer which may be missing due to the package being added by schematic ([#2506](https://github.com/blackbaud/skyux/issues/2506)) ([6212fa7](https://github.com/blackbaud/skyux/commit/6212fa7af5b71e35f05526e5c0cc2145d02e2e4e)) +* **components/layout:** box headingHidden input shouldn't hide controls component ([#2505](https://github.com/blackbaud/skyux/issues/2505)) ([dbdeb0d](https://github.com/blackbaud/skyux/commit/dbdeb0d72d3e119074caf2cf22f8ae555a479009)) +* **components/lists:** reorderable repeater will not throw a warning when no repeater items exist ([#2492](https://github.com/blackbaud/skyux/issues/2492)) ([a01a108](https://github.com/blackbaud/skyux/commit/a01a108cc8cb95ff245dad6f9e472ba60ef1c350)) +* **sdk/testing:** `expectAsync` type includes async matchers from Jasmine ([#2503](https://github.com/blackbaud/skyux/issues/2503)) ([b34086f](https://github.com/blackbaud/skyux/commit/b34086f230df8b7e9b2eb9a46ec2181c8bb64b19)) + +## [10.37.0](https://github.com/blackbaud/skyux/compare/10.36.0...10.37.0) (2024-07-16) + + +### Features + +* **components/ag-grid:** add support for AG Grid 31.3.4 ([#2491](https://github.com/blackbaud/skyux/issues/2491)) ([9a720f2](https://github.com/blackbaud/skyux/commit/9a720f2e7306a59134ee5b0fdb59eea4db0a222b)) + +## [10.36.0](https://github.com/blackbaud/skyux/compare/10.35.1...10.36.0) (2024-07-15) + + +### Features + +* **components/icon:** use native fetch() for retrieving icon sprite ([#2478](https://github.com/blackbaud/skyux/issues/2478)) ([198d895](https://github.com/blackbaud/skyux/commit/198d89588ab7d758587aa12e3be1a9df634c7cbb)) +* **components/layout:** add heading and inline help inputs to box component ([#2439](https://github.com/blackbaud/skyux/issues/2439)) ([c50280b](https://github.com/blackbaud/skyux/commit/c50280b250ca49bcfe2635c4dbc1bfe3207e5704)) + + +### Bug Fixes + +* **components/packages:** switch to `@ag-grid-devtools/cli` for AG Grid codemods ([#2483](https://github.com/blackbaud/skyux/issues/2483)) ([2fbfbef](https://github.com/blackbaud/skyux/commit/2fbfbefc15bf9f68ddefd2dbc8b9c5eebd9d2aca)) + +## [10.35.1](https://github.com/blackbaud/skyux/compare/10.35.0...10.35.1) (2024-07-12) + + +### Bug Fixes + +* **components/icon:** provide HTTP with interceptors ([#2474](https://github.com/blackbaud/skyux/issues/2474)) ([8607208](https://github.com/blackbaud/skyux/commit/86072086e4133750817b9862ba9868e2690b33ea)) + +## [10.35.0](https://github.com/blackbaud/skyux/compare/10.34.0...10.35.0) (2024-07-11) + + +### Features + +* **components/ag-grid:** add support for AG Grid 31.3.2 ([#2450](https://github.com/blackbaud/skyux/issues/2450)) ([cea996d](https://github.com/blackbaud/skyux/commit/cea996db6216e223c9998916e50ba7a4b78aa08f)) +* **components/datetime:** add support for additional date formats to `SkyDatePipe` ([#2447](https://github.com/blackbaud/skyux/issues/2447)) ([bbfb330](https://github.com/blackbaud/skyux/commit/bbfb330a8bed125063098ffde6c99c0f1a3deea6)) + + +### Bug Fixes + +* **code-examples:** satisfy ESLint rules for core, forms, indicators, and inline form code examples ([#2432](https://github.com/blackbaud/skyux/issues/2432)) ([6bd1182](https://github.com/blackbaud/skyux/commit/6bd118283fbccbab4c67764d061a51da2b0c80cd)) +* **code-examples:** satisfy ESLint rules for layout, lists, lookup, modals, pages, and others ([#2435](https://github.com/blackbaud/skyux/issues/2435)) ([73a4763](https://github.com/blackbaud/skyux/commit/73a47639b6349818b42de5c8d4dcbbf402c13884)) +* **components/datetime:** ignore extraneous properties when setting calculator value ([#2459](https://github.com/blackbaud/skyux/issues/2459)) ([4d2d70a](https://github.com/blackbaud/skyux/commit/4d2d70a378d69af7b30370ed44641b036703ce47)) +* **components/phone-field:** required phone field controls are not "touched" on initialization ([#2443](https://github.com/blackbaud/skyux/issues/2443)) ([84e1382](https://github.com/blackbaud/skyux/commit/84e1382a2233b381c8c8c859b38bd8f743ef672f)) +* **components/tiles:** do not expand/collapse content when help inline clicked ([#2468](https://github.com/blackbaud/skyux/issues/2468)) ([8a173ce](https://github.com/blackbaud/skyux/commit/8a173ce0fbe5660806f17ede06323f590f944c1c)) + + +### Reverts + +* **components/help-inline:** stop propagation of click events ([#2458](https://github.com/blackbaud/skyux/issues/2458)) ([e08d883](https://github.com/blackbaud/skyux/commit/e08d88366c391455df53922ddc1719a6c66d0021)) + +## [10.34.0](https://github.com/blackbaud/skyux/compare/10.33.0...10.34.0) (2024-07-08) + + +### Features + +* **components/help-inline:** update documentation and deprecate `indicators/help-inline` ([#2428](https://github.com/blackbaud/skyux/issues/2428)) ([8927671](https://github.com/blackbaud/skyux/commit/89276711d0b4a19a54e9cd1f00e72df3c0ee1596)) +* **components/icon:** add internal support for SVG-based icons ([#2433](https://github.com/blackbaud/skyux/issues/2433)) ([3d61fe0](https://github.com/blackbaud/skyux/commit/3d61fe0c635209111a8a7b2de09492f48475d82f)) + + +### Bug Fixes + +* **code-examples:** satisfy ESLint rules for action button and AG Grid code examples ([#2423](https://github.com/blackbaud/skyux/issues/2423)) ([26c36f5](https://github.com/blackbaud/skyux/commit/26c36f5d745315fcd79c8a1392455fdf5735e1a4)) +* **code-examples:** satisfy ESLint rules for colorpicker code examples ([#2424](https://github.com/blackbaud/skyux/issues/2424)) ([7d0d79b](https://github.com/blackbaud/skyux/commit/7d0d79b3ed0cc3b333f24672b7e2cd2a816f3a24)) +* **code-examples:** satisfy ESLint rules for datetime code examples ([#2427](https://github.com/blackbaud/skyux/issues/2427)) ([e222a73](https://github.com/blackbaud/skyux/commit/e222a736564509baee593daa877d80bd42d8f966)) +* **components/forms:** file attachment components report file size errors with appropriate orders of magnitude ([#2437](https://github.com/blackbaud/skyux/issues/2437)) ([8768cf9](https://github.com/blackbaud/skyux/commit/8768cf9ecbadaa7845d9093f3c2ed1516487095a)) +* **components/forms:** improve default value handling for heading styles on form group components ([#2420](https://github.com/blackbaud/skyux/issues/2420)) ([ffa7946](https://github.com/blackbaud/skyux/commit/ffa79466cfa356db4495ec4b5116d9668b0cb54f)) + ## [10.33.0](https://github.com/blackbaud/skyux/compare/10.32.0...10.33.0) (2024-07-01) diff --git a/apps/code-examples/.eslintrc.json b/apps/code-examples/.eslintrc.json index 73af0ebb9d..bb83826810 100644 --- a/apps/code-examples/.eslintrc.json +++ b/apps/code-examples/.eslintrc.json @@ -31,7 +31,14 @@ }, { "files": ["./src/app/code-examples/**/*.ts"], + "extends": ["../../libs/sdk/eslint-config/recommended"], + "parserOptions": { + "project": ["apps/code-examples/tsconfig.editor.json"], + "tsconfigRootDir": "." + }, "rules": { + "no-alert": "warn", + "no-console": "warn", "no-restricted-imports": [ "error", { diff --git a/apps/code-examples/src/app/app-routing.module.ts b/apps/code-examples/src/app/app-routing.module.ts index f494d93d16..5cd3cc5af3 100644 --- a/apps/code-examples/src/app/app-routing.module.ts +++ b/apps/code-examples/src/app/app-routing.module.ts @@ -57,6 +57,18 @@ const routes: Routes = [ loadChildren: () => import('./features/forms.module').then((m) => m.FormsModule), }, + { + path: 'help-inline', + loadChildren: () => + import('./features/help-inline.module').then( + (m) => m.HelpInlineFeatureModule, + ), + }, + { + path: 'icon', + loadChildren: () => + import('./features/icon.module').then((m) => m.IconFeatureModule), + }, { path: 'indicators', loadChildren: () => diff --git a/apps/code-examples/src/app/app.component.ts b/apps/code-examples/src/app/app.component.ts index 5103ddc744..767af5402a 100644 --- a/apps/code-examples/src/app/app.component.ts +++ b/apps/code-examples/src/app/app.component.ts @@ -29,7 +29,7 @@ export class AppComponent { }); const themeSettings = new SkyThemeSettings( - SkyTheme.presets['modern'], + SkyTheme.presets.modern, SkyThemeMode.presets.light, ); diff --git a/apps/code-examples/src/app/app.module.ts b/apps/code-examples/src/app/app.module.ts index 56715ac932..00210979ee 100644 --- a/apps/code-examples/src/app/app.module.ts +++ b/apps/code-examples/src/app/app.module.ts @@ -8,7 +8,7 @@ import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], - imports: [BrowserAnimationsModule, BrowserModule, AppRoutingModule], + imports: [AppRoutingModule, BrowserAnimationsModule, BrowserModule], providers: [SkyThemeService], bootstrap: [AppComponent], }) diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/context-menu.component.ts index 346476d03d..2539b1f86a 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -12,15 +14,15 @@ import { ICellRendererParams } from 'ag-grid-community'; imports: [SkyDropdownModule], }) export class ContextMenuComponent implements ICellRendererAngularComp { - public contextMenuAriaLabel = ''; - public deleteAriaLabel = ''; - public markInactiveAriaLabel = ''; - public moreInfoAriaLabel = ''; + protected contextMenuAriaLabel = ''; + protected deleteAriaLabel = ''; + protected markInactiveAriaLabel = ''; + protected moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; @@ -32,6 +34,6 @@ export class ContextMenuComponent implements ICellRendererAngularComp { } protected actionClicked(action: string): void { - alert(`${action} clicked for ${this.#name}`); + console.error(`${action} clicked for ${this.#name}`); } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data.ts index 69167d2b8c..c19a452159 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/data.ts @@ -95,7 +95,7 @@ export interface AgGridDemoRow { jobTitle?: AutocompleteOption; } -export const AG_GRID_DEMO_DATA = [ +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ { selected: true, name: 'Billy Bob', diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/demo.component.ts index ec5cac36b3..fcc5116316 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/demo.component.ts @@ -85,7 +85,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -127,7 +128,9 @@ export class DemoComponent { constructor() { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, }; this.gridOptions = this.#agGridSvc.getEditableGridOptions({ @@ -165,7 +168,7 @@ export class DemoComponent { if (result.reason === 'cancel' || result.reason === 'close') { alert('Edits canceled!'); } else { - this.gridData = result.data; + this.gridData = result.data as AgGridDemoRow[]; if (this.#gridApi) { this.#gridApi.refreshCells(); @@ -192,11 +195,13 @@ export class DemoComponent { } } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; - + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/edit-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/edit-modal.component.ts index 30c5ebe6c3..159e729388 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/edit-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/basic/edit-modal.component.ts @@ -96,7 +96,7 @@ export class EditModalComponent { type: SkyCellType.Date, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { return { skyComponentProperties: { minDate: params.data.startDate } }; }, @@ -107,7 +107,7 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { return { skyComponentProperties: { @@ -132,12 +132,9 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { - const selectedDepartment: string = - params.data && - params.data.department && - params.data.department.name; + const selectedDepartment = params.data?.department?.name; const editParams: { skyComponentProperties: SkyAgGridAutocompleteProperties; @@ -178,7 +175,9 @@ export class EditModalComponent { this.gridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, }; this.gridOptions = this.#agGridSvc.getEditableGridOptions({ @@ -196,15 +195,15 @@ export class EditModalComponent { #departmentSelectionChange( change: SkyAutocompleteSelectionChange, - node: IRowNode, + node: IRowNode, ): void { - if (change.selectedItem && change.selectedItem !== node.data.department) { + if (change.selectedItem && change.selectedItem !== node.data?.department) { this.#clearJobTitle(node); } } - #clearJobTitle(node: IRowNode | null): void { - if (node) { + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { node.data.jobTitle = undefined; this.#changeDetectorRef.markForCheck(); diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/demo.component.ts index 8663a5b340..adeeeaf44e 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/demo.component.ts @@ -15,7 +15,7 @@ import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; import { Subject, takeUntil } from 'rxjs'; -import { AG_GRID_DEMO_DATA } from './data'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; import { EditModalContext } from './edit-modal-context'; import { EditModalComponent } from './edit-modal.component'; import { FilterModalComponent } from './filter-modal.component'; @@ -130,7 +130,7 @@ export class DemoComponent implements OnInit, OnDestroy { if (result.reason === 'cancel' || result.reason === 'close') { alert('Edits canceled!'); } else { - this.items = result.data; + this.items = result.data as AgGridDemoRow[]; alert('Saving data!'); } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts index 6a6e427253..ecb1ec7b1b 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts @@ -71,7 +71,7 @@ export class EditModalComponent { type: SkyCellType.Date, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { return { skyComponentProperties: { @@ -86,7 +86,7 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { return { skyComponentProperties: { @@ -109,12 +109,9 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { - const selectedDepartment: string = - params.data && - params.data.department && - params.data.department.name; + const selectedDepartment = params.data?.department?.name; const editParams: { skyComponentProperties: SkyAgGridAutocompleteProperties; @@ -155,7 +152,9 @@ export class EditModalComponent { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, }; this.gridOptions = this.#agGridService.getEditableGridOptions({ @@ -173,15 +172,15 @@ export class EditModalComponent { #departmentSelectionChange( change: SkyAutocompleteSelectionChange, - node: IRowNode, + node: IRowNode, ): void { - if (change.selectedItem && change.selectedItem !== node.data.department) { + if (change.selectedItem && change.selectedItem !== node.data?.department) { this.#clearJobTitle(node); } } - #clearJobTitle(node: IRowNode | null): void { - if (node) { + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { node.data.jobTitle = undefined; if (this.#gridApi) { diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts index d6e436fc20..ff896f8d7c 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts @@ -13,6 +13,8 @@ import { import { SkyCheckboxModule } from '@skyux/forms'; import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { Filters } from './filters'; + @Component({ standalone: true, selector: 'app-filter-modal', @@ -30,10 +32,10 @@ export class FilterModalComponent { constructor() { if (this.#context.filterData?.filters) { - const filters = this.#context.filterData.filters; + const filters = this.#context.filterData.filters as Filters; - this.jobTitle = filters.jobTitle || 'any'; - this.hideSales = filters.hideSales || false; + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; } this.#changeDetector.markForCheck(); @@ -46,7 +48,7 @@ export class FilterModalComponent { result.filters = { jobTitle: this.jobTitle, hideSales: this.hideSales, - }; + } satisfies Filters; this.#changeDetector.markForCheck(); this.#instance.save(result); diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filters.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts index 8f67dc8840..68fe4a5b01 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts @@ -28,6 +28,7 @@ import { Subject, of, takeUntil } from 'rxjs'; import { ContextMenuComponent } from './context-menu.component'; import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; @Component({ standalone: true, @@ -85,7 +86,8 @@ export class ViewGridComponent implements OnInit, OnDestroy { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -238,16 +240,21 @@ export class ViewGridComponent implements OnInit, OnDestroy { this.#changeDetectorRef.markForCheck(); } - protected onRowSelected(rowSelectedEvent: RowSelectedEvent): void { - if (!rowSelectedEvent.data.selected) { + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { this.#updateData(); } } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } @@ -256,11 +263,11 @@ export class ViewGridComponent implements OnInit, OnDestroy { const filterData = this.#dataState.filterData; if (filterData?.filters) { - const filters = filterData.filters; + const filters = filterData.filters as Filters; filteredItems = items.filter((item) => { return ( - ((filters.hideSales && item.department.name !== 'Sales') || + (!!(filters.hideSales && item.department.name !== 'Sales') || !filters.hideSales) && ((filters.jobTitle !== 'any' && item.jobTitle?.name === filters.jobTitle) || @@ -286,7 +293,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { property === 'name' ) { const propertyText = item[property]?.toLowerCase(); - if (propertyText.indexOf(searchText) > -1) { + if (propertyText.includes(searchText)) { return true; } } @@ -301,7 +308,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { #setInitialColumnOrder(): void { const viewState = this.#dataState.getViewStateById(this.#viewId); - const visibleColumns = viewState?.displayedColumnIds || []; + const visibleColumns = viewState?.displayedColumnIds ?? []; this.#columnDefs.sort((col1, col2) => { const col1Index = visibleColumns.findIndex( diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data.ts index 69167d2b8c..c19a452159 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data.ts @@ -95,7 +95,7 @@ export interface AgGridDemoRow { jobTitle?: AutocompleteOption; } -export const AG_GRID_DEMO_DATA = [ +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ { selected: true, name: 'Billy Bob', diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/demo.component.ts index 8df9c2d6a3..56bfe1ad47 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/demo.component.ts @@ -99,7 +99,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), headerComponentParams: { inlineHelpComponent: InlineHelpComponent, }, @@ -155,7 +156,9 @@ export class DemoComponent { constructor() { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, }; this.gridOptions = this.#agGridSvc.getEditableGridOptions({ @@ -192,7 +195,7 @@ export class DemoComponent { if (result.reason === 'cancel' || result.reason === 'close') { alert('Edits canceled!'); } else { - this.gridData = result.data; + this.gridData = result.data as AgGridDemoRow[]; this.#gridApi?.refreshCells(); alert('Saving data!'); @@ -215,10 +218,13 @@ export class DemoComponent { } } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts index 04ea2ec9b6..09565ddc57 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts @@ -95,7 +95,7 @@ export class EditModalComponent { type: SkyCellType.Date, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { return { skyComponentProperties: { minDate: params.data.startDate } }; }, @@ -106,7 +106,7 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { return { skyComponentProperties: { @@ -129,12 +129,9 @@ export class EditModalComponent { type: SkyCellType.Autocomplete, editable: true, cellEditorParams: ( - params: ICellEditorParams, + params: ICellEditorParams, ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { - const selectedDepartment: string = - params.data && - params.data.department && - params.data.department.name; + const selectedDepartment = params.data?.department?.name; const editParams: { skyComponentProperties: SkyAgGridAutocompleteProperties; } = { skyComponentProperties: { data: [] } }; @@ -174,7 +171,9 @@ export class EditModalComponent { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, }; this.gridOptions = this.#agGridSvc.getEditableGridOptions({ @@ -192,15 +191,15 @@ export class EditModalComponent { #departmentSelectionChange( change: SkyAutocompleteSelectionChange, - node: IRowNode, + node: IRowNode, ): void { - if (change.selectedItem && change.selectedItem !== node.data.department) { + if (change.selectedItem && change.selectedItem !== node.data?.department) { this.#clearJobTitle(node); } } - #clearJobTitle(node: IRowNode | null): void { - if (node) { + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { node.data.jobTitle = undefined; this.#changeDetectorRef.markForCheck(); diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/demo.component.ts index 3a3dac0c82..ee0a910475 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic-multiselect/demo.component.ts @@ -62,7 +62,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -83,7 +84,9 @@ export class DemoComponent { constructor() { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, rowSelection: 'multiple', }; @@ -97,10 +100,13 @@ export class DemoComponent { this.#gridApi.sizeColumnsToFit(); } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/demo.component.ts index f34586edbe..dc35e1cf5f 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/basic/demo.component.ts @@ -12,7 +12,7 @@ import { } from 'ag-grid-community'; import { ContextMenuComponent } from './context-menu.component'; -import { AG_GRID_DEMO_DATA } from './data'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; @Component({ standalone: true, @@ -53,7 +53,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -74,7 +75,9 @@ export class DemoComponent { constructor() { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, rowSelection: 'single', }; @@ -88,10 +91,13 @@ export class DemoComponent { this.#gridApi.sizeColumnsToFit(); } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/demo.component.ts index 6f71036596..5d26068f41 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/demo.component.ts @@ -17,6 +17,7 @@ import { Subject, takeUntil } from 'rxjs'; import { AG_GRID_DEMO_DATA } from './data'; import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; import { ViewGridComponent } from './view-grid.component'; @Component({ @@ -54,7 +55,7 @@ export class DemoComponent implements OnInit, OnDestroy { filters: { hideSales: false, jobTitle: 'any', - }, + } satisfies Filters, }, views: [ { diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts index b6663f960a..28109d2ee9 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts @@ -13,6 +13,8 @@ import { import { SkyCheckboxModule } from '@skyux/forms'; import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { Filters } from './filters'; + @Component({ standalone: true, selector: 'app-filter-modal', @@ -30,10 +32,10 @@ export class FilterModalComponent { constructor() { if (this.#context.filterData && this.#context.filterData.filters) { - const filters = this.#context.filterData.filters; + const filters = this.#context.filterData.filters as Filters; - this.jobTitle = filters.jobTitle || 'any'; - this.hideSales = filters.hideSales || false; + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; } this.#changeDetectorRef.markForCheck(); @@ -46,7 +48,7 @@ export class FilterModalComponent { result.filters = { jobTitle: this.jobTitle, hideSales: this.hideSales, - }; + } satisfies Filters; this.#changeDetectorRef.markForCheck(); this.#instance.save(result); diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filters.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts index a873e21bbc..e6525d4928 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts @@ -31,6 +31,7 @@ import { Subject, of, takeUntil } from 'rxjs'; import { ContextMenuComponent } from './context-menu.component'; import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; @Component({ standalone: true, @@ -85,7 +86,8 @@ export class ViewGridComponent implements OnInit, OnDestroy { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -210,8 +212,10 @@ export class ViewGridComponent implements OnInit, OnDestroy { this.#updateDisplayedItems(); } - protected onRowSelected(rowSelectedEvent: RowSelectedEvent): void { - if (!rowSelectedEvent.data.selected) { + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { this.#updateDisplayedItems(); } } @@ -230,7 +234,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { property === 'name' ) { const propertyText = item[property].toLowerCase(); - if (propertyText.indexOf(searchText) > -1) { + if (propertyText.includes(searchText)) { return true; } } @@ -247,12 +251,12 @@ export class ViewGridComponent implements OnInit, OnDestroy { let filteredItems = items; const filterData = this.#dataState && this.#dataState.filterData; - if (filterData && filterData.filters) { - const filters = filterData.filters; + if (filterData?.filters) { + const filters = filterData.filters as Filters; filteredItems = items.filter((item) => { return ( - ((filters.hideSales && item.department.name !== 'Sales') || + (!!(filters.hideSales && item.department.name !== 'Sales') || !filters.hideSales) && ((filters.jobTitle !== 'any' && item.jobTitle?.name === filters.jobTitle) || @@ -265,10 +269,13 @@ export class ViewGridComponent implements OnInit, OnDestroy { return filteredItems; } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/demo.component.ts index cdd5ae997d..fa35ea2ccb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/demo.component.ts @@ -16,6 +16,7 @@ import { Subject, takeUntil } from 'rxjs'; import { AG_GRID_DEMO_DATA } from './data'; import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; import { ViewGridComponent } from './view-grid.component'; @Component({ @@ -55,7 +56,7 @@ export class DemoComponent implements OnInit, OnDestroy { filters: { hideSales: false, jobTitle: 'any', - }, + } satisfies Filters, }, views: [ { diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filter-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filter-modal.component.ts index b6663f960a..28109d2ee9 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filter-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filter-modal.component.ts @@ -13,6 +13,8 @@ import { import { SkyCheckboxModule } from '@skyux/forms'; import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { Filters } from './filters'; + @Component({ standalone: true, selector: 'app-filter-modal', @@ -30,10 +32,10 @@ export class FilterModalComponent { constructor() { if (this.#context.filterData && this.#context.filterData.filters) { - const filters = this.#context.filterData.filters; + const filters = this.#context.filterData.filters as Filters; - this.jobTitle = filters.jobTitle || 'any'; - this.hideSales = filters.hideSales || false; + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; } this.#changeDetectorRef.markForCheck(); @@ -46,7 +48,7 @@ export class FilterModalComponent { result.filters = { jobTitle: this.jobTitle, hideSales: this.hideSales, - }; + } satisfies Filters; this.#changeDetectorRef.markForCheck(); this.#instance.save(result); diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filters.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/view-grid.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/view-grid.component.ts index 42d5a82e2b..e7d272c08e 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/view-grid.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/view-grid.component.ts @@ -30,6 +30,7 @@ import { Subject, takeUntil } from 'rxjs'; import { ContextMenuComponent } from './context-menu.component'; import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; @Component({ standalone: true, @@ -76,7 +77,8 @@ export class ViewGridComponent implements OnInit, OnDestroy { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -193,8 +195,10 @@ export class ViewGridComponent implements OnInit, OnDestroy { this.#updateDisplayedItems(); } - protected onRowSelected(rowSelectedEvent: RowSelectedEvent): void { - if (!rowSelectedEvent.data.selected) { + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { this.#updateDisplayedItems(); } } @@ -204,7 +208,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { const searchText = this.#dataState && this.#dataState.searchText; if (searchText) { - searchedItems = items.filter(function (item: AgGridDemoRow) { + searchedItems = items.filter((item: AgGridDemoRow) => { let property: keyof typeof item; for (property in item) { @@ -214,7 +218,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { ) { const propertyText = item[property].toLowerCase(); - if (propertyText.indexOf(searchText) > -1) { + if (propertyText.includes(searchText)) { return true; } } @@ -231,11 +235,12 @@ export class ViewGridComponent implements OnInit, OnDestroy { let filteredItems = items; const filterData = this.#dataState && this.#dataState.filterData; - if (filterData && filterData.filters) { - const filters = filterData.filters; + if (filterData?.filters) { + const filters = filterData.filters as Filters; + filteredItems = items.filter((item: AgGridDemoRow) => { return ( - ((filters.hideSales && item.department.name !== 'Sales') || + (!!(filters.hideSales && item.department.name !== 'Sales') || !filters.hideSales) && ((filters.jobTitle !== 'any' && item.jobTitle?.name === filters.jobTitle) || @@ -248,10 +253,13 @@ export class ViewGridComponent implements OnInit, OnDestroy { return filteredItems; } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/demo.component.ts index b50790f82b..4e2b620aa7 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/demo.component.ts @@ -81,7 +81,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), headerComponentParams: { inlineHelpComponent: InlineHelpComponent, }, @@ -146,10 +147,13 @@ export class DemoComponent { } } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/demo.component.ts index 7c6e5993ae..fe7e40d11c 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/paging/demo.component.ts @@ -23,7 +23,7 @@ import { Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { ContextMenuComponent } from './context-menu.component'; -import { AG_GRID_DEMO_DATA } from './data'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; @Component({ standalone: true, @@ -65,7 +65,8 @@ export class DemoComponent implements OnInit, OnDestroy { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -93,7 +94,9 @@ export class DemoComponent implements OnInit, OnDestroy { constructor() { const gridOptions: GridOptions = { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, rowSelection: 'single', pagination: true, suppressPaginationPanel: true, @@ -108,7 +111,7 @@ export class DemoComponent implements OnInit, OnDestroy { public ngOnInit(): void { this.#subscriptions.add( this.#activatedRoute.queryParamMap - .pipe(map((params) => params.get('page') || '1')) + .pipe(map((params) => params.get('page') ?? '1')) .subscribe((page) => { this.currentPage = Number(page); this.#gridApi?.paginationGoToPage(this.currentPage - 1); @@ -142,20 +145,21 @@ export class DemoComponent implements OnInit, OnDestroy { this.#gridApi.paginationGoToPage(this.currentPage - 1); } - protected onPageChange(page: number): void { - this.#router - .navigate(['.'], { - relativeTo: this.#activatedRoute, - queryParams: { page: page.toString(10) }, - queryParamsHandling: 'merge', - }) - .then(); + protected async onPageChange(page: number): Promise { + await this.#router.navigate(['.'], { + relativeTo: this.#activatedRoute, + queryParams: { page: page.toString(10) }, + queryParamsHandling: 'merge', + }); } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/context-menu.component.ts index 346476d03d..acfc353ddb 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { AgGridDemoRow } from './data'; + @Component({ standalone: true, selector: 'app-context-menu', @@ -17,10 +19,10 @@ export class ContextMenuComponent implements ICellRendererAngularComp { public markInactiveAriaLabel = ''; public moreInfoAriaLabel = ''; - #name = ''; + #name: string | undefined; - public agInit(params: ICellRendererParams): void { - this.#name = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; this.contextMenuAriaLabel = `Context menu for ${this.#name}`; this.deleteAriaLabel = `Delete ${this.#name}`; this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/demo.component.ts index 34eead39e8..2e853df75b 100644 --- a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/top-scroll/demo.component.ts @@ -66,7 +66,8 @@ export class DemoComponent { field: 'endDate', headerName: 'End date', type: SkyCellType.Date, - valueFormatter: this.#endDateFormatter, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), }, { field: 'department', @@ -88,7 +89,9 @@ export class DemoComponent { this.gridOptions = this.#agGridSvc.getGridOptions({ gridOptions: { columnDefs: this.#columnDefs, - onGridReady: (gridReadyEvent): void => this.onGridReady(gridReadyEvent), + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, context: { enableTopScroll: true, }, @@ -115,10 +118,13 @@ export class DemoComponent { } } - #endDateFormatter(params: ValueFormatterParams): string { - const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + #endDateFormatter(params: ValueFormatterParams): string { return params.value - ? params.value.toLocaleDateString('en-us', dateConfig) + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) : 'N/A'; } } diff --git a/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/basic/demo.component.ts index 1847d4b646..803ba9ab7e 100644 --- a/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/basic/demo.component.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, inject } from '@angular/core'; import { - AbstractControl, FormBuilder, FormControl, FormGroup, @@ -10,6 +9,14 @@ import { } from '@angular/forms'; import { SkyColorpickerModule, SkyColorpickerOutput } from '@skyux/colorpicker'; +interface DemoForm { + favoriteColor: FormControl; +} + +function isColorpickerOutput(value: unknown): value is SkyColorpickerOutput { + return !!(value && typeof value === 'object' && 'rgba' in value); +} + @Component({ standalone: true, selector: 'app-demo', @@ -17,8 +24,8 @@ import { SkyColorpickerModule, SkyColorpickerOutput } from '@skyux/colorpicker'; imports: [CommonModule, ReactiveFormsModule, SkyColorpickerModule], }) export class DemoComponent { - protected formGroup: FormGroup; - protected favoriteColor: FormControl; + protected favoriteColor: FormControl; + protected formGroup: FormGroup; protected swatches: string[] = [ '#BD4040', @@ -30,17 +37,19 @@ export class DemoComponent { ]; constructor() { - this.favoriteColor = new FormControl('#f00', [ - (control: AbstractControl): ValidationErrors | null => { - if (control.value?.rgba?.alpha < 0.8) { - return { opaque: true }; - } - - return null; - }, - ]); + this.favoriteColor = new FormControl('#f00', { + nonNullable: true, + validators: [ + (control): ValidationErrors | null => { + return isColorpickerOutput(control.value) && + control.value.rgba.alpha < 0.8 + ? { opaque: true } + : null; + }, + ], + }); - this.formGroup = inject(FormBuilder).group({ + this.formGroup = inject(FormBuilder).group({ favoriteColor: this.favoriteColor, }); } @@ -50,8 +59,11 @@ export class DemoComponent { } protected submit(): void { - const controlValue = this.formGroup.get('favoriteColor')?.value; - const favoriteColor: string = controlValue.hex || controlValue; + const controlValue = this.favoriteColor.value; + const favoriteColor = isColorpickerOutput(controlValue) + ? controlValue.hex + : controlValue; + alert('Your favorite color is: \n' + favoriteColor); } } diff --git a/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/programmatic/demo.component.ts b/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/programmatic/demo.component.ts index 15d9eb55d8..bb3b3da4be 100644 --- a/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/programmatic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/colorpicker/colorpicker/programmatic/demo.component.ts @@ -1,6 +1,5 @@ import { Component, inject } from '@angular/core'; import { - AbstractControl, FormBuilder, FormControl, FormGroup, @@ -12,10 +11,19 @@ import { SkyColorpickerMessage, SkyColorpickerMessageType, SkyColorpickerModule, + SkyColorpickerOutput, } from '@skyux/colorpicker'; import { Subject } from 'rxjs'; +interface DemoForm { + favoriteColor: FormControl; +} + +function isColorpickerOutput(value: unknown): value is SkyColorpickerOutput { + return !!(value && typeof value === 'object' && 'rgba' in value); +} + @Component({ standalone: true, selector: 'app-demo', @@ -24,22 +32,24 @@ import { Subject } from 'rxjs'; }) export class DemoComponent { protected colorpickerController = new Subject(); - protected favoriteColor: FormControl; - protected formGroup: FormGroup; + protected favoriteColor: FormControl; + protected formGroup: FormGroup; protected showResetButton = false; constructor() { - this.favoriteColor = new FormControl('#f00', [ - (control: AbstractControl): ValidationErrors | null => { - if (control.value?.rgba?.alpha < 0.8) { - return { opaque: true }; - } - - return null; - }, - ]); + this.favoriteColor = new FormControl('#f00', { + nonNullable: true, + validators: [ + (control): ValidationErrors | null => { + return isColorpickerOutput(control.value) && + control.value.rgba.alpha < 0.8 + ? { opaque: true } + : null; + }, + ], + }); - this.formGroup = inject(FormBuilder).group({ + this.formGroup = inject(FormBuilder).group({ favoriteColor: this.favoriteColor, }); } @@ -57,7 +67,6 @@ export class DemoComponent { } #sendMessage(type: SkyColorpickerMessageType): void { - const message: SkyColorpickerMessage = { type }; - this.colorpickerController.next(message); + this.colorpickerController.next({ type }); } } diff --git a/apps/code-examples/src/app/code-examples/core/numeric/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/core/numeric/basic/demo.component.spec.ts index 9e4a436f0e..9d17384369 100644 --- a/apps/code-examples/src/app/code-examples/core/numeric/basic/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/core/numeric/basic/demo.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { SkyNumericOptions } from '@skyux/core'; import { DemoComponent } from './demo.component'; @@ -25,11 +24,22 @@ describe('Basic numeric options', () => { } fixture.detectChanges(); - await fixture.whenStable().then(); + await fixture.whenStable(); return { fixture }; } + function getTextContent( + fixture: ComponentFixture, + selector: string, + ): string { + const el = ( + fixture.nativeElement as HTMLElement + ).querySelector(selector); + + return el?.textContent?.trim() ?? ''; + } + beforeEach(() => { TestBed.configureTestingModule({ imports: [DemoComponent], @@ -41,11 +51,7 @@ describe('Basic numeric options', () => { fixture.detectChanges(); - expect( - fixture.debugElement - .query(By.css('.default-value')) - .nativeElement.innerText.trim(), - ).toBe('123.5K'); + expect(getTextContent(fixture, '.default-value')).toEqual('123.5K'); }); it('should show the expected number in a specified format', async () => { @@ -54,10 +60,6 @@ describe('Basic numeric options', () => { config: { truncate: false }, }); - expect( - fixture.debugElement - .query(By.css('.configured-value')) - .nativeElement.innerText.trim(), - ).toBe('5,000,000'); + expect(getTextContent(fixture, '.configured-value')).toEqual('5,000,000'); }); }); diff --git a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/demo.component.ts index 95b58cc720..1bcafcc0ff 100644 --- a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/demo.component.ts @@ -13,6 +13,7 @@ import { import { DATA_MANAGER_DEMO_DATA, DataManagerDemoRow } from './data'; import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; import { ViewGridComponent } from './view-grid.component'; import { ViewRepeaterComponent } from './view-repeater.component'; @@ -54,7 +55,7 @@ export class DemoComponent implements OnInit { filtersApplied: true, filters: { hideOrange: true, - }, + } satisfies Filters, }, views: [ { diff --git a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filter-modal.component.ts b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filter-modal.component.ts index 1ba14df3f9..2a28d09ffe 100644 --- a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filter-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filter-modal.component.ts @@ -7,6 +7,8 @@ import { import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { Filters } from './filters'; + @Component({ standalone: true, selector: 'app-filter-modal', @@ -28,9 +30,10 @@ export class FilterModalComponent implements OnInit { public ngOnInit(): void { if (this.#context.filterData?.filters) { - const filters = this.#context.filterData.filters; - this.fruitType = filters.type || 'any'; - this.hideOrange = filters.hideOrange || false; + const filters = this.#context.filterData.filters as Filters; + + this.fruitType = filters.type ?? 'any'; + this.hideOrange = filters.hideOrange ?? false; } } @@ -41,7 +44,7 @@ export class FilterModalComponent implements OnInit { result.filters = { type: this.fruitType, hideOrange: this.hideOrange, - }; + } satisfies Filters; this.#instance.save(result); } diff --git a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filters.ts b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filters.ts new file mode 100644 index 0000000000..a4e9ab9c2d --- /dev/null +++ b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + hideOrange?: boolean; + type?: string; +} diff --git a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-grid.component.ts b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-grid.component.ts index baa733d8a3..7b0982696b 100644 --- a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-grid.component.ts +++ b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-grid.component.ts @@ -27,6 +27,7 @@ import { Subject, of } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { DataManagerDemoRow } from './data'; +import { Filters } from './filters'; @Component({ standalone: true, @@ -150,8 +151,10 @@ export class ViewGridComponent implements OnInit, OnDestroy { this.#ngUnsubscribe.complete(); } - protected onRowSelected(rowSelectedEvent: RowSelectedEvent): void { - if (!rowSelectedEvent.data.selected) { + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { this.#updateData(); } } @@ -161,12 +164,12 @@ export class ViewGridComponent implements OnInit, OnDestroy { const filterData = this.#dataState && this.#dataState.filterData; - if (filterData && filterData.filters) { - const filters = filterData.filters; + if (filterData?.filters) { + const filters = filterData.filters as Filters; filteredItems = items.filter((item) => { return ( - ((filters.hideOrange && item.color !== 'orange') || + ((filters.hideOrange && item.color !== 'orange') ?? !filters.hideOrange) && ((filters.type !== 'any' && item.type === filters.type) || !filters.type || @@ -189,7 +192,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { const searchText = this.#dataState && this.#dataState.searchText; if (searchText) { - searchedItems = items.filter(function (item: DataManagerDemoRow) { + searchedItems = items.filter((item: DataManagerDemoRow) => { let property: keyof typeof item; for (property in item) { @@ -198,7 +201,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { (property === 'name' || property === 'description') ) { const propertyText = item[property].toLowerCase(); - if (propertyText.indexOf(searchText) > -1) { + if (propertyText.includes(searchText)) { return true; } } @@ -212,7 +215,7 @@ export class ViewGridComponent implements OnInit, OnDestroy { #setInitialColumnOrder(): void { const viewState = this.#dataState.getViewStateById(this.viewId); - const visibleColumns = viewState?.displayedColumnIds || []; + const visibleColumns = viewState?.displayedColumnIds ?? []; this.#columnDefs.sort((col1, col2) => { const col1Index = visibleColumns.findIndex( diff --git a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-repeater.component.ts b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-repeater.component.ts index 9418a23c77..95eb01d0c9 100644 --- a/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-repeater.component.ts +++ b/apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/view-repeater.component.ts @@ -20,6 +20,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { DataManagerDemoRow } from './data'; +import { Filters } from './filters'; @Component({ standalone: true, @@ -48,8 +49,12 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { sortEnabled: true, filterButtonEnabled: true, multiselectToolbarEnabled: true, - onClearAllClick: () => this.#clearAll(), - onSelectAllClick: () => this.#selectAll(), + onClearAllClick: () => { + this.#clearAll(); + }, + onSelectAllClick: () => { + this.#selectAll(); + }, }; readonly #changeDetector = inject(ChangeDetectorRef); @@ -83,7 +88,7 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { } protected onItemSelect(isSelected: boolean, item: DataManagerDemoRow): void { - const selectedItems = this.#dataState.selectedIds || []; + const selectedItems = this.#dataState.selectedIds ?? []; const itemIndex = selectedItems.indexOf(item.id); if (isSelected && itemIndex === -1) { @@ -102,10 +107,10 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { } #updateData(): void { - const selectedIds = this.#dataState.selectedIds || []; + const selectedIds = this.#dataState.selectedIds ?? []; this.items.forEach((item) => { - item.selected = selectedIds.indexOf(item.id) !== -1; + item.selected = selectedIds.includes(item.id); }); this.displayedItems = this.#filterItems(this.#searchItems(this.items)); @@ -131,7 +136,7 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { this.#dataState && this.#dataState.searchText?.toUpperCase(); if (searchText) { - searchedItems = items.filter(function (item: DataManagerDemoRow) { + searchedItems = items.filter((item: DataManagerDemoRow) => { let property: keyof typeof item; for (property in item) { @@ -140,7 +145,7 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { (property === 'name' || property === 'description') ) { const propertyText = item[property].toUpperCase(); - if (propertyText.indexOf(searchText) > -1) { + if (propertyText.includes(searchText)) { return true; } } @@ -157,11 +162,12 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { let filteredItems = items; const filterData = this.#dataState && this.#dataState.filterData; - if (filterData && filterData.filters) { - const filters = filterData.filters; + if (filterData?.filters) { + const filters = filterData.filters as Filters; + filteredItems = items.filter((item: DataManagerDemoRow) => { if ( - ((filters.hideOrange && item.color !== 'orange') || + ((filters.hideOrange && item.color !== 'orange') ?? !filters.hideOrange) && ((filters.type !== 'any' && item.type === filters.type) || !filters.type || @@ -178,7 +184,7 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { } #selectAll(): void { - const selectedIds = this.#dataState.selectedIds || []; + const selectedIds = this.#dataState.selectedIds ?? []; this.displayedItems.forEach((item) => { if (!item.selected) { @@ -193,7 +199,7 @@ export class ViewRepeaterComponent implements OnInit, OnDestroy { } #clearAll(): void { - const selectedIds = this.#dataState.selectedIds || []; + const selectedIds = this.#dataState.selectedIds ?? []; this.displayedItems.forEach((item) => { if (item.selected) { diff --git a/apps/code-examples/src/app/code-examples/datetime/date-range-picker/custom-calculator/demo.component.ts b/apps/code-examples/src/app/code-examples/datetime/date-range-picker/custom-calculator/demo.component.ts index 878a108f44..f8602c4af8 100644 --- a/apps/code-examples/src/app/code-examples/datetime/date-range-picker/custom-calculator/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/datetime/date-range-picker/custom-calculator/demo.component.ts @@ -55,7 +55,7 @@ export class DemoComponent { shortDescription: 'Date before today', type: SkyDateRangeCalculatorType.Before, validate: (value): ValidationErrors | null => { - if (value && value.endDate && value.endDate > new Date()) { + if (value?.endDate && value.endDate > new Date()) { return { dateIsAfterToday: true, }; diff --git a/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.html b/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.html index 7777130d01..2961fe6917 100644 --- a/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.html @@ -17,6 +17,6 @@

- Selected date: {{ formGroup.value.myDate }} + Selected date: {{ startDate.value | json }}

diff --git a/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.ts index 3af2148931..e409c02038 100644 --- a/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/datetime/datepicker/basic/demo.component.ts @@ -13,6 +13,23 @@ import { import { SkyDatepickerModule } from '@skyux/datetime'; import { SkyInputBoxModule } from '@skyux/forms'; +interface DemoForm { + startDate: FormControl; +} + +function validateDate( + control: AbstractControl, +): ValidationErrors | null { + const date = control.value; + const day = date?.getDay(); + + return day !== undefined && (day === 0 || day === 6) + ? { + invalidWeekend: true, + } + : null; +} + @Component({ standalone: true, selector: 'app-demo', @@ -26,31 +43,21 @@ import { SkyInputBoxModule } from '@skyux/forms'; ], }) export class DemoComponent { - protected formGroup: FormGroup; - protected startDate: FormControl; + protected formGroup: FormGroup; + protected startDate: FormControl; + protected helpPopoverContent = 'If you need help with registration, choose a date at least 8 business days after you arrive. The process takes up to 7 business days from the start date.'; - protected hintText = 'Must be before your 1 year anniversary.'; - #formBuilder = inject(FormBuilder); + protected hintText = 'Must be before your 1 year anniversary.'; constructor() { - this.startDate = this.#formBuilder.control(undefined, { - validators: [Validators.required, this.#validateDate], + this.startDate = new FormControl(null, { + validators: [Validators.required, validateDate], }); - this.formGroup = inject(FormBuilder).group({ + + this.formGroup = inject(FormBuilder).group({ startDate: this.startDate, }); } - - #validateDate(control: AbstractControl): ValidationErrors | null { - const date: Date = control.value; - const day = date?.getDay(); - if (day !== undefined && (day === 0 || day === 6)) { - return { - invalidWeekend: true, - }; - } - return null; - } } diff --git a/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.html b/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.html index fea5ceba7e..dcf62da00e 100644 --- a/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.html +++ b/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.html @@ -10,6 +10,6 @@

- Selected date: {{ formGroup.value.myDate }} + Selected date: {{ formGroup.value.startDate | json }}

diff --git a/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.ts b/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.ts index 976f33e9cc..41977ca5ae 100644 --- a/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/datetime/datepicker/custom-dates/demo.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { Component, inject } from '@angular/core'; import { FormBuilder, @@ -21,6 +22,7 @@ import { delay } from 'rxjs/operators'; selector: 'app-demo', templateUrl: './demo.component.html', imports: [ + CommonModule, FormsModule, ReactiveFormsModule, SkyDatepickerModule, diff --git a/apps/code-examples/src/app/code-examples/datetime/timepicker/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/datetime/timepicker/basic/demo.component.ts index 08e2acdbb7..682662604c 100644 --- a/apps/code-examples/src/app/code-examples/datetime/timepicker/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/datetime/timepicker/basic/demo.component.ts @@ -10,9 +10,27 @@ import { ValidationErrors, Validators, } from '@angular/forms'; -import { SkyTimepickerModule } from '@skyux/datetime'; +import { SkyTimepickerModule, SkyTimepickerTimeOutput } from '@skyux/datetime'; import { SkyInputBoxModule } from '@skyux/forms'; +interface DemoForm { + time: FormControl; +} + +function isTimepickerOutput(value: unknown): value is SkyTimepickerTimeOutput { + return !!(value && typeof value === 'object' && 'minute' in value); +} + +function validateTime( + control: AbstractControl, +): ValidationErrors | null { + const minute = isTimepickerOutput(control.value) + ? control.value.minute + : undefined; + + return minute && minute % 15 !== 0 ? { invalidMinute: true } : null; +} + @Component({ standalone: true, selector: 'app-demo', @@ -26,28 +44,22 @@ import { SkyInputBoxModule } from '@skyux/forms'; ], }) export class DemoComponent { - protected formGroup: FormGroup; - protected time: FormControl; + protected formGroup: FormGroup; + protected time: FormControl; + protected hintText = 'Choose a time that allows for late arrivals.'; + protected helpPopoverContent = 'Allow time to complete all activities that your team signed up for. All activities take about 30 minutes, except the ropes course, which takes 60 minutes.'; - #formBuilder = inject(FormBuilder); - constructor() { - this.time = this.#formBuilder.control('2:45', { - validators: [Validators.required, this.#validateTime], + this.time = new FormControl('2:45', { + nonNullable: true, + validators: [Validators.required, validateTime], }); - this.formGroup = this.#formBuilder.group({ + + this.formGroup = inject(FormBuilder).group({ time: this.time, }); } - - #validateTime(control: AbstractControl): ValidationErrors | null { - const minute = control.value?.minute; - if (minute && minute % 15 !== 0) { - return { invalidMinute: true }; - } - return null; - } } diff --git a/apps/code-examples/src/app/code-examples/forms/character-count/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/forms/character-count/demo.component.spec.ts index 3259c83294..6b2c1cbb93 100644 --- a/apps/code-examples/src/app/code-examples/forms/character-count/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/forms/character-count/demo.component.spec.ts @@ -42,12 +42,13 @@ describe('Character count demo', () => { await expectAsync(harness.isOverLimit()).toBeResolvedTo(false); // Update the value to exceed the limit and validate. - const inputEl = document.querySelector( - '.description-input', - ) as HTMLInputElement; + const inputEl = + document.querySelector('.description-input'); - inputEl.value += ' scholarship fund'; - inputEl.dispatchEvent(new Event('input')); + if (inputEl) { + inputEl.value += ' scholarship fund'; + inputEl.dispatchEvent(new Event('input')); + } await expectAsync(harness.getCharacterCount()).toBeResolvedTo(63); await expectAsync(harness.isOverLimit()).toBeResolvedTo(true); diff --git a/apps/code-examples/src/app/code-examples/forms/file-attachment/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/file-attachment/basic/demo.component.ts index 669508f39f..c2d61fa8c1 100644 --- a/apps/code-examples/src/app/code-examples/forms/file-attachment/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/file-attachment/basic/demo.component.ts @@ -47,7 +47,7 @@ export class DemoComponent { } protected validateFile(file: SkyFileItem): string | undefined { - return file.file.name.indexOf('a') === 0 + return file.file.name.startsWith('a') ? 'Upload a file that does not begin with the letter "a"' : undefined; } diff --git a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts index 34ba0988f9..8c6349880a 100644 --- a/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/forms/input-box/basic/demo.component.spec.ts @@ -46,12 +46,16 @@ describe('Basic input box demo', () => { const harness = await setupTest({ dataSkyId: 'input-box-last-name', }); - const inputEl = document.querySelector( + + const inputEl = document.querySelector( 'input.last-name-input-box', - ) as HTMLInputElement; - inputEl.value = ''; - SkyAppTestUtility.fireDomEvent(inputEl, 'input'); - SkyAppTestUtility.fireDomEvent(inputEl, 'blur'); + ); + + if (inputEl) { + inputEl.value = ''; + SkyAppTestUtility.fireDomEvent(inputEl, 'input'); + SkyAppTestUtility.fireDomEvent(inputEl, 'blur'); + } await expectAsync(harness.hasRequiredError()).toBeResolvedTo(true); }); @@ -108,12 +112,14 @@ describe('Basic input box demo', () => { dataSkyId: 'input-box-favorite-color', }); - const selectEl = document.querySelector( + const selectEl = document.querySelector( '.input-box-favorite-color-select', - ) as HTMLSelectElement; + ); - selectEl.value = 'invalid'; - selectEl.dispatchEvent(new Event('change')); + if (selectEl) { + selectEl.value = 'invalid'; + selectEl.dispatchEvent(new Event('change')); + } await expectAsync(harness.hasCustomFormError('invalid')).toBeResolvedTo( true, diff --git a/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.ts index cdaaa0121c..787b7f0a07 100644 --- a/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.ts @@ -11,6 +11,10 @@ import { } from '@angular/forms'; import { SkyRadioModule } from '@skyux/forms'; +interface DemoForm { + paymentMethod: FormControl; +} + interface Item { name: string; value: string; @@ -19,6 +23,12 @@ interface Item { helpContent?: string; } +function validatePaymentMethod( + control: AbstractControl, +): ValidationErrors | null { + return control.value === 'check' ? { processingIssue: true } : null; +} + @Component({ standalone: true, selector: 'app-demo', @@ -26,7 +36,7 @@ interface Item { imports: [CommonModule, FormsModule, ReactiveFormsModule, SkyRadioModule], }) export class DemoComponent { - protected formGroup: FormGroup<{ paymentMethod: FormControl }>; + protected formGroup: FormGroup; protected helpPopoverContent = "We don't charge fees for any payment method. The only exception is when credit card payments are late, which incurs a 2% fee."; protected helpPopoverTitle = 'Are there fees?'; @@ -50,24 +60,18 @@ export class DemoComponent { { name: 'Debit', value: 'debit' }, ]; - #formBuilder = inject(FormBuilder); + readonly #formBuilder = inject(FormBuilder); constructor() { this.paymentMethod = this.#formBuilder.control( this.paymentOptions[0].name, { - validators: this.#validatePaymentMethod, + validators: [validatePaymentMethod], }, ); - this.formGroup = inject(FormBuilder).group({ + + this.formGroup = this.#formBuilder.group({ paymentMethod: this.paymentMethod, }); } - - #validatePaymentMethod(control: AbstractControl): ValidationErrors | null { - if (control.value === 'check') { - return { processingIssue: true }; - } - return null; - } } diff --git a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/demo.component.ts index b474c18bd9..49efa0f6a1 100644 --- a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/demo.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { FormArray, FormBuilder, @@ -12,8 +12,6 @@ import { SkyIdModule } from '@skyux/core'; import { SkyCheckboxModule, SkySelectionBoxModule } from '@skyux/forms'; import { SkyIconModule } from '@skyux/indicators'; -import { Subject, takeUntil } from 'rxjs'; - @Component({ standalone: true, selector: 'app-demo', @@ -28,7 +26,7 @@ import { Subject, takeUntil } from 'rxjs'; SkySelectionBoxModule, ], }) -export class DemoComponent implements OnInit, OnDestroy { +export class DemoComponent { protected checkboxControls: FormControl[] | undefined; protected selectionBoxes: { @@ -58,8 +56,6 @@ export class DemoComponent implements OnInit, OnDestroy { protected formGroup: FormGroup; - #ngUnsubscribe = new Subject(); - readonly #formBuilder = inject(FormBuilder); constructor() { @@ -71,17 +67,6 @@ export class DemoComponent implements OnInit, OnDestroy { }); } - public ngOnInit(): void { - this.formGroup.valueChanges - .pipe(takeUntil(this.#ngUnsubscribe)) - .subscribe((value) => console.log(value)); - } - - public ngOnDestroy(): void { - this.#ngUnsubscribe.next(); - this.#ngUnsubscribe.complete(); - } - #buildCheckboxes(): FormArray { const checkboxArray = this.selectionBoxes.map((checkbox) => this.#formBuilder.control(checkbox.selected), diff --git a/apps/code-examples/src/app/code-examples/forms/selection-box/radio/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/selection-box/radio/demo.component.ts index 3ee5413f5b..07ab306dca 100644 --- a/apps/code-examples/src/app/code-examples/forms/selection-box/radio/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/selection-box/radio/demo.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { FormBuilder, FormGroup, @@ -10,8 +10,6 @@ import { SkyIdModule } from '@skyux/core'; import { SkyRadioModule, SkySelectionBoxModule } from '@skyux/forms'; import { SkyIconModule } from '@skyux/indicators'; -import { Subject, takeUntil } from 'rxjs'; - @Component({ standalone: true, selector: 'app-demo', @@ -26,7 +24,7 @@ import { Subject, takeUntil } from 'rxjs'; SkySelectionBoxModule, ], }) -export class DemoComponent implements OnInit, OnDestroy { +export class DemoComponent { protected items: Record[] = [ { name: 'Save time and effort', @@ -52,22 +50,9 @@ export class DemoComponent implements OnInit, OnDestroy { protected formGroup: FormGroup; - #ngUnsubscribe = new Subject(); - constructor() { this.formGroup = inject(FormBuilder).group({ myOption: this.items[2]['value'], }); } - - public ngOnInit(): void { - this.formGroup.valueChanges - .pipe(takeUntil(this.#ngUnsubscribe)) - .subscribe((value) => console.log(value)); - } - - public ngOnDestroy(): void { - this.#ngUnsubscribe.next(); - this.#ngUnsubscribe.complete(); - } } diff --git a/apps/code-examples/src/app/code-examples/forms/single-file-attachment/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/single-file-attachment/basic/demo.component.ts index f733005c31..a66228fa46 100644 --- a/apps/code-examples/src/app/code-examples/forms/single-file-attachment/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/single-file-attachment/basic/demo.component.ts @@ -47,7 +47,7 @@ export class DemoComponent { protected onFileChange(result: SkyFileAttachmentChange): void { const file = result.file; - if (file && file.errorType) { + if (file?.errorType) { this.#reactiveFile?.setValue(undefined); } else { this.#reactiveFile?.setValue(file); @@ -71,6 +71,6 @@ export class DemoComponent { } protected validateFile(file: SkyFileItem): string { - return file.file.name.indexOf('a') === 0 ? 'invalidStartingLetter' : ''; + return file.file.name.startsWith('a') ? 'invalidStartingLetter' : ''; } } diff --git a/apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.html b/apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.html similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.html rename to apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.html diff --git a/apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.spec.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.spec.ts rename to apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.spec.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/help-inline/basic/demo.component.ts rename to apps/code-examples/src/app/code-examples/help-inline/basic/demo.component.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.html b/apps/code-examples/src/app/code-examples/icon/basic/demo.component.html similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.html rename to apps/code-examples/src/app/code-examples/icon/basic/demo.component.html diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/icon/basic/demo.component.spec.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.spec.ts rename to apps/code-examples/src/app/code-examples/icon/basic/demo.component.spec.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/icon/basic/demo.component.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/basic/demo.component.ts rename to apps/code-examples/src/app/code-examples/icon/basic/demo.component.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.html b/apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.html similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.html rename to apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.html diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.spec.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.spec.ts rename to apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.spec.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.ts b/apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.ts similarity index 100% rename from apps/code-examples/src/app/code-examples/indicators/icon/icon-button/demo.component.ts rename to apps/code-examples/src/app/code-examples/icon/icon-button/demo.component.ts diff --git a/apps/code-examples/src/app/code-examples/indicators/illustration/basic/illustration-demo-resolver.service.ts b/apps/code-examples/src/app/code-examples/indicators/illustration/basic/illustration-demo-resolver.service.ts index b32c0beb8c..22f8627fdd 100644 --- a/apps/code-examples/src/app/code-examples/indicators/illustration/basic/illustration-demo-resolver.service.ts +++ b/apps/code-examples/src/app/code-examples/indicators/illustration/basic/illustration-demo-resolver.service.ts @@ -3,11 +3,12 @@ import { SkyIllustrationResolverService } from '@skyux/indicators'; @Injectable() export class IllustrationDemoResolverService extends SkyIllustrationResolverService { - public override async resolveUrl(name: string): Promise { - if (name === 'analytics-graph') { - return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yIC0yIDk2IDk2IiBoZWlnaHQ9Ijk2IiB3aWR0aD0iOTYiPjxkZWZzPjwvZGVmcz48cGF0aCBkPSJNOTAuMDgzMzMzMzMzMzMzMzQgOTAuMDgzMzMzMzMzMzMzMzRIMy44MzMzMzMzMzMzMzMzMzM1YTEuOTE2NjY2NjY2NjY2NjY2NyAxLjkxNjY2NjY2NjY2NjY2NjcgMCAwIDEgLTEuOTE2NjY2NjY2NjY2NjY2NyAtMS45MTY2NjY2NjY2NjY2NjY3VjEuOTE2NjY2NjY2NjY2NjY2NyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0yOC43NSA2OUE1Ljc1IDUuNzUgMCAxIDAgMjMgNjMuMjUgNS43NSA1Ljc1IDAgMCAwIDI4Ljc1IDY5WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02NC44NTIzMzMzMzMzMzMzMyAzMS44ODE4MzMzMzMzMzMzMzYgMTUuNjI0NjY2NjY2NjY2NjY2IC0xOS45NjAxNjY2NjY2NjY2NjYiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJtMzguMDg0MTY2NjY2NjY2NjcgMjguNTk2NjY2NjY2NjY2NjY4IDE3LjcwMjMzMzMzMzMzMzMzNSA2LjMxNzMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im0xLjkxNjY2NjY2NjY2NjY2NjcgNDUuMTc1ODMzMzMzMzMzMzQgMjUuNzU2MTY2NjY2NjY2NjcgLTE1LjMzMzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02Mi40ODMzMzMzMzMzMzMzNCA2MC4zOTggMTYuODg1ODMzMzMzMzMzMzM0IC05LjU4MzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0zNC41IDYzLjI1aDE3LjI1IiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0ibTEuOTE2NjY2NjY2NjY2NjY2NyA4MC41IDIxLjkwMzY2NjY2NjY2NjY3IC0xNC4yOTgzMzMzMzMzMzMzMzQiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNMzIuNTgzMzMzMzMzMzMzMzM2IDMyLjU4MzMzMzMzMzMzMzMzNkE1Ljc1IDUuNzUgMCAxIDAgMjYuODMzMzMzMzMzMzMzMzM2IDI2LjgzMzMzMzMzMzMzMzMzNmE1Ljc1IDUuNzUgMCAwIDAgNS43NSA1Ljc1WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik02MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA2MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0iTTg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OEE1Ljc1IDUuNzUgMCAxIDAgNzguNTgzMzMzMzMzMzMzMzQgNy42NjY2NjY2NjY2NjY2NjcgNS43NSA1Ljc1IDAgMCAwIDg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OFoiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNODQuMzMzMzMzMzMzMzMzMzQgNTMuNjY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA4NC4zMzMzMzMzMzMzMzMzNCA1My42NjY2NjY2NjY2NjY2N1oiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNNTcuNSA2OWE1Ljc1IDUuNzUgMCAxIDAgLTUuNzUgLTUuNzVBNS43NSA1Ljc1IDAgMCAwIDU3LjUgNjlaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PC9zdmc+'; - } + public override resolveUrl(name: string): Promise { + const url = + name === 'analytics-graph' + ? 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yIC0yIDk2IDk2IiBoZWlnaHQ9Ijk2IiB3aWR0aD0iOTYiPjxkZWZzPjwvZGVmcz48cGF0aCBkPSJNOTAuMDgzMzMzMzMzMzMzMzQgOTAuMDgzMzMzMzMzMzMzMzRIMy44MzMzMzMzMzMzMzMzMzM1YTEuOTE2NjY2NjY2NjY2NjY2NyAxLjkxNjY2NjY2NjY2NjY2NjcgMCAwIDEgLTEuOTE2NjY2NjY2NjY2NjY2NyAtMS45MTY2NjY2NjY2NjY2NjY3VjEuOTE2NjY2NjY2NjY2NjY2NyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0yOC43NSA2OUE1Ljc1IDUuNzUgMCAxIDAgMjMgNjMuMjUgNS43NSA1Ljc1IDAgMCAwIDI4Ljc1IDY5WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02NC44NTIzMzMzMzMzMzMzMyAzMS44ODE4MzMzMzMzMzMzMzYgMTUuNjI0NjY2NjY2NjY2NjY2IC0xOS45NjAxNjY2NjY2NjY2NjYiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJtMzguMDg0MTY2NjY2NjY2NjcgMjguNTk2NjY2NjY2NjY2NjY4IDE3LjcwMjMzMzMzMzMzMzMzNSA2LjMxNzMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im0xLjkxNjY2NjY2NjY2NjY2NjcgNDUuMTc1ODMzMzMzMzMzMzQgMjUuNzU2MTY2NjY2NjY2NjcgLTE1LjMzMzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02Mi40ODMzMzMzMzMzMzMzNCA2MC4zOTggMTYuODg1ODMzMzMzMzMzMzM0IC05LjU4MzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0zNC41IDYzLjI1aDE3LjI1IiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0ibTEuOTE2NjY2NjY2NjY2NjY2NyA4MC41IDIxLjkwMzY2NjY2NjY2NjY3IC0xNC4yOTgzMzMzMzMzMzMzMzQiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNMzIuNTgzMzMzMzMzMzMzMzM2IDMyLjU4MzMzMzMzMzMzMzMzNkE1Ljc1IDUuNzUgMCAxIDAgMjYuODMzMzMzMzMzMzMzMzM2IDI2LjgzMzMzMzMzMzMzMzMzNmE1Ljc1IDUuNzUgMCAwIDAgNS43NSA1Ljc1WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik02MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA2MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0iTTg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OEE1Ljc1IDUuNzUgMCAxIDAgNzguNTgzMzMzMzMzMzMzMzQgNy42NjY2NjY2NjY2NjY2NjcgNS43NSA1Ljc1IDAgMCAwIDg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OFoiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNODQuMzMzMzMzMzMzMzMzMzQgNTMuNjY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA4NC4zMzMzMzMzMzMzMzMzNCA1My42NjY2NjY2NjY2NjY2N1oiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNNTcuNSA2OWE1Ljc1IDUuNzUgMCAxIDAgLTUuNzUgLTUuNzVBNS43NSA1Ljc1IDAgMCAwIDU3LjUgNjlaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PC9zdmc+' + : ''; - return ''; + return Promise.resolve(url); } } diff --git a/apps/code-examples/src/app/code-examples/indicators/tokens/custom/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/indicators/tokens/custom/demo.component.spec.ts index 14bb17ca42..ce4c02aa73 100644 --- a/apps/code-examples/src/app/code-examples/indicators/tokens/custom/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/indicators/tokens/custom/demo.component.spec.ts @@ -26,9 +26,11 @@ describe('Tokens basic demo', () => { fixture: ComponentFixture, buttonName: 'change' | 'destroy' | 'focus-last' | 'reset', ): void { - fixture.nativeElement - .querySelector(`.tokens-demo-${buttonName}-btn`) - .click(); + const btn = ( + fixture.nativeElement as HTMLElement + ).querySelector(`.tokens-demo-${buttonName}-btn`); + + btn?.click(); } beforeEach(() => { @@ -59,8 +61,10 @@ describe('Tokens basic demo', () => { await employedToken.select(); expect( - fixture.nativeElement.querySelector('.tokens-demo-selected').innerText, - ).toBe('Employed'); + (fixture.nativeElement as HTMLElement).querySelector( + '.tokens-demo-selected', + )?.textContent, + ).toEqual('Employed'); }); it('should change tokens when the user clicks the "Change tokens" button', async () => { diff --git a/apps/code-examples/src/app/code-examples/indicators/wait/element/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/indicators/wait/element/demo.component.spec.ts index 09eec2c8c9..421d95ff8e 100644 --- a/apps/code-examples/src/app/code-examples/indicators/wait/element/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/indicators/wait/element/demo.component.spec.ts @@ -27,7 +27,10 @@ describe('Basic wait', () => { it('should show the wait component when the user performs an action', async () => { const { waitHarness, fixture } = await setupTest(); - fixture.nativeElement.querySelector('.sky-btn').click(); + (fixture.nativeElement as HTMLElement) + .querySelector('.sky-btn') + ?.click(); + fixture.detectChanges(); await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(true); diff --git a/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.spec.ts index 6f48453823..2acfa7999a 100644 --- a/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.spec.ts @@ -8,12 +8,14 @@ import { DemoComponent } from './demo.component'; describe('Page wait', () => { function setupTest(): { rootLoader: HarnessLoader; + el: HTMLElement; fixture: ComponentFixture; } { const fixture = TestBed.createComponent(DemoComponent); const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const el = fixture.nativeElement as HTMLElement; - return { rootLoader, fixture }; + return { rootLoader, el, fixture }; } beforeEach(() => { @@ -23,8 +25,9 @@ describe('Page wait', () => { }); it('should show the page wait component when the user performs an action', async () => { - const { rootLoader, fixture } = setupTest(); - const buttons = fixture.nativeElement.querySelectorAll('.sky-btn'); + const { rootLoader, el } = setupTest(); + + const buttons = el.querySelectorAll('.sky-btn'); buttons[0].click(); @@ -38,8 +41,9 @@ describe('Page wait', () => { }); it('should show the non-blocking page wait component when the user performs an action', async () => { - const { rootLoader, fixture } = setupTest(); - const buttons = fixture.nativeElement.querySelectorAll('.sky-btn'); + const { rootLoader, el } = setupTest(); + + const buttons = el.querySelectorAll('.sky-btn'); buttons[1].click(); diff --git a/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.ts b/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.ts index d189695efc..f69d90f345 100644 --- a/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/indicators/wait/page/demo.component.ts @@ -22,12 +22,10 @@ export class DemoComponent implements OnDestroy { } else { this.#waitSvc.beginNonBlockingPageWait(); } + } else if (isBlocking) { + this.#waitSvc.endBlockingPageWait(); } else { - if (isBlocking) { - this.#waitSvc.endBlockingPageWait(); - } else { - this.#waitSvc.endNonBlockingPageWait(); - } + this.#waitSvc.endNonBlockingPageWait(); } this.isWaiting = !this.isWaiting; diff --git a/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.html b/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.html index fb9fcab1ad..35b9e8dce3 100644 --- a/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.html @@ -25,11 +25,9 @@ -
-
- - - -
+ + + +
diff --git a/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.ts index 3c24513c27..36f475c57f 100644 --- a/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/inline-form/inline-form/basic/demo.component.ts @@ -15,6 +15,10 @@ import { SkyInlineFormModule, } from '@skyux/inline-form'; +interface DemoForm { + firstName: FormControl; +} + @Component({ standalone: true, selector: 'app-demo', @@ -29,7 +33,7 @@ import { }) export class DemoComponent { protected firstName = 'Jane'; - protected formGroup: FormGroup; + protected formGroup: FormGroup; protected inlineFormConfig: SkyInlineFormConfig = { buttonLayout: SkyInlineFormButtonLayout.SaveCancel, @@ -39,25 +43,25 @@ export class DemoComponent { constructor() { this.formGroup = inject(FormBuilder).group({ - myFirstName: new FormControl(), + firstName: new FormControl('', { nonNullable: true }), }); } protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { if (args.reason === 'save') { - this.firstName = this.formGroup.get('myFirstName')?.value; + this.firstName = this.formGroup.value.firstName ?? ''; } this.showForm = false; this.formGroup.patchValue({ - myFirstName: undefined, + firstName: undefined, }); } protected onInlineFormOpen(): void { this.showForm = true; this.formGroup.patchValue({ - myFirstName: this.firstName, + firstName: this.firstName, }); } } diff --git a/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.html b/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.html index d82531c82a..004db6ffe1 100644 --- a/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.html +++ b/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.html @@ -25,11 +25,9 @@ -
-
- - - -
+ + + +
diff --git a/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.ts b/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.ts index 64ed1db249..ffcf724a6f 100644 --- a/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/inline-form/inline-form/custom-buttons/demo.component.ts @@ -15,6 +15,10 @@ import { SkyInlineFormModule, } from '@skyux/inline-form'; +interface DemoForm { + firstName: FormControl; +} + @Component({ standalone: true, selector: 'app-demo', @@ -29,7 +33,7 @@ import { }) export class DemoComponent implements OnInit { protected firstName = 'Jane'; - protected formGroup: FormGroup; + protected formGroup: FormGroup; protected inlineFormConfig: SkyInlineFormConfig = { buttonLayout: SkyInlineFormButtonLayout.Custom, @@ -61,7 +65,7 @@ export class DemoComponent implements OnInit { constructor() { this.formGroup = inject(FormBuilder).group({ - myFirstName: new FormControl(), + firstName: new FormControl('', { nonNullable: true }), }); } @@ -80,16 +84,16 @@ export class DemoComponent implements OnInit { protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { switch (args.reason) { case 'save': - this.firstName = this.formGroup.get('myFirstName')?.value; + this.firstName = this.formGroup.value.firstName ?? ''; this.showForm = false; break; case 'clear': - this.formGroup.get('myFirstName')?.patchValue(undefined); + this.formGroup.patchValue({ firstName: '' }); break; case 'reset': - this.formGroup.get('myFirstName')?.setValue(this.firstName); + this.formGroup.setValue({ firstName: this.firstName }); break; default: @@ -101,7 +105,7 @@ export class DemoComponent implements OnInit { protected onInlineFormOpen(): void { this.showForm = true; this.formGroup.patchValue({ - myFirstName: this.firstName, + firstName: this.firstName, }); } } diff --git a/apps/code-examples/src/app/code-examples/inline-form/inline-form/repeaters/demo.component.ts b/apps/code-examples/src/app/code-examples/inline-form/inline-form/repeaters/demo.component.ts index 1053b0ca58..a10254dda3 100644 --- a/apps/code-examples/src/app/code-examples/inline-form/inline-form/repeaters/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/inline-form/inline-form/repeaters/demo.component.ts @@ -16,6 +16,12 @@ import { } from '@skyux/inline-form'; import { SkyRepeaterModule } from '@skyux/lists'; +interface DemoForm { + id: FormControl; + note: FormControl; + title: FormControl; +} + interface Item { id: string; title: string | undefined; @@ -37,6 +43,7 @@ interface Item { }) export class DemoComponent { protected activeInlineFormId: string | undefined; + protected formGroup: FormGroup; protected inlineFormConfig: SkyInlineFormConfig = { buttonLayout: SkyInlineFormButtonLayout.SaveCancel, @@ -65,13 +72,11 @@ export class DemoComponent { }, ]; - protected formGroup: FormGroup; - constructor() { this.formGroup = inject(FormBuilder).group({ - id: new FormControl(), - title: new FormControl(), - note: new FormControl(), + id: new FormControl('', { nonNullable: true }), + title: new FormControl('', { nonNullable: true }), + note: new FormControl('', { nonNullable: true }), }); } @@ -88,9 +93,10 @@ export class DemoComponent { const found = this.items.find( (item) => item.id === this.activeInlineFormId, ); + if (found) { - found.note = this.formGroup.get('note')?.value; - found.title = this.formGroup.get('title')?.value; + found.note = this.formGroup.value.note; + found.title = this.formGroup.value.title; } } diff --git a/apps/code-examples/src/app/code-examples/layout/back-to-top/infinite-scroll/demo.component.ts b/apps/code-examples/src/app/code-examples/layout/back-to-top/infinite-scroll/demo.component.ts index 61b9031fbf..5c185fe9ff 100644 --- a/apps/code-examples/src/app/code-examples/layout/back-to-top/infinite-scroll/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/layout/back-to-top/infinite-scroll/demo.component.ts @@ -129,21 +129,18 @@ export class DemoComponent implements OnInit { ]; public ngOnInit(): void { - this.addData(0, 5); + void this.#addData(0, 5); } public onScrollEnd(): void { - this.addData(this.personList.length, 5); + void this.#addData(this.personList.length, 5); } - private addData(start: number, rowSize: number): void { + async #addData(start: number, rowSize: number): Promise { if (this.hasMore) { - this.mockRemote(start, rowSize).then( - (result: { data: Person[]; hasMore: boolean }) => { - this.personList = this.personList.concat(result.data); - this.hasMore = result.hasMore; - }, - ); + const result = await this.mockRemote(start, rowSize); + this.personList = this.personList.concat(result.data); + this.hasMore = result.hasMore; } } diff --git a/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.html b/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.html index 7c0eaa8988..875eda546b 100644 --- a/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.html @@ -1,7 +1,8 @@ - - -

Box header

-
+ diff --git a/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.spec.ts index 44bac8fd95..b5b349ae14 100644 --- a/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.spec.ts @@ -4,7 +4,7 @@ import { SkyBoxHarness } from '@skyux/layout/testing'; import { DemoComponent } from './demo.component'; -describe('Basic box', async () => { +describe('Basic box', () => { async function setupTest(): Promise<{ boxHarness: SkyBoxHarness; fixture: ComponentFixture; @@ -31,6 +31,6 @@ describe('Basic box', async () => { fixture.detectChanges(); - await expectAsync(boxHarness.getAriaLabel()).toBeResolvedTo('boxDemo'); + await expectAsync(boxHarness.getAriaLabel()).toBeResolvedTo('Box header'); }); }); diff --git a/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.html b/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.html deleted file mode 100644 index 4c0c132119..0000000000 --- a/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - -

Box header

- -
- - - - - - - - - - - - - -

Box content

-
-
diff --git a/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.ts b/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.ts deleted file mode 100644 index 5d78c18ac8..0000000000 --- a/apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { SkyHelpInlineModule } from '@skyux/indicators'; -import { SkyBoxModule } from '@skyux/layout'; -import { SkyDropdownModule } from '@skyux/popovers'; - -@Component({ - standalone: true, - selector: 'app-demo', - templateUrl: './demo.component.html', - imports: [SkyBoxModule, SkyDropdownModule, SkyHelpInlineModule], -}) -export class DemoComponent { - protected onActionClick(): void { - alert('Help inline button clicked!'); - } -} diff --git a/apps/code-examples/src/app/code-examples/lists/filter/modal/demo.component.ts b/apps/code-examples/src/app/code-examples/lists/filter/modal/demo.component.ts index 03e028b0d2..feb85c9a3a 100644 --- a/apps/code-examples/src/app/code-examples/lists/filter/modal/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lists/filter/modal/demo.component.ts @@ -82,7 +82,7 @@ export class DemoComponent { modalInstance.closed.subscribe((result: SkyModalCloseArgs) => { if (result.reason === 'save') { - this.appliedFilters = result.data.slice(); + this.appliedFilters = (result.data as Filter[]).slice(); this.filteredItems = this.#filterItems(this.items, this.appliedFilters); this.#changeDetectorRef.markForCheck(); } diff --git a/apps/code-examples/src/app/code-examples/lists/infinite-scroll/repeater/demo.component.ts b/apps/code-examples/src/app/code-examples/lists/infinite-scroll/repeater/demo.component.ts index e559918326..ca99fb3f36 100644 --- a/apps/code-examples/src/app/code-examples/lists/infinite-scroll/repeater/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lists/infinite-scroll/repeater/demo.component.ts @@ -17,22 +17,19 @@ export class DemoComponent implements OnInit { protected itemsHaveMore = true; public ngOnInit(): void { - this.#addData(); + void this.#addData(); } protected onScrollEnd(): void { if (this.itemsHaveMore) { - this.#addData(); + void this.#addData(); } } - #addData(): void { - this.#mockRemote().then( - (result: { data: InfiniteScrollDemoItem[]; hasMore: boolean }) => { - this.items = this.items.concat(result.data); - this.itemsHaveMore = result.hasMore; - }, - ); + async #addData(): Promise { + const result = await this.#mockRemote(); + this.items = this.items.concat(result.data); + this.itemsHaveMore = result.hasMore; } #mockRemote(): Promise<{ diff --git a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.ts b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.ts index d56b6d4f31..195e2a0f48 100644 --- a/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lists/paging/with-content/demo.component.ts @@ -32,9 +32,11 @@ export class DemoComponent { protected pagedData = this.contentChange.pipe( switchMap((args) => - this.#demoDataSvc - .getPagedData(args.currentPage, this.pageSize) - .pipe(tap(() => args.loadingComplete())), + this.#demoDataSvc.getPagedData(args.currentPage, this.pageSize).pipe( + tap(() => { + args.loadingComplete(); + }), + ), ), shareReplay(1), ); diff --git a/apps/code-examples/src/app/code-examples/lists/repeater/add-remove/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lists/repeater/add-remove/demo.component.spec.ts index 52cc6a1b12..7b71b1d78d 100644 --- a/apps/code-examples/src/app/code-examples/lists/repeater/add-remove/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lists/repeater/add-remove/demo.component.spec.ts @@ -1,18 +1,15 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - SkyRepeaterHarness, - SkyRepeaterItemHarness, -} from '@skyux/lists/testing'; +import { SkyRepeaterHarness } from '@skyux/lists/testing'; import { DemoComponent } from './demo.component'; describe('Repeater add remove demo', () => { async function setupTest(): Promise<{ - repeaterHarness: SkyRepeaterHarness | null; - repeaterItems: SkyRepeaterItemHarness[] | null; + el: HTMLElement; fixture: ComponentFixture; + repeaterHarness: SkyRepeaterHarness; }> { const fixture = TestBed.createComponent(DemoComponent); const loader = TestbedHarnessEnvironment.loader(fixture); @@ -21,9 +18,9 @@ describe('Repeater add remove demo', () => { SkyRepeaterHarness.with({ dataSkyId: 'repeater-demo' }), ); - const repeaterItems = await repeaterHarness.getRepeaterItems(); + const el = fixture.nativeElement as HTMLElement; - return { repeaterHarness, repeaterItems, fixture }; + return { el, fixture, repeaterHarness }; } beforeEach(() => { @@ -33,11 +30,13 @@ describe('Repeater add remove demo', () => { }); it('should allow items to be expanded and collapsed', async () => { - const { repeaterItems } = await setupTest(); + const { repeaterHarness } = await setupTest(); + + const repeaterItems = await repeaterHarness.getRepeaterItems(); let first = true; - for (const item of repeaterItems!) { + for (const item of repeaterItems) { await expectAsync(item.isCollapsible()).toBeResolvedTo(true); // in single expand mode, the first item is expanded by default @@ -75,72 +74,68 @@ describe('Repeater add remove demo', () => { }, ]; - let repeaterItems = await repeaterHarness?.getRepeaterItems(); + let repeaterItems = await repeaterHarness.getRepeaterItems(); expect(repeaterItems).toBeDefined(); - expect(repeaterItems?.length).toBe(expectedContent.length); + expect(repeaterItems.length).toBe(expectedContent.length); - if (repeaterItems) { - for (const item of repeaterItems) { - await expectAsync(item.isReorderable()).toBeResolvedTo(true); - } + for (const item of repeaterItems) { + await expectAsync(item.isReorderable()).toBeResolvedTo(true); + } - await expectAsync(repeaterItems?.[1].getTitleText()).toBeResolvedTo( - expectedContent[1].title, - ); + await expectAsync(repeaterItems[1].getTitleText()).toBeResolvedTo( + expectedContent[1].title, + ); - await repeaterItems?.[1].sendToTop(); - repeaterItems = await repeaterHarness?.getRepeaterItems(); + await repeaterItems[1].sendToTop(); + repeaterItems = await repeaterHarness.getRepeaterItems(); - await expectAsync(repeaterItems?.[1].getTitleText()).toBeResolvedTo( - expectedContent[0].title, - ); - } + await expectAsync(repeaterItems[1].getTitleText()).toBeResolvedTo( + expectedContent[0].title, + ); }); it('should allow items to be added and removed', async () => { - const { repeaterHarness, fixture } = await setupTest(); + const { repeaterHarness, el, fixture } = await setupTest(); - let repeaterItems = await repeaterHarness?.getRepeaterItems(); + let repeaterItems = await repeaterHarness.getRepeaterItems(); expect(repeaterItems).toBeDefined(); - expect(repeaterItems?.length).toBe(4); + expect(repeaterItems.length).toBe(4); - if (repeaterItems) { - for (const item of repeaterItems) { - await expectAsync(item.isSelectable()).toBeResolvedTo(true); - } + for (const item of repeaterItems) { + await expectAsync(item.isSelectable()).toBeResolvedTo(true); + } - const addButton = fixture.nativeElement.querySelector( - '[data-sky-id="add-button"]', - ); + const addButton = el.querySelector( + '[data-sky-id="add-button"]', + ); - const removeButton = fixture.nativeElement.querySelector( - '[data-sky-id="remove-button"]', - ); + const removeButton = el.querySelector( + '[data-sky-id="remove-button"]', + ); - addButton.click(); - fixture.detectChanges(); + addButton?.click(); + fixture.detectChanges(); - repeaterItems = await repeaterHarness?.getRepeaterItems(); - expect(repeaterItems).toBeDefined(); - expect(repeaterItems?.length).toBe(5); + repeaterItems = await repeaterHarness.getRepeaterItems(); + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(5); - await expectAsync(repeaterItems?.[0].isSelected()).toBeResolvedTo(false); - await repeaterItems?.[0].select(); + await expectAsync(repeaterItems[0].isSelected()).toBeResolvedTo(false); + await repeaterItems[0].select(); - await expectAsync(repeaterItems?.[0].isSelected()).toBeResolvedTo(true); - await expectAsync(repeaterItems?.[1].isSelected()).toBeResolvedTo(false); + await expectAsync(repeaterItems[0].isSelected()).toBeResolvedTo(true); + await expectAsync(repeaterItems[1].isSelected()).toBeResolvedTo(false); - await repeaterItems?.[1].select(); - await expectAsync(repeaterItems?.[1].isSelected()).toBeResolvedTo(true); + await repeaterItems[1].select(); + await expectAsync(repeaterItems[1].isSelected()).toBeResolvedTo(true); - removeButton.click(); - fixture.detectChanges(); + removeButton?.click(); + fixture.detectChanges(); - repeaterItems = await repeaterHarness?.getRepeaterItems(); - expect(repeaterItems).toBeDefined(); - expect(repeaterItems?.length).toBe(3); - } + repeaterItems = await repeaterHarness.getRepeaterItems(); + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(3); }); }); diff --git a/apps/code-examples/src/app/code-examples/lists/repeater/inline-form/demo.component.ts b/apps/code-examples/src/app/code-examples/lists/repeater/inline-form/demo.component.ts index 1053b0ca58..c7514e826e 100644 --- a/apps/code-examples/src/app/code-examples/lists/repeater/inline-form/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lists/repeater/inline-form/demo.component.ts @@ -16,6 +16,12 @@ import { } from '@skyux/inline-form'; import { SkyRepeaterModule } from '@skyux/lists'; +interface DemoForm { + id: FormControl; + note: FormControl; + title: FormControl; +} + interface Item { id: string; title: string | undefined; @@ -37,6 +43,7 @@ interface Item { }) export class DemoComponent { protected activeInlineFormId: string | undefined; + protected formGroup: FormGroup; protected inlineFormConfig: SkyInlineFormConfig = { buttonLayout: SkyInlineFormButtonLayout.SaveCancel, @@ -65,13 +72,11 @@ export class DemoComponent { }, ]; - protected formGroup: FormGroup; - constructor() { this.formGroup = inject(FormBuilder).group({ - id: new FormControl(), - title: new FormControl(), - note: new FormControl(), + id: new FormControl('', { nonNullable: true }), + title: new FormControl('', { nonNullable: true }), + note: new FormControl('', { nonNullable: true }), }); } @@ -89,8 +94,8 @@ export class DemoComponent { (item) => item.id === this.activeInlineFormId, ); if (found) { - found.note = this.formGroup.get('note')?.value; - found.title = this.formGroup.get('title')?.value; + found.note = this.formGroup.value.note; + found.title = this.formGroup.value.title; } } diff --git a/apps/code-examples/src/app/code-examples/lists/sort/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/lists/sort/basic/demo.component.ts index c7d9eda81d..0a33ff86c7 100644 --- a/apps/code-examples/src/app/code-examples/lists/sort/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lists/sort/basic/demo.component.ts @@ -102,7 +102,7 @@ export class DemoComponent implements OnInit { } protected sortItems(option: SortOption): void { - this.sortedItems = this.sortedItems.sort(function (a: Item, b: Item) { + this.sortedItems = this.sortedItems.sort((a, b) => { const descending = option.descending ? -1 : 1; const sortProperty: keyof typeof a = option.name; diff --git a/apps/code-examples/src/app/code-examples/lookup/autocomplete/advanced/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/autocomplete/advanced/demo.component.ts index fc7cde552c..f61f04b2cd 100644 --- a/apps/code-examples/src/app/code-examples/lookup/autocomplete/advanced/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/autocomplete/advanced/demo.component.ts @@ -68,6 +68,6 @@ export class DemoComponent { } protected onPlanetSelection(args: SkyAutocompleteSelectionChange): void { - alert(`You selected ${args.selectedItem.name}`); + alert(`You selected ${(args.selectedItem as Planet).name}`); } } diff --git a/apps/code-examples/src/app/code-examples/lookup/autocomplete/custom-search/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/autocomplete/custom-search/demo.component.ts index d12c1be29b..a91d8b8cdc 100644 --- a/apps/code-examples/src/app/code-examples/lookup/autocomplete/custom-search/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/autocomplete/custom-search/demo.component.ts @@ -61,7 +61,7 @@ export class DemoComponent { const results = oceans.filter((ocean: Ocean) => { const val = ocean.title; const isMatch = - val && val.toString().toLowerCase().indexOf(searchTextLower) > -1; + val && val.toString().toLowerCase().includes(searchTextLower); return isMatch; }); diff --git a/apps/code-examples/src/app/code-examples/lookup/country-field/basic/demo.component.html b/apps/code-examples/src/app/code-examples/lookup/country-field/basic/demo.component.html index 54f11adf5d..b7c743d524 100644 --- a/apps/code-examples/src/app/code-examples/lookup/country-field/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/lookup/country-field/basic/demo.component.html @@ -4,7 +4,7 @@ labelText="Country" [helpPopoverContent]="helpPopoverContent" > - + ; +} + +function validateCountry( + control: AbstractControl, +): ValidationErrors | null { + return control.value?.name === 'Mexico' ? { invalidCountry: true } : null; +} @Component({ standalone: true, @@ -25,30 +35,26 @@ import { SkyCountryFieldModule } from '@skyux/lookup'; ], }) export class DemoComponent { - protected countryControl: FormControl; - protected countryForm: FormGroup; + protected countryControl: FormControl; + protected countryForm: FormGroup; + protected helpPopoverContent = 'We use the country to validate your passport within 10 business days. You can update it at any time.'; constructor() { - this.countryControl = new FormControl(undefined, { - validators: [this.#validateCountry, Validators.required], - }); - - this.countryControl.setValue({ - name: 'Australia', - iso2: 'au', - }); + this.countryControl = new FormControl( + { + name: 'Australia', + iso2: 'au', + }, + { + nonNullable: true, + validators: [validateCountry, Validators.required], + }, + ); this.countryForm = new FormGroup({ - countryControl: this.countryControl, + country: this.countryControl, }); } - - #validateCountry(control: AbstractControl): ValidationErrors | null { - if (control.value?.name === 'Mexico') { - return { invalidCountry: true }; - } - return null; - } } diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.html b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.html index 74a389599f..293f6c5cc2 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.html +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.html @@ -15,10 +15,10 @@ formControlName="favoriteNames" idProperty="name" [enableShowMore]="true" - (searchAsync)="searchAsync($event)" [showAddButton]="true" [showMoreConfig]="showMoreConfig" (addClick)="addClick($event)" + (searchAsync)="searchAsync($event)" />
{ beforeEach(() => { // Create a mock search service. In a real-world application, the search // service would make a web request which should be avoided in unit tests. - mockSvc = jasmine.createSpyObj('DemoService', ['search']); + mockSvc = jasmine.createSpyObj('DemoService', ['search']); TestBed.configureTestingModule({ imports: [DemoComponent, NoopAnimationsModule], diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.ts index dee894089e..0babab6dac 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.ts @@ -16,7 +16,7 @@ import { SkyLookupModule, SkyLookupShowMoreConfig, } from '@skyux/lookup'; -import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; +import { SkyModalService } from '@skyux/modals'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -52,7 +52,6 @@ export class DemoComponent implements OnInit, OnDestroy { #subscriptions = new Subscription(); readonly #svc = inject(DemoService); - readonly #modalSvc = inject(SkyModalService); readonly #waitSvc = inject(SkyWaitService); @@ -96,16 +95,19 @@ export class DemoComponent implements OnInit, OnDestroy { public addClick(args: SkyLookupAddClickEventArgs): void { const modal = this.#modalSvc.open(AddItemModalComponent); + this.#subscriptions.add( - modal.closed.subscribe((close: SkyModalCloseArgs) => { + modal.closed.subscribe((close) => { if (close.reason === 'save') { + const person = close.data as Person; + this.#subscriptions.add( this.#waitSvc - .blockingWrap(this.#svc.addPerson(close.data)) + .blockingWrap(this.#svc.addPerson(person)) .subscribe((data) => { args.itemAdded({ - item: close.data, - data: data, + item: person, + data, }); }), ); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.html b/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.html deleted file mode 100644 index fc7b66a9c3..0000000000 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
- - - - -
- Form model: -
{{ favoritesForm.value | json }}
-
- -
diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.spec.ts deleted file mode 100644 index c7e3d3865d..0000000000 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { SkyInputBoxHarness } from '@skyux/forms/testing'; -import { SkyLookupHarness } from '@skyux/lookup/testing'; - -import { of } from 'rxjs'; - -import { DemoComponent } from './demo.component'; -import { DemoService } from './demo.service'; - -describe('Lookup asynchronous search demo', () => { - let mockSvc!: jasmine.SpyObj; - - async function setupTest(): Promise<{ - lookupHarness: SkyLookupHarness; - fixture: ComponentFixture; - }> { - const fixture = TestBed.createComponent(DemoComponent); - const loader = TestbedHarnessEnvironment.loader(fixture); - - const lookupHarness = await ( - await loader.getHarness( - SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), - ) - ).queryHarness(SkyLookupHarness); - - return { lookupHarness, fixture }; - } - - beforeEach(() => { - // Create a mock search service. In a real-world application, the search - // service would make a web request which should be avoided in unit tests. - mockSvc = jasmine.createSpyObj('DemoService', ['search']); - - TestBed.configureTestingModule({ - imports: [DemoComponent, NoopAnimationsModule], - providers: [ - { - provide: DemoService, - useValue: mockSvc, - }, - ], - }); - }); - - it('should set the expected initial value', async () => { - const { lookupHarness } = await setupTest(); - - await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ - 'Shirley', - ]); - }); - - it('should update the form control when a favorite name is selected', async () => { - const { lookupHarness, fixture } = await setupTest(); - - mockSvc.search.and.callFake((searchText) => - of({ - hasMore: false, - people: - searchText === 'b' - ? [ - { - name: 'Bernard', - }, - ] - : [], - totalCount: 1, - }), - ); - - await lookupHarness.enterText('b'); - await lookupHarness.selectSearchResult({ - text: 'Bernard', - }); - - expect(fixture.componentInstance.favoritesForm.value.favoriteNames).toEqual( - [{ name: 'Shirley' }, { name: 'Bernard' }], - ); - }); - - it('should respect the selection descriptor', async () => { - const { lookupHarness } = await setupTest(); - - mockSvc.search.and.callFake(() => - of({ - hasMore: false, - people: [ - { - id: '21', - name: 'Bernard', - }, - ], - totalCount: 1, - }), - ); - - await lookupHarness.clickShowMoreButton(); - - const picker = await lookupHarness.getShowMorePicker(); - - await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( - 'Search names', - ); - await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( - 'Select names', - ); - }); -}); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.ts deleted file mode 100644 index f3b6edf0b7..0000000000 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, OnInit, inject } from '@angular/core'; -import { - FormBuilder, - FormControl, - FormGroup, - FormsModule, - ReactiveFormsModule, - ValidationErrors, -} from '@angular/forms'; -import { SkyFormErrorModule, SkyInputBoxModule } from '@skyux/forms'; -import { SkyWaitService } from '@skyux/indicators'; -import { - SkyAutocompleteSearchAsyncArgs, - SkyLookupAddClickEventArgs, - SkyLookupModule, - SkyLookupShowMoreConfig, -} from '@skyux/lookup'; - -import { map } from 'rxjs/operators'; - -import { DemoService } from './demo.service'; -import { Person } from './person'; - -@Component({ - standalone: true, - selector: 'app-demo', - templateUrl: './demo.component.html', - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - SkyFormErrorModule, - SkyInputBoxModule, - SkyLookupModule, - ], -}) -export class DemoComponent implements OnInit { - public favoritesForm: FormGroup<{ - favoriteNames: FormControl; - }>; - - public showMoreConfig: SkyLookupShowMoreConfig = { - nativePickerConfig: { - selectionDescriptor: 'names', - }, - }; - - readonly #svc = inject(DemoService); - readonly #waitSvc = inject(SkyWaitService); - - constructor() { - const names = new FormControl([{ name: 'Shirley' }], { - validators: [ - (control): ValidationErrors => { - if ( - control.value?.some((person: Person) => !person.name.match(/e/i)) - ) { - return { letterE: true }; - } - - return {}; - }, - ], - }); - - this.favoritesForm = inject(FormBuilder).group({ - favoriteNames: names, - }); - } - - public ngOnInit(): void { - // If you need to execute some logic after the lookup values change, - // subscribe to Angular's built-in value changes observable. - this.favoritesForm.valueChanges.subscribe((changes) => { - console.log('Lookup value changes:', changes); - }); - } - - protected onSubmit(): void { - alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); - } - - protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { - // In a real-world application the search service might return an Observable - // created by calling HttpClient.get(). Assigning that Observable to the result - // allows the lookup component to cancel the web request if it does not complete - // before the user searches again. - args.result = this.#svc.search(args.searchText).pipe( - map((result) => ({ - hasMore: result.hasMore, - items: result.people, - totalCount: result.totalCount, - })), - ); - } - - protected addClick(args: SkyLookupAddClickEventArgs): void { - const person: Person = { - name: 'Newman', - }; - - this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { - args.itemAdded({ - item: person, - }); - }); - } -} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/async/person.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/async/person.ts deleted file mode 100644 index 3763f53ace..0000000000 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/async/person.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Person { - name: string; -} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.html b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.html index 1335134c00..b31a0a9ae9 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.html +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.html @@ -13,12 +13,11 @@ >
{ + let mockSvc!: jasmine.SpyObj; + async function setupTest(): Promise<{ lookupHarness: SkyLookupHarness; fixture: ComponentFixture; @@ -25,8 +114,18 @@ describe('Lookup custom picker demo', () => { } beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + TestBed.configureTestingModule({ imports: [DemoComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], }); }); @@ -41,10 +140,23 @@ describe('Lookup custom picker demo', () => { it('should update the form control when a favorite name is selected', async () => { const { lookupHarness, fixture } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.enterText('Be'); const allResultHarnesses = await lookupHarness.getSearchResults(); - const firstResultHarness = allResultHarnesses && allResultHarnesses[0]; + const firstResultHarness = allResultHarnesses[0]; if (firstResultHarness) { await firstResultHarness.select(); @@ -61,6 +173,14 @@ describe('Lookup custom picker demo', () => { it('should use a custom picker', async () => { const { lookupHarness, fixture } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: people, + totalCount: 20, + }), + ); + // Show the custom picker. await lookupHarness.clickShowMoreButton(); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.ts index da8b9378fc..e3ee7018c4 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.ts @@ -8,14 +8,19 @@ import { ReactiveFormsModule, } from '@angular/forms'; import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; import { - SkyAutocompleteSearchFunctionFilter, + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, SkyLookupModule, SkyLookupShowMoreConfig, SkyLookupShowMoreCustomPickerContext, } from '@skyux/lookup'; import { SkyModalService } from '@skyux/modals'; +import { map } from 'rxjs/operators'; + +import { DemoService } from './demo.service'; import { Person } from './person'; import { PickerModalComponent } from './picker-modal.component'; @@ -37,107 +42,23 @@ export class DemoComponent implements OnInit { }>; protected showMoreConfig: SkyLookupShowMoreConfig; - protected searchFilters: SkyAutocompleteSearchFunctionFilter[]; - - protected people: Person[] = [ - { - name: 'Abed', - formal: 'Mr. Nadir', - }, - { - name: 'Alex', - formal: 'Mr. Osbourne', - }, - { - name: 'Ben', - formal: 'Mr. Chang', - }, - { - name: 'Britta', - formal: 'Ms. Perry', - }, - { - name: 'Buzz', - formal: 'Mr. Hickey', - }, - { - name: 'Craig', - formal: 'Mr. Pelton', - }, - { - name: 'Elroy', - formal: 'Mr. Patashnik', - }, - { - name: 'Garrett', - formal: 'Mr. Lambert', - }, - { - name: 'Ian', - formal: 'Mr. Duncan', - }, - { - name: 'Jeff', - formal: 'Mr. Winger', - }, - { - name: 'Leonard', - formal: 'Mr. Rodriguez', - }, - { - name: 'Neil', - formal: 'Mr. Neil', - }, - { - name: 'Pierce', - formal: 'Mr. Hawthorne', - }, - { - name: 'Preston', - formal: 'Mr. Koogler', - }, - { - name: 'Rachel', - formal: 'Ms. Rachel', - }, - { - name: 'Shirley', - formal: 'Ms. Bennett', - }, - { - name: 'Todd', - formal: 'Mr. Jacobson', - }, - { - name: 'Troy', - formal: 'Mr. Barnes', - }, - { - name: 'Vaughn', - formal: 'Mr. Miller', - }, - { - name: 'Vicki', - formal: 'Ms. Jenkins', - }, - ]; readonly #modalSvc = inject(SkyModalService); + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); constructor() { + const names = new FormControl([ + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + ]); + this.favoritesForm = inject(FormBuilder).group({ - favoriteNames: [[this.people[15]]], + favoriteNames: names, }); - this.searchFilters = [ - (_, item): boolean => { - const names = this.favoritesForm.value.favoriteNames; - - // Only show people in the search results that have not been chosen already. - return !names?.some((option) => option.name === item.name); - }, - ]; - this.showMoreConfig = { customPicker: { open: (context): void => { @@ -154,7 +75,7 @@ export class DemoComponent implements OnInit { instance.closed.subscribe((closeArgs) => { if (closeArgs.reason === 'save') { this.favoritesForm.controls.favoriteNames.setValue( - closeArgs.data, + closeArgs.data as Person[], ); } }); @@ -171,8 +92,31 @@ export class DemoComponent implements OnInit { }); } - protected onAddButtonClicked(): void { - alert('Add button clicked!'); + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + formal: 'Mr. Parker', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); } protected onSubmit(): void { diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.service.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.service.ts new file mode 100644 index 0000000000..e26d31eb38 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.html b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.html index 02b66f7e46..b8924f54ef 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.html +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.html @@ -1,26 +1,32 @@ Names -
- - - - - {{ people[i].name }} - - - {{ people[i].formal }} - - - - -
+ @if (peopleForm) { +
+ + @for ( + personControl of peopleForm.controls.people.controls; + track personControl; + let i = $index + ) { + + + + {{ people[i].name }} + + + {{ people[i].formal }} + + + + } + +
+ } @else { +
+ +
+ }
- + {{ item.name }}
@@ -40,7 +40,7 @@
- + {{ item.name }}
diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.spec.ts index 38ec0724ec..52766d3f18 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.spec.ts @@ -4,10 +4,15 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { SkyInputBoxHarness } from '@skyux/forms/testing'; import { SkyLookupHarness } from '@skyux/lookup/testing'; +import { of } from 'rxjs'; + import { DemoComponent } from './demo.component'; +import { DemoService } from './demo.service'; import { ItemHarness } from './item-harness'; describe('Lookup result templates demo', () => { + let mockSvc!: jasmine.SpyObj; + async function setupTest(): Promise<{ lookupHarness: SkyLookupHarness; fixture: ComponentFixture; @@ -25,8 +30,18 @@ describe('Lookup result templates demo', () => { } beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + TestBed.configureTestingModule({ imports: [DemoComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], }); }); @@ -41,6 +56,19 @@ describe('Lookup result templates demo', () => { it('should use the expected dropdown item template', async () => { const { lookupHarness } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.enterText('be'); const results = await lookupHarness.getSearchResults(); @@ -56,6 +84,19 @@ describe('Lookup result templates demo', () => { it('should use the expected modal item template', async () => { const { lookupHarness } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.clickShowMoreButton(); const pickerHarness = await lookupHarness.getShowMorePicker(); @@ -74,10 +115,23 @@ describe('Lookup result templates demo', () => { it('should update the form control when a favorite name is selected', async () => { const { lookupHarness, fixture } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.enterText('be'); const allResultHarnesses = await lookupHarness.getSearchResults(); - const firstResultHarness = allResultHarnesses && allResultHarnesses[0]; + const firstResultHarness = allResultHarnesses[0]; await firstResultHarness.select(); expect( @@ -91,6 +145,19 @@ describe('Lookup result templates demo', () => { it('should respect the selection descriptor', async () => { const { lookupHarness } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.ts index 716229fe6e..a02cf57a81 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.component.ts @@ -14,12 +14,17 @@ import { ReactiveFormsModule, } from '@angular/forms'; import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; import { - SkyAutocompleteSearchFunctionFilter, + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, SkyLookupModule, SkyLookupShowMoreConfig, } from '@skyux/lookup'; +import { map } from 'rxjs/operators'; + +import { DemoService } from './demo.service'; import { Person } from './person'; @Component({ @@ -39,91 +44,6 @@ export class DemoComponent implements OnInit { favoriteNames: FormControl; }>; - protected searchFilters: SkyAutocompleteSearchFunctionFilter[]; - - protected people: Person[] = [ - { - name: 'Abed', - formal: 'Mr. Nadir', - }, - { - name: 'Alex', - formal: 'Mr. Osbourne', - }, - { - name: 'Ben', - formal: 'Mr. Chang', - }, - { - name: 'Britta', - formal: 'Ms. Perry', - }, - { - name: 'Buzz', - formal: 'Mr. Hickey', - }, - { - name: 'Craig', - formal: 'Mr. Pelton', - }, - { - name: 'Elroy', - formal: 'Mr. Patashnik', - }, - { - name: 'Garrett', - formal: 'Mr. Lambert', - }, - { - name: 'Ian', - formal: 'Mr. Duncan', - }, - { - name: 'Jeff', - formal: 'Mr. Winger', - }, - { - name: 'Leonard', - formal: 'Mr. Rodriguez', - }, - { - name: 'Neil', - formal: 'Mr. Neil', - }, - { - name: 'Pierce', - formal: 'Mr. Hawthorne', - }, - { - name: 'Preston', - formal: 'Mr. Koogler', - }, - { - name: 'Rachel', - formal: 'Ms. Rachel', - }, - { - name: 'Shirley', - formal: 'Ms. Bennett', - }, - { - name: 'Todd', - formal: 'Mr. Jacobson', - }, - { - name: 'Troy', - formal: 'Mr. Barnes', - }, - { - name: 'Vaughn', - formal: 'Mr. Miller', - }, - { - name: 'Vicki', - formal: 'Ms. Jenkins', - }, - ]; - protected showMoreConfig: SkyLookupShowMoreConfig = { nativePickerConfig: { selectionDescriptor: 'names', @@ -139,19 +59,20 @@ export class DemoComponent implements OnInit { } } + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); + constructor() { + const names = new FormControl([ + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + ]); + this.favoritesForm = inject(FormBuilder).group({ - favoriteNames: [[this.people[15]]], + favoriteNames: names, }); - - this.searchFilters = [ - (_, item): boolean => { - const names = this.favoritesForm.value.favoriteNames; - - // Only show people in the search results that have not been chosen already. - return !names?.some((option) => option.name === item.name); - }, - ]; } public ngOnInit(): void { @@ -162,8 +83,31 @@ export class DemoComponent implements OnInit { }); } - protected onAddButtonClicked(): void { - alert('Add button clicked!'); + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + formal: 'Mr. Parker', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); } protected onSubmit(): void { diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.service.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.service.ts new file mode 100644 index 0000000000..e26d31eb38 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/search-results.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.html b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.html index fc53631b7f..cb2be33728 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.html +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.html @@ -1,11 +1,11 @@
+ @if (favoritesForm.controls.favoriteName.errors?.['letterE']) { + + }
{ + let mockSvc!: jasmine.SpyObj; -describe('Lookup single select demo', () => { async function setupTest(): Promise<{ lookupHarness: SkyLookupHarness; fixture: ComponentFixture; @@ -16,7 +21,7 @@ describe('Lookup single select demo', () => { const lookupHarness = await ( await loader.getHarness( - SkyInputBoxHarness.with({ dataSkyId: 'favorite-name-field' }), + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), ) ).queryHarness(SkyLookupHarness); @@ -24,8 +29,18 @@ describe('Lookup single select demo', () => { } beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + TestBed.configureTestingModule({ imports: [DemoComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], }); }); @@ -38,28 +53,56 @@ describe('Lookup single select demo', () => { it('should update the form control when a favorite name is selected', async () => { const { lookupHarness, fixture } = await setupTest(); - await lookupHarness.enterText('be'); + mockSvc.search.and.callFake((searchText) => + of({ + hasMore: false, + people: + searchText === 'b' + ? [ + { + name: 'Bernard', + }, + ] + : [], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('b'); await lookupHarness.selectSearchResult({ - text: 'Ben', + text: 'Bernard', }); expect(fixture.componentInstance.favoritesForm.value.favoriteName).toEqual([ - { name: 'Ben' }, + { name: 'Bernard' }, ]); }); it('should respect the selection descriptor', async () => { const { lookupHarness } = await setupTest(); + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + id: '21', + name: 'Bernard', + }, + ], + totalCount: 1, + }), + ); + await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( - 'Search name', + 'Search names', ); await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( - 'Select name', + 'Select names', ); }); }); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.ts index 8509b8be82..45ae15fcbd 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.component.ts @@ -1,19 +1,26 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { + AbstractControl, FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, + ValidationErrors, } from '@angular/forms'; -import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyFormErrorModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; import { - SkyAutocompleteSearchFunctionFilter, + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, SkyLookupModule, SkyLookupShowMoreConfig, } from '@skyux/lookup'; +import { map } from 'rxjs/operators'; + +import { DemoService } from './demo.service'; import { Person } from './person'; @Component({ @@ -24,6 +31,7 @@ import { Person } from './person'; CommonModule, FormsModule, ReactiveFormsModule, + SkyFormErrorModule, SkyInputBoxModule, SkyLookupModule, ], @@ -35,50 +43,31 @@ export class DemoComponent implements OnInit { public showMoreConfig: SkyLookupShowMoreConfig = { nativePickerConfig: { - selectionDescriptor: 'name', + selectionDescriptor: 'names', }, }; - protected searchFilters: SkyAutocompleteSearchFunctionFilter[]; + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); - protected people: Person[] = [ - { name: 'Abed' }, - { name: 'Alex' }, - { name: 'Ben' }, - { name: 'Britta' }, - { name: 'Buzz' }, - { name: 'Craig' }, - { name: 'Elroy' }, - { name: 'Garrett' }, - { name: 'Ian' }, - { name: 'Jeff' }, - { name: 'Leonard' }, - { name: 'Neil' }, - { name: 'Pierce' }, - { name: 'Preston' }, - { name: 'Rachel' }, - { name: 'Shirley' }, - { name: 'Todd' }, - { name: 'Troy' }, - { name: 'Vaughn' }, - { name: 'Vicki' }, - ]; + constructor() { + const name = new FormControl([{ name: 'Shirley' }], { + validators: [ + (control: AbstractControl): ValidationErrors => { + if ( + control.value?.some((person: Person) => !person.name.match(/e/i)) + ) { + return { letterE: true }; + } - protected name: Person[] = [this.people[15]]; + return {}; + }, + ], + }); - constructor() { this.favoritesForm = inject(FormBuilder).group({ - favoriteName: [[this.people[15]]], + favoriteName: name, }); - - this.searchFilters = [ - (_, item): boolean => { - const names = this.favoritesForm.value.favoriteName; - - // Only show people in the search results that have not been chosen already. - return !names?.some((option) => option.name === item.name); - }, - ]; } public ngOnInit(): void { @@ -89,11 +78,33 @@ export class DemoComponent implements OnInit { }); } - protected onAddButtonClicked(): void { - alert('Add button clicked!'); - } - protected onSubmit(): void { alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); } + + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); + } } diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.service.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.service.ts new file mode 100644 index 0000000000..e4e5bd0427 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { name: 'Abed' }, + { name: 'Alex' }, + { name: 'Ben' }, + { name: 'Britta' }, + { name: 'Buzz' }, + { name: 'Craig' }, + { name: 'Elroy' }, + { name: 'Garrett' }, + { name: 'Ian' }, + { name: 'Jeff' }, + { name: 'Leonard' }, + { name: 'Neil' }, + { name: 'Pierce' }, + { name: 'Preston' }, + { name: 'Rachel' }, + { name: 'Shirley' }, + { name: 'Todd' }, + { name: 'Troy' }, + { name: 'Vaughn' }, + { name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/search-results.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/single-select/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/apps/code-examples/src/app/code-examples/lookup/search/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/search/basic/demo.component.ts index e9e049d4ec..9b55f85c7b 100644 --- a/apps/code-examples/src/app/code-examples/lookup/search/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/search/basic/demo.component.ts @@ -50,7 +50,7 @@ export class DemoComponent { this.searchText = searchText; if (searchText) { - filteredItems = this.items.filter(function (item: Item) { + filteredItems = this.items.filter((item: Item) => { let property: keyof typeof item; for (property in item) { @@ -58,7 +58,7 @@ export class DemoComponent { Object.prototype.hasOwnProperty.call(item, property) && (property === 'title' || property === 'note') ) { - if (item[property].indexOf(searchText) > -1) { + if (item[property].includes(searchText)) { return true; } } diff --git a/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.spec.ts index aabdb9599b..7adf078d58 100644 --- a/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.spec.ts @@ -13,26 +13,29 @@ describe('Selection modal demo', () => { async function setupTest(): Promise<{ harness: SkySelectionModalHarness; + el: HTMLElement; fixture: ComponentFixture; }> { const fixture = TestBed.createComponent(DemoComponent); - const openBtn = fixture.nativeElement.querySelector( + const el = fixture.nativeElement as HTMLElement; + + const openBtn = el.querySelector( '.selection-modal-demo-show-btn', ); - openBtn.click(); + openBtn?.click(); fixture.detectChanges(); const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); const harness = await rootLoader.getHarness(SkySelectionModalHarness); - return { harness, fixture }; + return { harness, el, fixture }; } beforeEach(() => { // Create a mock search service. In a real-world application, the search // service would make a web request which should be avoided in unit tests. - mockSvc = jasmine.createSpyObj('DemoService', ['search']); + mockSvc = jasmine.createSpyObj('DemoService', ['search']); mockSvc.search.and.callFake((searchText) => { return of({ @@ -56,7 +59,7 @@ describe('Selection modal demo', () => { }); it('should update the selected items list when an item is selected', async () => { - const { harness, fixture } = await setupTest(); + const { harness, el } = await setupTest(); await harness.enterSearchText('ra'); await harness.selectSearchResult({ @@ -64,7 +67,7 @@ describe('Selection modal demo', () => { }); await harness.saveAndClose(); - const selectedItemEls = fixture.nativeElement.querySelectorAll( + const selectedItemEls = el.querySelectorAll( '.selection-modal-demo-selected li', ); @@ -73,7 +76,7 @@ describe('Selection modal demo', () => { }); it('should not update the selected items list when the user cancels the selection modal', async () => { - const { harness, fixture } = await setupTest(); + const { harness, el } = await setupTest(); await harness.enterSearchText('ra'); await harness.selectSearchResult({ @@ -81,7 +84,7 @@ describe('Selection modal demo', () => { }); await harness.cancel(); - const selectedItemEls = fixture.nativeElement.querySelectorAll( + const selectedItemEls = el.querySelectorAll( '.selection-modal-demo-selected li', ); diff --git a/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.ts index eb55ed58b2..043441474d 100644 --- a/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/selection-modal/add-item/demo.component.ts @@ -6,7 +6,7 @@ import { SkySelectionModalSearchResult, SkySelectionModalService, } from '@skyux/lookup'; -import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; +import { SkyModalService } from '@skyux/modals'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -55,10 +55,11 @@ export class DemoComponent implements OnDestroy { const modal = this.#modalSvc.open(AddItemModalComponent); this.#subscriptions.add( - modal.closed.subscribe((close: SkyModalCloseArgs) => { + modal.closed.subscribe((close) => { if (close.reason === 'save') { - this.#searchSvc.addItem(close.data); - args.itemAdded({ item: close.data }); + const person = close.data as Person; + this.#searchSvc.addItem(person); + args.itemAdded({ item: person }); } }), ); diff --git a/apps/code-examples/src/app/code-examples/lookup/selection-modal/basic/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lookup/selection-modal/basic/demo.component.spec.ts index aabdb9599b..028eaacd4d 100644 --- a/apps/code-examples/src/app/code-examples/lookup/selection-modal/basic/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lookup/selection-modal/basic/demo.component.spec.ts @@ -13,26 +13,28 @@ describe('Selection modal demo', () => { async function setupTest(): Promise<{ harness: SkySelectionModalHarness; + el: HTMLElement; fixture: ComponentFixture; }> { const fixture = TestBed.createComponent(DemoComponent); - const openBtn = fixture.nativeElement.querySelector( + const el = fixture.nativeElement as HTMLElement; + const openBtn = el.querySelector( '.selection-modal-demo-show-btn', ); - openBtn.click(); + openBtn?.click(); fixture.detectChanges(); const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); const harness = await rootLoader.getHarness(SkySelectionModalHarness); - return { harness, fixture }; + return { harness, el, fixture }; } beforeEach(() => { // Create a mock search service. In a real-world application, the search // service would make a web request which should be avoided in unit tests. - mockSvc = jasmine.createSpyObj('DemoService', ['search']); + mockSvc = jasmine.createSpyObj('DemoService', ['search']); mockSvc.search.and.callFake((searchText) => { return of({ @@ -56,7 +58,7 @@ describe('Selection modal demo', () => { }); it('should update the selected items list when an item is selected', async () => { - const { harness, fixture } = await setupTest(); + const { harness, el } = await setupTest(); await harness.enterSearchText('ra'); await harness.selectSearchResult({ @@ -64,7 +66,7 @@ describe('Selection modal demo', () => { }); await harness.saveAndClose(); - const selectedItemEls = fixture.nativeElement.querySelectorAll( + const selectedItemEls = el.querySelectorAll( '.selection-modal-demo-selected li', ); @@ -73,7 +75,7 @@ describe('Selection modal demo', () => { }); it('should not update the selected items list when the user cancels the selection modal', async () => { - const { harness, fixture } = await setupTest(); + const { harness, el } = await setupTest(); await harness.enterSearchText('ra'); await harness.selectSearchResult({ @@ -81,7 +83,7 @@ describe('Selection modal demo', () => { }); await harness.cancel(); - const selectedItemEls = fixture.nativeElement.querySelectorAll( + const selectedItemEls = el.querySelectorAll( '.selection-modal-demo-selected li', ); diff --git a/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-controller/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-controller/demo.component.spec.ts index 03076fd7e8..db8e5aec46 100644 --- a/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-controller/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-controller/demo.component.spec.ts @@ -7,10 +7,10 @@ import { import { DemoComponent } from './demo.component'; describe('Testing with SkyConfirmTestingController', () => { - async function setupTest(): Promise<{ + function setupTest(): { confirmController: SkyConfirmTestingController; fixture: ComponentFixture; - }> { + } { const confirmController = TestBed.inject(SkyConfirmTestingController); const fixture = TestBed.createComponent(DemoComponent); @@ -23,8 +23,8 @@ describe('Testing with SkyConfirmTestingController', () => { }); }); - it('should click "OK" on a confirmation dialog', async () => { - const { confirmController, fixture } = await setupTest(); + it('should click "OK" on a confirmation dialog', () => { + const { confirmController, fixture } = setupTest(); fixture.componentInstance.launchConfirm(); fixture.detectChanges(); @@ -39,8 +39,8 @@ describe('Testing with SkyConfirmTestingController', () => { expect(fixture.componentInstance.selectedAction).toEqual('ok'); }); - it('should cancel the confirmation dialog', async () => { - const { confirmController, fixture } = await setupTest(); + it('should cancel the confirmation dialog', () => { + const { confirmController, fixture } = setupTest(); fixture.componentInstance.launchConfirm(); fixture.detectChanges(); diff --git a/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-harness/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-harness/demo.component.spec.ts index 4ed2baab77..6bd8c07a54 100644 --- a/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-harness/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/modals/confirm/basic-with-harness/demo.component.spec.ts @@ -10,9 +10,10 @@ describe('Testing with SkyConfirmHarness', () => { fixture: ComponentFixture; }> { const fixture = TestBed.createComponent(DemoComponent); - const openBtn = fixture.nativeElement.querySelector(confirmBtnClass); + const el = fixture.nativeElement as HTMLElement; + const openBtn = el.querySelector(confirmBtnClass); - openBtn.click(); + openBtn?.click(); const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); const confirmHarness = await rootLoader.getHarness(SkyConfirmHarness); @@ -25,7 +26,9 @@ describe('Testing with SkyConfirmHarness', () => { expectedText: string, ): void { expect( - fixture.nativeElement.querySelector('.displayed-text')?.innerText, + (fixture.nativeElement as HTMLElement).querySelector( + '.displayed-text', + )?.innerText, ).toEqual(expectedText); } diff --git a/apps/code-examples/src/app/code-examples/modals/modal/basic-with-controller/demo.component.ts b/apps/code-examples/src/app/code-examples/modals/modal/basic-with-controller/demo.component.ts index 0423564b6c..7fbbba03dd 100644 --- a/apps/code-examples/src/app/code-examples/modals/modal/basic-with-controller/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/modals/modal/basic-with-controller/demo.component.ts @@ -31,7 +31,9 @@ export class DemoComponent implements OnDestroy { readonly #modalSvc = inject(SkyModalService); public ngOnDestroy(): void { - this.#instances.forEach((i) => i.close()); + this.#instances.forEach((i) => { + i.close(); + }); } public openModal(): void { diff --git a/apps/code-examples/src/app/code-examples/modals/modal/basic-with-harness/demo.component.ts b/apps/code-examples/src/app/code-examples/modals/modal/basic-with-harness/demo.component.ts index bd0a29f0a7..35e62ed1c2 100644 --- a/apps/code-examples/src/app/code-examples/modals/modal/basic-with-harness/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/modals/modal/basic-with-harness/demo.component.ts @@ -54,8 +54,7 @@ export class DemoComponent implements OnDestroy { instance.closed.subscribe((result) => { if (result.reason === 'save') { // Display the updated value. - const data = result.data as ModalDemoData; - this.demoValue = data.value1; + this.demoValue = (result.data as ModalDemoData).value1; } }); }); diff --git a/apps/code-examples/src/app/code-examples/modals/modal/with-error/demo.component.ts b/apps/code-examples/src/app/code-examples/modals/modal/with-error/demo.component.ts index bd0a29f0a7..35e62ed1c2 100644 --- a/apps/code-examples/src/app/code-examples/modals/modal/with-error/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/modals/modal/with-error/demo.component.ts @@ -54,8 +54,7 @@ export class DemoComponent implements OnDestroy { instance.closed.subscribe((result) => { if (result.reason === 'save') { // Display the updated value. - const data = result.data as ModalDemoData; - this.demoValue = data.value1; + this.demoValue = (result.data as ModalDemoData).value1; } }); }); diff --git a/apps/code-examples/src/app/code-examples/modals/modal/with-error/modal.component.ts b/apps/code-examples/src/app/code-examples/modals/modal/with-error/modal.component.ts index 903751a980..2bb207bf53 100644 --- a/apps/code-examples/src/app/code-examples/modals/modal/with-error/modal.component.ts +++ b/apps/code-examples/src/app/code-examples/modals/modal/with-error/modal.component.ts @@ -47,7 +47,7 @@ export class ModalComponent { this.#waitSvc .blockingWrap(this.#dataSvc.save(this.demoForm.value, true)) - .subscribe((data) => { + .subscribe(() => { this.errors = [{ message: 'There was an error saving the form.' }]; }); } diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts index b4027838b4..638784dd9b 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { Item } from './item'; + @Component({ standalone: true, selector: 'app-dashboards-grid-context-menu', @@ -15,8 +17,8 @@ export class DashboardGridContextMenuComponent { protected dashboardName = ''; - public agInit(params: ICellRendererParams): void { - this.dashboardName = params.data && params.data.dashboard; + public agInit(params: ICellRendererParams): void { + this.dashboardName = params.data?.dashboard ?? ''; } public refresh(): boolean { diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts index b4f1823f49..09a41eb22b 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts @@ -5,7 +5,7 @@ import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; -describe('List page list layout demo', async () => { +describe('List page list layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/item.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/item.ts new file mode 100644 index 0000000000..fdbea72423 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/item.ts @@ -0,0 +1,5 @@ +export interface Item { + dashboard: string; + name: string; + lastUpdated: string; +} diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/list-page-content.component.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/list-page-content.component.ts index b9c0c42fe1..69b71d2165 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/list-page-content.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/list-page-content.component.ts @@ -12,6 +12,7 @@ import { AgGridModule } from 'ag-grid-angular'; import { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'; import { DashboardGridContextMenuComponent } from './dashboards-grid-context-menu.component'; +import { Item } from './item'; @Component({ standalone: true, @@ -27,7 +28,7 @@ import { DashboardGridContextMenuComponent } from './dashboards-grid-context-men ], }) export class ListPageContentComponent implements OnInit { - protected items = [ + protected items: Item[] = [ { dashboard: 'Cash Flow Tracker', name: 'Kanesha Hutto', diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts index 5b60e65779..dc496c7534 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { Contact } from './contact'; + @Component({ standalone: true, selector: 'app-contacts-grid-context-menu', @@ -13,8 +15,8 @@ import { ICellRendererParams } from 'ag-grid-community'; export class ContactContextMenuComponent implements ICellRendererAngularComp { protected contactName = ''; - public agInit(params: ICellRendererParams): void { - this.contactName = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.contactName = params.data?.name ?? ''; } public refresh(): boolean { diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact.ts new file mode 100644 index 0000000000..9c2dc362e4 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact.ts @@ -0,0 +1,5 @@ +export interface Contact { + name: string; + organization: string; + emailAddress: string; +} diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts index c3a058f9db..ceaa921f10 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts @@ -6,7 +6,7 @@ import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; -describe('List page tabs layout demo', async () => { +describe('List page tabs layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts index 6196c864e0..88df6191c7 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { SkyTabIndex, SkyTabsModule } from '@skyux/tabs'; +import { Contact } from './contact'; import { ListPageContactsGridComponent } from './list-page-contacts-grid.component'; @Component({ @@ -13,7 +14,7 @@ import { ListPageContactsGridComponent } from './list-page-contacts-grid.compone export class ListPageContentComponent { protected activeTabIndex: SkyTabIndex = 0; - protected myContacts = [ + protected myContacts: Contact[] = [ { name: 'Wonda Lumpkin', organization: 'Riverfront College of the Arts', diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.spec.ts index ae5e22d726..5961ebf8c5 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.spec.ts @@ -6,7 +6,7 @@ import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; -describe('Record page blocks layout demo', async () => { +describe('Record page blocks layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachment.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachment.ts new file mode 100644 index 0000000000..746fdbb3c4 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachment.ts @@ -0,0 +1,6 @@ +export interface Attachment { + name: string; + description: string; + size: string; + dateAdded: string; +} diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts index 94d173294b..66173da05d 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts @@ -4,6 +4,8 @@ import { SkyDropdownModule } from '@skyux/popovers'; import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; +import { Attachment } from './attachment'; + @Component({ standalone: true, selector: 'app-attachments-grid-context-menu', @@ -15,8 +17,8 @@ export class AttachmentsGridContextMenuComponent { protected attachmentName = ''; - public agInit(params: ICellRendererParams): void { - this.attachmentName = params.data && params.data.name; + public agInit(params: ICellRendererParams): void { + this.attachmentName = params.data?.name ?? ''; } public refresh(): boolean { diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts index 21f5a3240e..709a51bf58 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts @@ -6,7 +6,7 @@ import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; -describe('Record page tabs layout demo', async () => { +describe('Record page tabs layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/detail.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/detail.ts new file mode 100644 index 0000000000..158a7675bd --- /dev/null +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/detail.ts @@ -0,0 +1,4 @@ +export interface Detail { + detail: string; + info: string; +} diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts index 66e165c989..d25b5a8ded 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts @@ -12,6 +12,7 @@ import { SkyIconModule, SkyKeyInfoModule } from '@skyux/indicators'; import { AgGridModule } from 'ag-grid-angular'; import { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'; +import { Attachment } from './attachment'; import { AttachmentsGridContextMenuComponent } from './attachments-grid-context-menu.component'; @Component({ @@ -29,7 +30,7 @@ import { AttachmentsGridContextMenuComponent } from './attachments-grid-context- ], }) export class RecordPageAttachmentsTabComponent implements OnInit { - protected items = [ + protected items: Attachment[] = [ { name: 'Agreement.pdf', description: 'Cardholder agreement', diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts index a6b272e97e..4a2e01e324 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts @@ -8,6 +8,8 @@ import { } from '@skyux/layout'; import { SkyRepeaterModule } from '@skyux/lists'; +import { Detail } from './detail'; + @Component({ standalone: true, selector: 'app-record-page-overview-tab', @@ -24,7 +26,7 @@ import { SkyRepeaterModule } from '@skyux/lists'; ], }) export class RecordPageOverviewTabComponent { - protected recordDetails = [ + protected recordDetails: Detail[] = [ { detail: 'Designation', info: 'General operating', diff --git a/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.spec.ts index b0174483be..69201f03ba 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.spec.ts @@ -6,7 +6,7 @@ import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; -describe('Split view page fit layout demo', async () => { +describe('Split view page fit layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; diff --git a/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts b/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts index 10a29a35fc..700362d7a8 100644 --- a/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts +++ b/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts @@ -14,49 +14,51 @@ describe('CustomSkyHrefResolverService', () => { expect(service).toBeTruthy(); }); - it('should return a link as-is', () => { + it('should return a link as-is', async () => { const url = 'https://www.blackbaud.com'; - const result = service.resolveHref({ url }); - result.then((href) => { - expect(href.url).toEqual(url); - expect(href.userHasAccess).toEqual(true); - }); + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(true); }); - it('should return a link with allow protocol', () => { + it('should return a link with allow protocol', async () => { const url = 'allow://www.blackbaud.com'; - const result = service.resolveHref({ url }); - result.then((href) => { - expect(href.url).toEqual('https://www.blackbaud.com'); - expect(href.userHasAccess).toEqual(true); - }); + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual('https://www.blackbaud.com'); + expect(href.userHasAccess).toEqual(true); }); - it('should return a link with deny protocol', () => { + it('should return a link with deny protocol', async () => { const url = 'deny://www.blackbaud.com'; - const result = service.resolveHref({ url }); - result.then((href) => { - expect(href.url).toEqual(url); - expect(href.userHasAccess).toEqual(false); - }); + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(false); }); it('should return a link with slow protocol', fakeAsync(() => { const url = 'slow://www.blackbaud.com'; const result = service.resolveHref({ url }); - result.then((href) => { - expect(href.url).toEqual('https://www.blackbaud.com'); - expect(href.userHasAccess).toEqual(true); - }); + + result + .then((href) => { + expect(href.url).toEqual('https://www.blackbaud.com'); + expect(href.userHasAccess).toEqual(true); + }) + .catch(() => { + fail('expected test to resolve'); + }); + tick(3000); })); - it('should return a link with unknown protocol', () => { + it('should return a link with unknown protocol', async () => { const url = 'unknown://www.blackbaud.com'; - const result = service.resolveHref({ url }); - result.then((href) => { - expect(href.url).toEqual(url); - expect(href.userHasAccess).toEqual(false); - }); + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(false); }); }); diff --git a/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts b/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts index ba07a6056c..7de7212048 100644 --- a/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts +++ b/apps/code-examples/src/app/code-examples/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts @@ -12,23 +12,30 @@ import { SkyHref, SkyHrefResolver } from '@skyux/router'; export class CustomSkyHrefResolverService implements SkyHrefResolver { public resolveHref(param: { url: string }): Promise { const url = param.url; + if (url.startsWith('http:') || url.startsWith('https:')) { - return Promise.resolve({ - url: url, + return Promise.resolve({ + url, userHasAccess: true, }); - } else if (url.startsWith('allow:')) { - return Promise.resolve({ + } + + if (url.startsWith('allow:')) { + return Promise.resolve({ url: url.replace('allow:', 'https:'), userHasAccess: true, }); - } else if (url.startsWith('deny:')) { - return Promise.resolve({ - url: url, + } + + if (url.startsWith('deny:')) { + return Promise.resolve({ + url, userHasAccess: false, }); - } else if (url.startsWith('slow:')) { - return new Promise((resolve) => { + } + + if (url.startsWith('slow:')) { + return new Promise((resolve) => { setTimeout(() => { resolve({ url: url.replace('slow:', 'https:'), @@ -36,16 +43,18 @@ export class CustomSkyHrefResolverService implements SkyHrefResolver { }); }, 3000); }); - } else if (url.startsWith('1bb-nav:')) { - return Promise.resolve({ + } + + if (url.startsWith('1bb-nav:')) { + return Promise.resolve({ url: `https://docs.blackbaud.com/engineering-system-docs/learn/spa/spa-navigation/spa-to-spa-navigation`, userHasAccess: true, }); - } else { - return Promise.resolve({ - url: url, - userHasAccess: false, - }); } + + return Promise.resolve({ + url, + userHasAccess: false, + }); } } diff --git a/apps/code-examples/src/app/code-examples/split-view/split-view/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/split-view/split-view/basic/demo.component.ts index 28d8170c28..4340830e04 100644 --- a/apps/code-examples/src/app/code-examples/split-view/split-view/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/split-view/split-view/basic/demo.component.ts @@ -21,6 +21,11 @@ import { Subject } from 'rxjs'; import { Record } from './record'; +interface DemoForm { + approvedAmount: FormControl; + comments: FormControl; +} + @Component({ standalone: true, selector: 'app-demo', @@ -87,7 +92,7 @@ export class DemoComponent { ]; protected activeRecord: Record; - protected splitViewDemoForm: FormGroup; + protected splitViewDemoForm: FormGroup; protected splitViewStream = new Subject(); #_activeIndex = 0; @@ -98,9 +103,14 @@ export class DemoComponent { // Start with the first item selected. this.activeIndex = 0; this.activeRecord = this.items[this.activeIndex]; + this.splitViewDemoForm = new FormGroup({ - approvedAmount: new FormControl(this.activeRecord.approvedAmount), - comments: new FormControl(this.activeRecord.comments), + approvedAmount: new FormControl(this.activeRecord.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(this.activeRecord.comments, { + nonNullable: true, + }), }); } @@ -124,8 +134,10 @@ export class DemoComponent { #loadFormGroup(record: Record): void { this.splitViewDemoForm = new FormGroup({ - approvedAmount: new FormControl(record.approvedAmount), - comments: new FormControl(record.comments), + approvedAmount: new FormControl(record.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(record.comments, { nonNullable: true }), }); } @@ -164,9 +176,9 @@ export class DemoComponent { #saveForm(): void { this.activeRecord.approvedAmount = - this.splitViewDemoForm.value.approvedAmount; + this.splitViewDemoForm.value.approvedAmount ?? 0; + this.activeRecord.comments = this.splitViewDemoForm.value.comments ?? ''; - this.activeRecord.comments = this.splitViewDemoForm.value.comments; this.splitViewDemoForm.reset(this.splitViewDemoForm.value); } diff --git a/apps/code-examples/src/app/code-examples/split-view/split-view/page-bound/demo.component.ts b/apps/code-examples/src/app/code-examples/split-view/split-view/page-bound/demo.component.ts index 13e69f47f3..2b1586a1d6 100644 --- a/apps/code-examples/src/app/code-examples/split-view/split-view/page-bound/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/split-view/split-view/page-bound/demo.component.ts @@ -26,6 +26,11 @@ import { Subject } from 'rxjs'; import { Record } from './record'; +interface DemoForm { + approvedAmount: FormControl; + comments: FormControl; +} + @Component({ standalone: true, selector: 'app-demo', @@ -141,7 +146,7 @@ export class DemoComponent { ]; protected activeRecord: Record; - protected splitViewDemoForm: FormGroup; + protected splitViewDemoForm: FormGroup; protected splitViewStream = new Subject(); protected unapprovedTransaction = true; @@ -155,8 +160,12 @@ export class DemoComponent { this.activeRecord = this.items[this.activeIndex]; this.splitViewDemoForm = new FormGroup({ - approvedAmount: new FormControl(this.activeRecord.approvedAmount), - comments: new FormControl(this.activeRecord.comments), + approvedAmount: new FormControl(this.activeRecord.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(this.activeRecord.comments, { + nonNullable: true, + }), }); } @@ -180,8 +189,10 @@ export class DemoComponent { #loadFormGroup(record: Record): void { this.splitViewDemoForm = new FormGroup({ - approvedAmount: new FormControl(record.approvedAmount), - comments: new FormControl(record.comments), + approvedAmount: new FormControl(record.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(record.comments, { nonNullable: true }), }); } @@ -220,9 +231,10 @@ export class DemoComponent { #saveForm(): void { this.activeRecord.approvedAmount = parseFloat( - this.splitViewDemoForm.value.approvedAmount, + `${this.splitViewDemoForm.value.approvedAmount ?? 0}`, ); - this.activeRecord.comments = this.splitViewDemoForm.value.comments; + + this.activeRecord.comments = this.splitViewDemoForm.value.comments ?? ''; this.unapprovedTransaction = this.items.findIndex((item) => item.amount !== item.approvedAmount) >= 0; diff --git a/apps/code-examples/src/app/code-examples/tabs/sectioned-form/modal/information-form.component.ts b/apps/code-examples/src/app/code-examples/tabs/sectioned-form/modal/information-form.component.ts index c35d38b205..65a302ed94 100644 --- a/apps/code-examples/src/app/code-examples/tabs/sectioned-form/modal/information-form.component.ts +++ b/apps/code-examples/src/app/code-examples/tabs/sectioned-form/modal/information-form.component.ts @@ -8,6 +8,7 @@ import { } from '@angular/core'; import { FormBuilder, + FormControl, FormGroup, ReactiveFormsModule, Validators, @@ -32,7 +33,11 @@ import { SkySectionedFormService } from '@skyux/tabs'; }) export class InformationFormComponent implements OnInit { protected id = '5324901'; - protected formGroup: FormGroup; + protected formGroup: FormGroup<{ + name: FormControl; + nameRequired: FormControl; + id: FormControl; + }>; protected name = ''; protected nameRequired = false; @@ -41,18 +46,20 @@ export class InformationFormComponent implements OnInit { constructor() { this.formGroup = inject(FormBuilder).group({ - name: [this.name], - nameRequired: [this.nameRequired], - id: [this.id, Validators.pattern('^[0-9]+$')], + name: new FormControl(this.name, { nonNullable: true }), + nameRequired: new FormControl(this.nameRequired, { nonNullable: true }), + id: new FormControl(this.id, { + nonNullable: true, + validators: [Validators.pattern('^[0-9]+$')], + }), }); } public ngOnInit(): void { this.formGroup.valueChanges.subscribe((changes) => { - console.log(changes); - this.id = changes.id; - this.name = changes.name; - this.nameRequired = changes.nameRequired; + this.id = changes.id ?? ''; + this.name = changes.name ?? ''; + this.nameRequired = !!changes.nameRequired; this.#checkValidity(); }); diff --git a/apps/code-examples/src/app/code-examples/text-editor/text-editor/demo.component.ts b/apps/code-examples/src/app/code-examples/text-editor/text-editor/demo.component.ts index 5294a37baa..73302a1100 100644 --- a/apps/code-examples/src/app/code-examples/text-editor/text-editor/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/text-editor/text-editor/demo.component.ts @@ -12,6 +12,12 @@ import { } from '@angular/forms'; import { SkyTextEditorModule } from '@skyux/text-editor'; +function validateText( + control: AbstractControl, +): ValidationErrors | null { + return !control.value?.includes('Blackbaud') ? { companyName: true } : null; +} + @Component({ standalone: true, selector: 'app-demo', @@ -32,18 +38,11 @@ export class DemoComponent { constructor() { this.myText = new FormControl(this.#richText, { nonNullable: true, - validators: [Validators.required, this.#validateText], + validators: [Validators.required, validateText], }); + this.formGroup = inject(FormBuilder).group({ myText: this.myText, }); } - - #validateText(control: AbstractControl): ValidationErrors | null { - if (!control.value || !control.value.includes('Blackbaud')) { - return { companyName: true }; - } else { - return null; - } - } } diff --git a/apps/code-examples/src/app/features/help-inline.module.ts b/apps/code-examples/src/app/features/help-inline.module.ts new file mode 100644 index 0000000000..a33b400467 --- /dev/null +++ b/apps/code-examples/src/app/features/help-inline.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'basic', + loadComponent: () => + import('../code-examples/help-inline/basic/demo.component').then( + (c) => c.DemoComponent, + ), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class HelpInlineFeatureModule {} diff --git a/apps/code-examples/src/app/features/icon.module.ts b/apps/code-examples/src/app/features/icon.module.ts new file mode 100644 index 0000000000..bb4af797a4 --- /dev/null +++ b/apps/code-examples/src/app/features/icon.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'basic', + loadComponent: () => + import('../code-examples/icon/basic/demo.component').then( + (c) => c.DemoComponent, + ), + }, + { + path: 'button', + loadComponent: () => + import('../code-examples/icon/icon-button/demo.component').then( + (c) => c.DemoComponent, + ), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class IconFeatureModule {} diff --git a/apps/code-examples/src/app/features/indicators.module.ts b/apps/code-examples/src/app/features/indicators.module.ts index 8f6620b948..4c19c28c14 100644 --- a/apps/code-examples/src/app/features/indicators.module.ts +++ b/apps/code-examples/src/app/features/indicators.module.ts @@ -9,27 +9,6 @@ const routes: Routes = [ (c) => c.DemoComponent, ), }, - { - path: 'help-inline/basic', - loadComponent: () => - import( - '../code-examples/indicators/help-inline/basic/demo.component' - ).then((c) => c.DemoComponent), - }, - { - path: 'icon/basic', - loadComponent: () => - import('../code-examples/indicators/icon/basic/demo.component').then( - (c) => c.DemoComponent, - ), - }, - { - path: 'icon/button', - loadComponent: () => - import( - '../code-examples/indicators/icon/icon-button/demo.component' - ).then((c) => c.DemoComponent), - }, { path: 'illustration/basic', loadComponent: () => diff --git a/apps/code-examples/src/app/features/layout.module.ts b/apps/code-examples/src/app/features/layout.module.ts index 038471417f..2f8f57066e 100644 --- a/apps/code-examples/src/app/features/layout.module.ts +++ b/apps/code-examples/src/app/features/layout.module.ts @@ -37,13 +37,6 @@ const routes: Routes = [ (c) => c.DemoComponent, ), }, - { - path: 'box/inline-help', - loadComponent: () => - import('../code-examples/layout/box/inline-help/demo.component').then( - (c) => c.DemoComponent, - ), - }, { path: 'card/basic', loadComponent: () => diff --git a/apps/code-examples/src/app/features/lookup.module.ts b/apps/code-examples/src/app/features/lookup.module.ts index fa91b99a34..d1b0ac16f9 100644 --- a/apps/code-examples/src/app/features/lookup.module.ts +++ b/apps/code-examples/src/app/features/lookup.module.ts @@ -44,13 +44,6 @@ const routes: Routes = [ (c) => c.DemoComponent, ), }, - { - path: 'lookup/async', - loadComponent: () => - import('../code-examples/lookup/lookup/async/demo.component').then( - (c) => c.DemoComponent, - ), - }, { path: 'lookup/custom-picker', loadComponent: () => diff --git a/apps/code-examples/src/app/home/home.component.html b/apps/code-examples/src/app/home/home.component.html index b6534dbcee..667a3d967f 100644 --- a/apps/code-examples/src/app/home/home.component.html +++ b/apps/code-examples/src/app/home/home.component.html @@ -221,6 +221,19 @@ +
  • + Help inline + +
  • +
  • + Icon + +
  • Indicators
      @@ -230,19 +243,6 @@
    • Basic
  • -
  • - Help inline - -
  • -
  • - Icon - -
  • Illustration
      @@ -332,7 +332,6 @@ Box
    • @@ -491,9 +490,8 @@ Lookup
      • - Async with add button + Add button
      • -
      • Async
      • Custom picker
      • diff --git a/apps/code-examples/src/main.ts b/apps/code-examples/src/main.ts index d9a2e7e4a5..cd9cbc4fad 100644 --- a/apps/code-examples/src/main.ts +++ b/apps/code-examples/src/main.ts @@ -10,4 +10,7 @@ if (environment.production) { platformBrowserDynamic() .bootstrapModule(AppModule) - .catch((err) => console.error(err)); + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + }); diff --git a/apps/e2e/angular-tree-component-storybook/project.json b/apps/e2e/angular-tree-component-storybook/project.json index 123f23131c..34b1acc444 100644 --- a/apps/e2e/angular-tree-component-storybook/project.json +++ b/apps/e2e/angular-tree-component-storybook/project.json @@ -33,7 +33,7 @@ { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "15kb" + "maximumError": "150kb" } ], "outputHashing": "all" diff --git a/apps/e2e/indicators-storybook-e2e/src/e2e/icon.component.cy.ts b/apps/e2e/indicators-storybook-e2e/src/e2e/icon.component.cy.ts index aab1755122..20649c08d2 100644 --- a/apps/e2e/indicators-storybook-e2e/src/e2e/icon.component.cy.ts +++ b/apps/e2e/indicators-storybook-e2e/src/e2e/icon.component.cy.ts @@ -10,6 +10,9 @@ describe('indicators-storybook', () => { ); it('should render the component', () => { cy.get('#ready') + .should('exist') + .end() + .get('#sky-icon-svg-sprite') .should('exist') .end() .get('app-icon') diff --git a/apps/e2e/indicators-storybook/src/app/icon/icon.component.html b/apps/e2e/indicators-storybook/src/app/icon/icon.component.html index bd8fa44caf..2e445bcab6 100644 --- a/apps/e2e/indicators-storybook/src/app/icon/icon.component.html +++ b/apps/e2e/indicators-storybook/src/app/icon/icon.component.html @@ -37,4 +37,94 @@ {{ fa }}
  • +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + Some text. +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    + + +
    diff --git a/apps/e2e/layout-storybook/src/app/box/box.component.html b/apps/e2e/layout-storybook/src/app/box/box.component.html index 57fb82997b..18124b6262 100644 --- a/apps/e2e/layout-storybook/src/app/box/box.component.html +++ b/apps/e2e/layout-storybook/src/app/box/box.component.html @@ -1,5 +1,10 @@ - +

    {{ boxType.name }}

    -
    +
    - + @if ("favoriteColor.errors?.['opaque']") { + + }
    - diff --git a/apps/playground/src/app/components/forms/field-group/field-group.component.html b/apps/playground/src/app/components/forms/field-group/field-group.component.html index e3cdf29292..8692474b6e 100644 --- a/apps/playground/src/app/components/forms/field-group/field-group.component.html +++ b/apps/playground/src/app/components/forms/field-group/field-group.component.html @@ -7,39 +7,47 @@ helpPopoverContent="content" helpKey="index.html" > - + + + - - Active - + + + +
    + +
    - + @for (view of views; track view) { + + } diff --git a/apps/playground/src/app/components/forms/field-group/field-group.component.ts b/apps/playground/src/app/components/forms/field-group/field-group.component.ts index a2d1508ccb..e5b538579d 100644 --- a/apps/playground/src/app/components/forms/field-group/field-group.component.ts +++ b/apps/playground/src/app/components/forms/field-group/field-group.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { FormBuilder, @@ -9,12 +9,16 @@ import { } from '@angular/forms'; import { SkyIdModule } from '@skyux/core'; import { + SkyCheckboxModule, SkyFieldGroupModule, SkyInputBoxModule, SkyRadioModule, SkyToggleSwitchModule, } from '@skyux/forms'; +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + interface Item { icon: string; label: string; @@ -26,9 +30,10 @@ interface Item { templateUrl: './field-group.component.html', standalone: true, imports: [ - CommonModule, + AsyncPipe, FormsModule, ReactiveFormsModule, + SkyCheckboxModule, SkyFieldGroupModule, SkyIdModule, SkyInputBoxModule, @@ -46,6 +51,7 @@ export class FieldGroupComponent { { icon: 'list', label: 'List', name: 'list' }, { icon: 'map-marker', label: 'Map', name: 'map' }, ]; + protected lazyName = of('Name').pipe(delay(2200)); constructor() { this.formGroup = this.#formBuilder.group({ diff --git a/apps/playground/src/app/components/forms/radio/radio.component.html b/apps/playground/src/app/components/forms/radio/radio.component.html index a96af32d60..4faaf85fc4 100644 --- a/apps/playground/src/app/components/forms/radio/radio.component.html +++ b/apps/playground/src/app/components/forms/radio/radio.component.html @@ -29,6 +29,13 @@ > Mark all fields as touched +

    Radio buttons (reactive)

    @@ -41,8 +48,7 @@

    Radio buttons (reactive)

    headingText="What is your favorite season? (reactive)" [required]="required" [stacked]="true" - headingLevel="3" - headingStyle="3" + [headingStyle]="headingStyle" >
    • diff --git a/apps/playground/src/app/components/forms/radio/radio.component.ts b/apps/playground/src/app/components/forms/radio/radio.component.ts index eff22e10c5..c76f479408 100644 --- a/apps/playground/src/app/components/forms/radio/radio.component.ts +++ b/apps/playground/src/app/components/forms/radio/radio.component.ts @@ -36,6 +36,8 @@ export class RadioComponent { public selectedValue = '3'; + public headingStyle: number | undefined; + constructor(formBuilder: UntypedFormBuilder) { this.favoriteSeason = new UntypedFormControl( { @@ -77,4 +79,13 @@ export class RadioComponent { model.control.markAsTouched(); model.control.updateValueAndValidity(); } + + public toggleHeadingStyle(): void { + const newStyle = (this.headingStyle ?? 2) + 1; + if (newStyle > 5) { + this.headingStyle = undefined; + } else { + this.headingStyle = newStyle; + } + } } diff --git a/apps/playground/src/app/components/help-inline/help-inline.component.html b/apps/playground/src/app/components/help-inline/help-inline.component.html index b6bb30b9d1..f69c8de6e4 100644 --- a/apps/playground/src/app/components/help-inline/help-inline.component.html +++ b/apps/playground/src/app/components/help-inline/help-inline.component.html @@ -3,18 +3,9 @@

      Giving - - This is a popover. -

      diff --git a/apps/playground/src/app/components/indicators/help-inline/help-inline.component.html b/apps/playground/src/app/components/indicators/help-inline/help-inline.component.html new file mode 100644 index 0000000000..b6bb30b9d1 --- /dev/null +++ b/apps/playground/src/app/components/indicators/help-inline/help-inline.component.html @@ -0,0 +1,20 @@ +
      +

      + Giving + + + + This is a popover. + +

      +
      diff --git a/apps/playground/src/app/components/indicators/help-inline/help-inline.component.ts b/apps/playground/src/app/components/indicators/help-inline/help-inline.component.ts new file mode 100644 index 0000000000..10b876605d --- /dev/null +++ b/apps/playground/src/app/components/indicators/help-inline/help-inline.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectorRef, Component, inject } from '@angular/core'; + +@Component({ + selector: 'app-help-inline', + templateUrl: './help-inline.component.html', +}) +export class HelpInlineComponent { + public popoverOpen = false; + + #changeDetector = inject(ChangeDetectorRef); + + public popoverChange(isOpen): void { + this.popoverOpen = isOpen; + this.#changeDetector.markForCheck(); + } +} diff --git a/apps/playground/src/app/components/indicators/help-inline/help-inline.module.ts b/apps/playground/src/app/components/indicators/help-inline/help-inline.module.ts new file mode 100644 index 0000000000..54fca169f0 --- /dev/null +++ b/apps/playground/src/app/components/indicators/help-inline/help-inline.module.ts @@ -0,0 +1,40 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SkyHelpInlineModule } from '@skyux/indicators'; +import { SkyPopoverModule } from '@skyux/popovers'; + +import { ComponentRouteInfo } from '../../../shared/component-info/component-route-info'; + +import { HelpInlineComponent } from './help-inline.component'; + +const routes: ComponentRouteInfo[] = [ + { + path: '', + component: HelpInlineComponent, + data: { + name: 'Legacy help inline', + icon: 'question', + library: 'indicators/help-inline', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class HelpInlineRoutingModule {} + +@NgModule({ + declarations: [HelpInlineComponent], + imports: [ + CommonModule, + HelpInlineRoutingModule, + SkyHelpInlineModule, + SkyPopoverModule, + ], +}) +export class HelpInlineModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/indicators/icon/icon-routing.module.ts b/apps/playground/src/app/components/indicators/icon/icon-routing.module.ts new file mode 100644 index 0000000000..6208fefc44 --- /dev/null +++ b/apps/playground/src/app/components/indicators/icon/icon-routing.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../../shared/component-info/component-route-info'; + +import { IconDemoComponent } from './icon.component'; + +const routes: ComponentRouteInfo[] = [ + { + path: '', + component: IconDemoComponent, + data: { + name: 'Icon', + icon: 'picture-o', + library: 'indicators', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IconRoutingModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/indicators/icon/icon.component.html b/apps/playground/src/app/components/indicators/icon/icon.component.html new file mode 100644 index 0000000000..445fe847cd --- /dev/null +++ b/apps/playground/src/app/components/indicators/icon/icon.component.html @@ -0,0 +1,40 @@ +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + Some text. +
      +
      + + + +
      diff --git a/apps/playground/src/app/components/indicators/icon/icon.component.ts b/apps/playground/src/app/components/indicators/icon/icon.component.ts new file mode 100644 index 0000000000..36783dff0e --- /dev/null +++ b/apps/playground/src/app/components/indicators/icon/icon.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + selector: 'app-icon', + standalone: true, + imports: [SkyIconModule], + templateUrl: './icon.component.html', + styles: [ + ` + :host { + --sky-icon-color: pink; + } + `, + ], +}) +export class IconDemoComponent {} diff --git a/apps/playground/src/app/components/indicators/icon/icon.module.ts b/apps/playground/src/app/components/indicators/icon/icon.module.ts new file mode 100644 index 0000000000..e02ad9bd99 --- /dev/null +++ b/apps/playground/src/app/components/indicators/icon/icon.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; + +import { IconRoutingModule } from './icon-routing.module'; +import { IconDemoComponent } from './icon.component'; + +@NgModule({ + imports: [CommonModule, IconDemoComponent, IconRoutingModule, SkyAlertModule], +}) +export class IconModule { + public static routes = IconRoutingModule.routes; +} diff --git a/apps/playground/src/app/components/indicators/indicators.module.ts b/apps/playground/src/app/components/indicators/indicators.module.ts index fabd63c5c6..22432cb26f 100644 --- a/apps/playground/src/app/components/indicators/indicators.module.ts +++ b/apps/playground/src/app/components/indicators/indicators.module.ts @@ -7,6 +7,17 @@ const routes: Routes = [ loadChildren: () => import('./alert/alert.module').then((m) => m.AlertModule), }, + { + path: 'help-inline-legacy', + loadChildren: () => + import('./help-inline/help-inline.module').then( + (m) => m.HelpInlineModule, + ), + }, + { + path: 'icon', + loadChildren: () => import('./icon/icon.module').then((m) => m.IconModule), + }, { path: 'illustration', loadChildren: () => diff --git a/apps/playground/src/app/components/layout/box/box.component.html b/apps/playground/src/app/components/layout/box/box.component.html index b0f077b593..7daa9bcc29 100644 --- a/apps/playground/src/app/components/layout/box/box.component.html +++ b/apps/playground/src/app/components/layout/box/box.component.html @@ -69,6 +69,39 @@

      Box 2 header

      + + + + + + + + + + + + + + + + + + + + + Box 3 content + + +
      @@ -77,4 +110,13 @@

      Box 2 header

      [disabled]="!showHeader" labelText="Show inline help" /> +
      + +Help content diff --git a/apps/playground/src/app/components/layout/box/box.component.ts b/apps/playground/src/app/components/layout/box/box.component.ts index aa3237f31a..fe1906a344 100644 --- a/apps/playground/src/app/components/layout/box/box.component.ts +++ b/apps/playground/src/app/components/layout/box/box.component.ts @@ -16,8 +16,18 @@ export class BoxComponent { public showControls = true; public showHeader = true; public showHelp = true; + public headingStyle: number | undefined; - public onHelpClick() { + public onHelpClick(): void { alert(`Help is available for this component.`); } + + public toggleHeadingStyle(): void { + const newStyle = (this.headingStyle ?? 1) + 1; + if (newStyle > 5) { + this.headingStyle = undefined; + } else { + this.headingStyle = newStyle; + } + } } diff --git a/apps/playground/src/app/components/layout/description-list/description-list.component.html b/apps/playground/src/app/components/layout/description-list/description-list.component.html index 9c217be932..e19850c41e 100644 --- a/apps/playground/src/app/components/layout/description-list/description-list.component.html +++ b/apps/playground/src/app/components/layout/description-list/description-list.component.html @@ -1,6 +1,9 @@
      - + {{ item.term }} - + {{ item.term }}
      +Help + diff --git a/apps/playground/src/app/components/layout/description-list/description-list.component.ts b/apps/playground/src/app/components/layout/description-list/description-list.component.ts index d29697badb..6f61c91d1e 100644 --- a/apps/playground/src/app/components/layout/description-list/description-list.component.ts +++ b/apps/playground/src/app/components/layout/description-list/description-list.component.ts @@ -24,4 +24,10 @@ export class DescriptionListComponent { description: '2024', }, ]; + + public showHelp = false; + + public toggleHelp(): void { + this.showHelp = !this.showHelp; + } } diff --git a/apps/playground/src/app/components/lookup/lookup/lookup.component.html b/apps/playground/src/app/components/lookup/lookup/lookup.component.html index 91e03a4c71..6fec61ef28 100644 --- a/apps/playground/src/app/components/lookup/lookup/lookup.component.html +++ b/apps/playground/src/app/components/lookup/lookup/lookup.component.html @@ -20,9 +20,9 @@

    Touched
    - + diff --git a/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.html b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.html index 08a6c80c23..77961cf46f 100644 --- a/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.html +++ b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.html @@ -1,37 +1,44 @@

    Standard phone field

    -
    - - +
    + +
    -

    Entered phone number is {{ phoneNumber }}

    +@if (phoneNumber) { +

    Entered phone number is {{ phoneNumber }}

    +}

    Reactive phone field with default country

    - + -

    - Entered phone number is {{ phoneControl.value }} -

    +@if (phoneControl.value) { +

    Entered phone number is {{ phoneControl.value }}

    +} + +

    Phone field inside input box

    @@ -51,7 +58,7 @@

    Phone field inside input box

    Phone field inside input box mode="detect" > - +
    diff --git a/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.scss b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.scss new file mode 100644 index 0000000000..b05bfc7613 --- /dev/null +++ b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.scss @@ -0,0 +1,6 @@ +.phone-field-playground-instance { + width: 50%; + height: 160px; + margin: 10px; + padding: 10px; +} diff --git a/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.ts b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.ts index 5277f4ec08..5e229d1129 100644 --- a/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.ts +++ b/apps/playground/src/app/components/phone-field/phone-field/phone-field.component.ts @@ -14,10 +14,21 @@ export class PhoneFieldComponent implements OnInit { public phoneControl: UntypedFormControl; - public ngOnInit() { - this.phoneControl = new UntypedFormControl(); + public selectedCountry = { + iso2: 'US', + }; + + public ngOnInit(): void { + this.phoneControl = new UntypedFormControl('733 05 92 50'); this.phoneForm = new UntypedFormGroup({ phoneControl: this.phoneControl, }); + this.phoneControl.valueChanges.subscribe((change) => console.log(change)); + } + + public switchToAustralia(): void { + this.selectedCountry = { + iso2: 'au', + }; } } diff --git a/apps/playground/src/app/shared/help.service.ts b/apps/playground/src/app/shared/help.service.ts index 06c6559f40..08723049db 100644 --- a/apps/playground/src/app/shared/help.service.ts +++ b/apps/playground/src/app/shared/help.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { SkyHelpOpenArgs, SkyHelpService } from '@skyux/core'; @Injectable() -export class PlaygroundHelpService implements SkyHelpService { - public openHelp(args: SkyHelpOpenArgs): void { +export class PlaygroundHelpService extends SkyHelpService { + public override openHelp(args: SkyHelpOpenArgs): void { alert('help key: ' + args.helpKey); } } diff --git a/karma.conf.js b/karma.conf.js index 2e9221dffb..4b920a94f9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -38,12 +38,12 @@ module.exports = () => { ], customLaunchers: { ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', + base: 'Chrome', flags: [ - '--no-sandbox', - '--headless', + '--headless=new', '--disable-gpu', '--disable-dev-shm-usage', + '--window-size=1920,1080', ], }, }, @@ -52,7 +52,6 @@ module.exports = () => { jasmine: { random: false, }, - clearContext: false, // leave Jasmine Spec Runner output visible in browser }, coverageReporter: { dir: join(__dirname, './coverage'), diff --git a/libs/components/a11y/package.json b/libs/components/a11y/package.json index c9ba7c01f1..18eeb43128 100644 --- a/libs/components/a11y/package.json +++ b/libs/components/a11y/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER" }, diff --git a/libs/components/action-bars/package.json b/libs/components/action-bars/package.json index 2b18dc043c..060cd79c64 100644 --- a/libs/components/action-bars/package.json +++ b/libs/components/action-bars/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/animations": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", diff --git a/libs/components/ag-grid/package.json b/libs/components/ag-grid/package.json index 68dbc14d1d..93225b978d 100644 --- a/libs/components/ag-grid/package.json +++ b/libs/components/ag-grid/package.json @@ -21,9 +21,9 @@ } }, "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", "@skyux/autonumeric": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/data-manager": "0.0.0-PLACEHOLDER", @@ -35,8 +35,8 @@ "@skyux/lookup": "0.0.0-PLACEHOLDER", "@skyux/popovers": "0.0.0-PLACEHOLDER", "@skyux/theme": "0.0.0-PLACEHOLDER", - "ag-grid-angular": "~31.2.0", - "ag-grid-community": "~31.2.0" + "ag-grid-angular": "^31.3.4", + "ag-grid-community": "^31.3.4" }, "dependencies": { "@skyux/icon": "0.0.0-PLACEHOLDER", diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.spec.ts index 3826846d95..db1331a8fa 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.spec.ts @@ -532,6 +532,7 @@ it('should move the horizontal scroll based on enableTopScroll check', async () 'ag-floating-top', 'ag-body', 'ag-sticky-top', + 'ag-sticky-bottom', 'ag-floating-bottom', 'ag-overlay', ]); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts index 195809fdf0..db6962f4c6 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts @@ -509,6 +509,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { 'ag-floating-top', 'ag-body', 'ag-sticky-top', + 'ag-sticky-bottom', 'ag-floating-bottom', 'ag-overlay', ]); @@ -530,6 +531,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { 'ag-floating-top', 'ag-body', 'ag-sticky-top', + 'ag-sticky-bottom', 'ag-floating-bottom', 'ag-body-horizontal-scroll', 'ag-overlay', @@ -547,6 +549,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { 'ag-floating-top', 'ag-body', 'ag-sticky-top', + 'ag-sticky-bottom', 'ag-floating-bottom', 'ag-overlay', ]); @@ -562,6 +565,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { 'ag-floating-top', 'ag-body', 'ag-sticky-top', + 'ag-sticky-bottom', 'ag-floating-bottom', 'ag-body-horizontal-scroll', 'ag-overlay', diff --git a/libs/components/angular-tree-component/package.json b/libs/components/angular-tree-component/package.json index db1bb2d030..8fc6a89fe9 100644 --- a/libs/components/angular-tree-component/package.json +++ b/libs/components/angular-tree-component/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@blackbaud/angular-tree-component": "^1.0.0", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/animations/package.json b/libs/components/animations/package.json index 6a3775788c..60c2127279 100644 --- a/libs/components/animations/package.json +++ b/libs/components/animations/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/animations": "^17.3.12", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12" }, "dependencies": { "tslib": "^2.6.2" diff --git a/libs/components/assets/package.json b/libs/components/assets/package.json index 59afed3b06..f59146c4a8 100644 --- a/libs/components/assets/package.json +++ b/libs/components/assets/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12" }, "dependencies": { "tslib": "^2.6.2" diff --git a/libs/components/autonumeric/package.json b/libs/components/autonumeric/package.json index c0456a8ad0..5572e50ef4 100644 --- a/libs/components/autonumeric/package.json +++ b/libs/components/autonumeric/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", "autonumeric": "^4.10.5" }, "dependencies": { diff --git a/libs/components/avatar/package.json b/libs/components/avatar/package.json index 5186fcd2c8..c67ce81192 100644 --- a/libs/components/avatar/package.json +++ b/libs/components/avatar/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/errors": "0.0.0-PLACEHOLDER", diff --git a/libs/components/colorpicker/package.json b/libs/components/colorpicker/package.json index d9cb2ef382..985115c59b 100644 --- a/libs/components/colorpicker/package.json +++ b/libs/components/colorpicker/package.json @@ -16,11 +16,11 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.html b/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.html index 25c955afe6..6efc194ce1 100644 --- a/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.html +++ b/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.html @@ -30,6 +30,7 @@
    @@ -73,18 +73,18 @@ *ngIf="selectable" class="sky-repeater-item-checkbox" [checked]="isSelected" - [label]=" + [labelHidden]="true" + [labelText]=" itemName ? ('skyux_repeater_item_checkbox_label' | skyLibResources: itemName) : ('skyux_repeater_item_checkbox_label_default' | skyLibResources) " (change)="onCheckboxChange($event)" - > - + />
    @@ -92,7 +92,7 @@ -
    +
    @@ -136,8 +136,7 @@ " [direction]="isExpanded ? 'up' : 'down'" (directionChange)="chevronDirectionChange($event)" - > - + />
    diff --git a/libs/components/lists/src/lib/modules/repeater/repeater-item.component.scss b/libs/components/lists/src/lib/modules/repeater/repeater-item.component.scss index ee4bc62240..24cce64b05 100644 --- a/libs/components/lists/src/lib/modules/repeater/repeater-item.component.scss +++ b/libs/components/lists/src/lib/modules/repeater/repeater-item.component.scss @@ -184,13 +184,13 @@ sky-repeater-item { border-left-color: $sky-theme-modern-background-color-primary-dark; } - &:focus, - &:active:focus { + &:focus-visible, + &:focus-visible:active { outline: solid 2px $sky-theme-modern-background-color-primary-dark; outline-offset: -2px; } - &:focus:not(:active) { + &:focus-visible:not(:active) { box-shadow: $sky-theme-modern-elevation-3-shadow-size $sky-theme-modern-elevation-3-shadow-color; } diff --git a/libs/components/lists/src/lib/modules/repeater/repeater.component.spec.ts b/libs/components/lists/src/lib/modules/repeater/repeater.component.spec.ts index ba6fef7ed4..b607bec31b 100644 --- a/libs/components/lists/src/lib/modules/repeater/repeater.component.spec.ts +++ b/libs/components/lists/src/lib/modules/repeater/repeater.component.spec.ts @@ -1457,9 +1457,10 @@ describe('Repeater item component', () => { const fixture = TestBed.createComponent( RepeaterWithMissingTagsFixtureComponent, ); - const consoleSpy = spyOn(console, 'warn'); + const logService = TestBed.inject(SkyLogService); + const logServiceSpy = spyOn(logService, 'warn'); detectChangesAndTick(fixture); - expect(consoleSpy).toHaveBeenCalled(); + expect(logServiceSpy).toHaveBeenCalled(); })); describe('dragula integration', () => { @@ -1528,7 +1529,7 @@ describe('Repeater item component', () => { let cmp: RepeaterTestComponent; let el: any; let mockDragulaService: MockDragulaService; - let consoleSpy: jasmine.Spy; + let logServiceSpy: jasmine.Spy; function fireDragEvent(dragEvent: 'drag' | 'dragend', index: number): void { const groupName = fixture.componentInstance.repeater?.dragulaGroupName; @@ -1551,7 +1552,8 @@ describe('Repeater item component', () => { cmp = fixture.componentInstance; el = fixture.nativeElement; - consoleSpy = spyOn(console, 'warn'); + const logService = TestBed.inject(SkyLogService); + logServiceSpy = spyOn(logService, 'warn'); fixture.detectChanges(); cmp.reorderable = true; @@ -1581,7 +1583,7 @@ describe('Repeater item component', () => { it('should not show a console warning if all item tags are defined', fakeAsync(() => { detectChangesAndTick(fixture); - expect(consoleSpy).not.toHaveBeenCalled(); + expect(logServiceSpy).not.toHaveBeenCalled(); })); it('should set newly added items to reorderable if repeater is reorderable', fakeAsync(() => { @@ -1993,7 +1995,7 @@ describe('Repeater item component', () => { cmp.showRepeaterWithNgFor = true; detectChangesAndTick(fixture); - expect(consoleSpy).not.toHaveBeenCalled(); + expect(logServiceSpy).not.toHaveBeenCalled(); cmp.items = [ { @@ -2006,10 +2008,34 @@ describe('Repeater item component', () => { ]; detectChangesAndTick(fixture); - expect(consoleSpy).toHaveBeenCalledWith( + expect(logServiceSpy).toHaveBeenCalledWith( 'Please supply tag properties for each repeater item when reordering functionality is enabled.', ); })); + + it('should not show a console warning when the items change and no items exist', fakeAsync(() => { + cmp.showRepeaterWithNgFor = true; + detectChangesAndTick(fixture); + + expect(logServiceSpy).not.toHaveBeenCalled(); + + cmp.items = []; + detectChangesAndTick(fixture); + + expect(logServiceSpy).not.toHaveBeenCalled(); + })); + + it('should not show a console warning when the items change and items are undefined', fakeAsync(() => { + cmp.showRepeaterWithNgFor = true; + detectChangesAndTick(fixture); + + expect(logServiceSpy).not.toHaveBeenCalled(); + + cmp.items = undefined; + detectChangesAndTick(fixture); + + expect(logServiceSpy).not.toHaveBeenCalled(); + })); }); describe('aria roles', () => { diff --git a/libs/components/lists/src/lib/modules/repeater/repeater.component.ts b/libs/components/lists/src/lib/modules/repeater/repeater.component.ts index a2361949a5..ec5591533d 100644 --- a/libs/components/lists/src/lib/modules/repeater/repeater.component.ts +++ b/libs/components/lists/src/lib/modules/repeater/repeater.component.ts @@ -389,10 +389,9 @@ export class SkyRepeaterComponent } #everyItemHasTag(): boolean { - /* sanity check */ - /* istanbul ignore if */ + /* safety check */ if (!this.items || this.items.length === 0) { - return false; + return true; } return this.items.toArray().every((item) => { return item.tag !== undefined; @@ -470,7 +469,7 @@ export class SkyRepeaterComponent #validateTags(): void { if (this.reorderable && !this.#everyItemHasTag()) { - console.warn( + this.#logSvc.warn( 'Please supply tag properties for each repeater item when reordering functionality is enabled.', ); } diff --git a/libs/components/lists/src/lib/modules/sort/sort-item.component.scss b/libs/components/lists/src/lib/modules/sort/sort-item.component.scss new file mode 100644 index 0000000000..d888df8560 --- /dev/null +++ b/libs/components/lists/src/lib/modules/sort/sort-item.component.scss @@ -0,0 +1,59 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; +@use 'libs/components/theme/src/lib/styles/_public-api/themes/modern/_compat/mixins' + as modernMixins; + +.sky-sort-item { + @include mixins.sky-dropdown-item(false); +} + +.sky-sort-item-selected { + background-color: $sky-background-color-selected; + padding: 4px; + margin: 0; +} + +.sky-theme-modern { + .sky-sort-item { + margin: 0; + @include modernMixins.sky-theme-modern-btn-tab; + @include modernMixins.sky-theme-modern-btn-tab-dropdown-item; + padding: $sky-theme-modern-space-sm $sky-theme-modern-space-lg; + + &:focus-within { + background-color: transparent; + box-shadow: $sky-theme-modern-elevation-3-shadow-size + $sky-theme-modern-elevation-3-shadow-color; + outline: solid 2px var(--sky-background-color-primary-dark); + outline-offset: -2px; + } + + & button { + padding: 0; + color: $sky-text-color-deemphasized; + &:focus-visible { + outline: none; + } + } + } + + .sky-sort-item-selected { + @include modernMixins.sky-theme-modern-btn-tab-selected-dropdown-item; + padding-left: calc(#{$sky-theme-modern-space-lg} - 3px); + background-color: inherit; + + & button { + font-weight: $sky-theme-modern-text-weight-regular-value; + color: $sky-text-color-default; + } + } +} + +.sky-theme-modern.sky-theme-modern-dark { + .sky-sort-item button { + color: $sky-theme-modern-mode-dark-font-deemphasized-color; + } + .sky-sort-item-selected button { + color: $sky-theme-modern-mode-dark-font-body-default-color; + } +} diff --git a/libs/components/lists/src/lib/modules/sort/sort-item.component.ts b/libs/components/lists/src/lib/modules/sort/sort-item.component.ts index 16e3798ce0..6c26c5e4de 100644 --- a/libs/components/lists/src/lib/modules/sort/sort-item.component.ts +++ b/libs/components/lists/src/lib/modules/sort/sort-item.component.ts @@ -11,6 +11,7 @@ import { SimpleChanges, TemplateRef, ViewChild, + ViewEncapsulation, } from '@angular/core'; import { BehaviorSubject, Subscription } from 'rxjs'; @@ -24,7 +25,9 @@ let sortItemIdNumber = 0; @Component({ selector: 'sky-sort-item', templateUrl: './sort-item.component.html', + styleUrls: ['./sort-item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, }) export class SkySortItemComponent implements OnInit, OnChanges, OnDestroy { /** diff --git a/libs/components/lists/src/lib/modules/sort/sort.component.scss b/libs/components/lists/src/lib/modules/sort/sort.component.scss index c02a5d7450..bec771622c 100644 --- a/libs/components/lists/src/lib/modules/sort/sort.component.scss +++ b/libs/components/lists/src/lib/modules/sort/sort.component.scss @@ -21,60 +21,3 @@ display: inline; } } - -::ng-deep { - .sky-sort-item { - @include mixins.sky-dropdown-item(); - } - - .sky-sort-item-selected { - background-color: $sky-background-color-selected; - padding: 4px; - margin: 0; - } - - @include mixins.sky-theme-modern { - .sky-sort-item { - margin: 0; - @include modernMixins.sky-theme-modern-btn-tab; - @include modernMixins.sky-theme-modern-btn-tab-dropdown-item; - padding: $sky-theme-modern-space-sm $sky-theme-modern-space-lg; - - &:focus-within { - background-color: transparent; - box-shadow: $sky-theme-modern-elevation-3-shadow-size - $sky-theme-modern-elevation-3-shadow-color; - outline: solid 2px var(--sky-background-color-primary-dark); - outline-offset: -2px; - } - - & button { - padding: 0; - color: $sky-text-color-deemphasized; - &:focus-visible { - outline: none; - } - } - } - - .sky-sort-item-selected { - @include modernMixins.sky-theme-modern-btn-tab-selected-dropdown-item; - padding-left: calc(#{$sky-theme-modern-space-lg} - 3px); - background-color: inherit; - - & button { - font-weight: $sky-theme-modern-text-weight-regular-value; - color: $sky-text-color-default; - } - } - } - - @include mixins.sky-theme-modern-dark { - .sky-sort-item button { - color: $sky-theme-modern-mode-dark-font-deemphasized-color; - } - .sky-sort-item-selected button { - color: $sky-theme-modern-mode-dark-font-body-default-color; - } - } -} diff --git a/libs/components/lookup/package.json b/libs/components/lookup/package.json index 6c44811772..d0793407dd 100644 --- a/libs/components/lookup/package.json +++ b/libs/components/lookup/package.json @@ -16,12 +16,12 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts index 935fb7daa6..8837909977 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts @@ -77,6 +77,12 @@ describe('Autocomplete component', () => { ) as HTMLElement; } + function getWaitWrapper(): HTMLElement | null { + return document.querySelector( + '.sky-autocomplete-results-waiting', + ); + } + function enterSearch( newValue: string, fixture: ComponentFixture< @@ -790,6 +796,79 @@ describe('Autocomplete component', () => { expect(dropdownElement).not.toBeNull(); })); + it('should show dropdown async search is running with no action area', fakeAsync(() => { + fixture.detectChanges(); + const spy = spyOn( + asyncAutocomplete.searchAsync, + 'emit', + ).and.callThrough(); + + enterSearch('r', fixture, true); + + expect(spy).toHaveBeenCalledWith({ + displayType: 'popover', + offset: 0, + searchText: 'r', + result: jasmine.any(Observable), + }); + + let dropdownElement = getSearchResultsContainer(); + let waitWrapper = getWaitWrapper(); + + expect(dropdownElement).not.toBeNull(); + expect(waitWrapper).not.toBeNull(); + + tick(200); + fixture.detectChanges(); + + dropdownElement = getSearchResultsContainer(); + waitWrapper = getWaitWrapper(); + + expect(dropdownElement).not.toBeNull(); + expect(waitWrapper).toBeNull(); + + expect(asyncAutocomplete.searchResults.length).toEqual(6); + expect(asyncAutocomplete.searchResults[0].data.name).toEqual('Red'); + expect(asyncAutocomplete.searchResults[1].data.name).toEqual('Green'); + })); + + it('should show dropdown async search is running with an action area', fakeAsync(() => { + component.showAddButton = true; + fixture.detectChanges(); + const spy = spyOn( + asyncAutocomplete.searchAsync, + 'emit', + ).and.callThrough(); + + enterSearch('r', fixture, true); + + expect(spy).toHaveBeenCalledWith({ + displayType: 'popover', + offset: 0, + searchText: 'r', + result: jasmine.any(Observable), + }); + + let dropdownElement = getSearchResultsContainer(); + let waitWrapper = getWaitWrapper(); + + expect(dropdownElement).not.toBeNull(); + expect(waitWrapper).not.toBeNull(); + + tick(200); + fixture.detectChanges(); + + dropdownElement = getSearchResultsContainer(); + waitWrapper = getWaitWrapper(); + + expect(dropdownElement).not.toBeNull(); + expect(waitWrapper).toBeNull(); + + expect(asyncAutocomplete.searchResults.length).toEqual(6); + expect(asyncAutocomplete.searchResults[0].data.name).toEqual('Red'); + expect(asyncAutocomplete.searchResults[1].data.name).toEqual('Green'); + })); + it('should emit an undefined value when text input is cleared', fakeAsync(() => { fixture.detectChanges(); diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts index 5c4d679389..191d1f3d86 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts @@ -725,6 +725,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { #searchTextChanged(searchText: string | undefined): void { if (this.#hasFocus) { + this.#openDropdown(); const isEmpty = !searchText || !searchText.trim() || searchText.match(/^\s+$/); @@ -792,6 +793,8 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { this.#updateIsResultsVisible(); this.#changeDetector.markForCheck(); + // Safety check + /* istanbul ignore else */ if (this.isOpen) { // Let the results populate in the DOM before recalculating placement. setTimeout(() => { diff --git a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.html b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.html index f307db7187..efde33b468 100644 --- a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.html +++ b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.html @@ -1,8 +1,13 @@
    - - + Test + diff --git a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.ts b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.ts index 0cbcd31045..efc213041c 100644 --- a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-input-box.component.fixture.ts @@ -18,6 +18,10 @@ export class SkyLookupInputBoxTestComponent { }) public lookupComponent!: SkyLookupComponent; + public ariaLabel: string | undefined; + + public ariaLabelledBy: string | undefined; + public autocompleteAttribute: string | undefined; public data: any[] = []; @@ -55,4 +59,24 @@ export class SkyLookupInputBoxTestComponent { friends: new UntypedFormControl(this.friends), }); } + + public resetForm(): void { + this.form.reset(); + } + + public setMultiSelect(): void { + this.selectMode = 'multiple'; + } + + public setSingleSelect(): void { + this.selectMode = 'single'; + } + + public setValue(index: number | undefined): void { + if (this.data && index) { + this.form.controls['friends'].setValue([this.data[index]]); + } else { + this.form.controls['friends'].setValue(undefined); + } + } } diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup-autocomplete-adapter.ts b/libs/components/lookup/src/lib/modules/lookup/lookup-autocomplete-adapter.ts index 5c8a1365e5..b3ee22804a 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup-autocomplete-adapter.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup-autocomplete-adapter.ts @@ -39,8 +39,9 @@ export class SkyLookupAutocompleteAdapter { } /** - * The array of object properties to search. + * The array of object properties to search when utilizing the `data` property and the built-in search function. * @default ["name"] + * @deprecated Use the `searchAsync` event emitter and callback instead to provide data to the lookup component. */ @Input() public set propertiesToSearch(value: string[] | undefined) { @@ -57,9 +58,8 @@ export class SkyLookupAutocompleteAdapter { * The function to dynamically manage the data source when users * change the text in the lookup field. The search function must return * an array or a promise of an array. The `search` property is particularly - * useful when the data source does not live in the source code. If the - * search requires calling a remote data source, use `searchAsync` instead of - * `search`. + * useful when the data source does not live in the source code. + * @deprecated Use the `searchAsync` event emitter and callback instead to provide searched data to the lookup component. */ @Input() public set search(value: SkyAutocompleteSearchFunction | undefined) { @@ -95,10 +95,11 @@ export class SkyLookupAutocompleteAdapter { /** * The array of functions to call against each search result in order - * to filter the search results when using the default search function. When + * to filter the search results when using the `data` input and the default search function. When * using a custom search function via the `search` property filters must be * applied manually inside that function. The function must return `true` or * `false` for each result to indicate whether to display it in the dropdown list. + * @deprecated Use the `searchAsync` event emitter and callback instead to provide searched data to the lookup component. */ @Input() public set searchFilters( @@ -118,13 +119,15 @@ export class SkyLookupAutocompleteAdapter { /** * The maximum number of search results to display in the dropdown * list. By default, the lookup component displays all matching results. + * This property has no effect on the results in the "Show more" picker. */ @Input() public searchResultsLimit: number | undefined; /** * Fires when users enter new search information and allows results to be - * returned via an observable. + * returned via an observable. The event is also fired with empty search text + * when the "Show more" picker is opened without search text. */ @Output() public searchAsync = new EventEmitter(); diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup.component.html b/libs/components/lookup/src/lib/modules/lookup/lookup.component.html index 2add7a3c78..a13316c451 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.html +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.html @@ -13,7 +13,7 @@ #lookupWrapper > { - fixture.componentInstance.ariaLabelledBy = 'my-lookup-label'; - fixture.componentInstance.setSingleSelect(); - fixture.componentInstance.setValue(2); - - fixture.detectChanges(); - await fixture.whenStable(); - await expectAsync(document.body).toBeAccessible(axeConfig); - - fixture.componentInstance.ariaLabel = 'My lookup label'; - fixture.componentInstance.ariaLabelledBy = undefined; - - fixture.detectChanges(); - await fixture.whenStable(); - await expectAsync(document.body).toBeAccessible(axeConfig); - - fixture.componentInstance.setMultiSelect(); - fixture.componentInstance.setValue(1); - - fixture.detectChanges(); - await fixture.whenStable(); - await expectAsync(document.body).toBeAccessible(axeConfig); - - fixture.componentInstance.setSingleSelect(); - fixture.componentInstance.resetForm(); - - fixture.detectChanges(); - await fixture.whenStable(); - await expectAsync(document.body).toBeAccessible(axeConfig); - - const inputElement = getInputElement( - fixture.componentInstance.lookupComponent, - ); - inputElement.value = 'r'; - inputElement.focus(); - SkyAppTestUtility.fireDomEvent(inputElement, 'keyup', { - keyboardEventInit: { key: '' }, - }); - - fixture.detectChanges(); - await fixture.whenStable(); - await expectAsync(document.body).toBeAccessible(axeConfig); - }); - }); - // for testing non-async search args being passed around correctly describe('search args (non-async)', () => { // to test the passing of the 'context' arg @@ -7573,5 +7519,167 @@ describe('Lookup component', function () { expect(document.activeElement).not.toEqual(input); })); }); + + describe('a11y', function () { + const axeConfig = { + rules: { + region: { + enabled: false, + }, + }, + }; + + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should be accessible [mode: single, value: set, ariaLabel: undefined, ariaLabelledBy: set]', async () => { + fixture.componentInstance.ariaLabelledBy = 'my-lookup-label'; + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(2); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: set, ariaLabel: set, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(2); + fixture.componentInstance.ariaLabel = 'My lookup label'; + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: set, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(2); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: undefined, ariaLabel: undefined, ariaLabelledBy: set]', async () => { + fixture.componentInstance.ariaLabelledBy = 'my-lookup-label'; + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(undefined); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: undefined, ariaLabel: set, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(undefined); + fixture.componentInstance.ariaLabel = 'My lookup label'; + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: undefined, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setSingleSelect(); + fixture.componentInstance.setValue(undefined); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: set, ariaLabel: undefined, ariaLabelledBy: set]', async () => { + fixture.componentInstance.ariaLabelledBy = 'my-lookup-label'; + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(2); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: set, ariaLabel: set, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(2); + fixture.componentInstance.ariaLabel = 'My lookup label'; + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: set, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(2); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: undefined, ariaLabel: undefined, ariaLabelledBy: set]', async () => { + fixture.componentInstance.ariaLabelledBy = 'my-lookup-label'; + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(undefined); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: undefined, ariaLabel: set, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(undefined); + fixture.componentInstance.ariaLabel = 'My lookup label'; + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: undefined, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setMultiSelect(); + fixture.componentInstance.setValue(undefined); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: single, value: input typed, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setSingleSelect(); + const inputElement = getInputElement( + fixture.componentInstance.lookupComponent, + ); + inputElement.value = 'r'; + inputElement.focus(); + SkyAppTestUtility.fireDomEvent(inputElement, 'keyup', { + keyboardEventInit: { key: '' }, + }); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + + it('should be accessible [mode: multiple, value: input typed, ariaLabel: undefined, ariaLabelledBy: undefined]', async () => { + fixture.componentInstance.setMultiSelect(); + const inputElement = getInputElement( + fixture.componentInstance.lookupComponent, + ); + inputElement.value = 'r'; + inputElement.focus(); + SkyAppTestUtility.fireDomEvent(inputElement, 'keyup', { + keyboardEventInit: { key: '' }, + }); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(document.body).toBeAccessible(axeConfig); + }); + }); }); }); diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts index 91e8d7c792..5bbd173c42 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts @@ -66,6 +66,7 @@ export class SkyLookupComponent * [to support accessibility](https://developer.blackbaud.com/skyux/learn/accessibility). * If the input includes a visible label, use `ariaLabelledBy` instead. * For more information about the `aria-label` attribute, see the [WAI-ARIA definition](https://www.w3.org/TR/wai-aria/#aria-label). + * @deprecated Use the input box `labelText` input instead. */ @Input() public ariaLabel: string | undefined; @@ -76,6 +77,7 @@ export class SkyLookupComponent * [to support accessibility](https://developer.blackbaud.com/skyux/learn/accessibility). * If the input does not include a visible label, use `ariaLabel` instead. * For more information about the `aria-labelledby` attribute, see the [WAI-ARIA definition](https://www.w3.org/TR/wai-aria/#aria-labelledby). + * @deprecated Use the input box `labelText` input instead. */ @Input() public ariaLabelledBy: string | undefined; @@ -93,6 +95,7 @@ export class SkyLookupComponent * enter text. You can specify static data such as an array of objects, or * you can pull data from a database. * @default [] + * @deprecated Use the `searchAsync` event emitter and callback instead to provide data to the lookup component. */ @Input() public set data(value: any[] | undefined) { diff --git a/libs/components/modals/package.json b/libs/components/modals/package.json index 0a1cbdb7b3..1c61d80876 100644 --- a/libs/components/modals/package.json +++ b/libs/components/modals/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/router": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/router": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/help-inline": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.html b/libs/components/modals/src/lib/modules/modal/modal.component.html index 58132ebfad..e20f7f56e8 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.html +++ b/libs/components/modals/src/lib/modules/modal/modal.component.html @@ -77,7 +77,8 @@ tabindex="0" skyId [attr.aria-labelledby]="headerId.id" - (skyModalScrollShadow)="scrollShadowChange($event)" + (skyScrollShadow)="scrollShadowChange($event)" + [skyScrollShadowEnabled]="scrollShadowEnabled" #modalContentId="skyId" #modalContentWrapper > diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts b/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts index e49efadfd3..f9f99e0afe 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts @@ -1058,7 +1058,7 @@ describe('Modal component', () => { await expectAsync(getModalElement()).toBeAccessible(); }); - describe('when modern theme', () => { + describe('scroll shadow', () => { function scrollContent(contentEl: HTMLElement, top: number): void { contentEl.scrollTop = top; @@ -1113,129 +1113,221 @@ describe('Modal component', () => { } } - it('should progressively show a drop shadow as the modal content scrolls', fakeAsync(() => { - setModernTheme(); - - const modalInstance1 = openModal(ModalTestComponent); - - const modalHeaderEl = document.querySelector( - '.sky-modal-header', - ) as HTMLElement; - const modalContentEl = document.querySelector( - '.sky-modal-content', - ) as HTMLElement; - const modalFooterEl = document.querySelector( - '.sky-modal-footer', - ) as HTMLElement; - - const fixtureContentEl = document.querySelector( - '.modal-fixture-content', - ) as HTMLElement; - fixtureContentEl.style.height = `${window.innerHeight + 100}px`; - - scrollContent(modalContentEl, 0); - validateShadow(modalHeaderEl); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 15); - validateShadow(modalHeaderEl, 0.15); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 30); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 31); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.3); - - scrollContent( - modalContentEl, - modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, - ); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.15); + describe('when default theme', () => { + it('should not show a drop shadow as the modal content scrolls', fakeAsync(() => { + const modalInstance1 = openModal(ModalTestComponent); + + const modalHeaderEl = document.querySelector( + '.sky-modal-header', + ) as HTMLElement; + const modalContentEl = document.querySelector( + '.sky-modal-content', + ) as HTMLElement; + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + fixtureContentEl.style.height = `${window.innerHeight + 100}px`; + + scrollContent(modalContentEl, 0); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 15); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 30); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 31); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); - scrollContent( - modalContentEl, - modalContentEl.scrollHeight - modalContentEl.clientHeight, - ); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl); + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); - closeModal(modalInstance1); - })); + closeModal(modalInstance1); + })); - it('should check for shadow when elements are added to the modal content', fakeAsync(() => { - let mutateCallback: MutationCallback | undefined; + it('should not check for shadow when elements are added to the modal content', fakeAsync(() => { + let mutateCallback: MutationCallback | undefined; - const fakeMutationObserver: MutationObserver = { - observe: jasmine.createSpy('observe'), - disconnect: jasmine.createSpy('disconnect'), - takeRecords: jasmine.createSpy('takeRecords'), - }; + const fakeMutationObserver: MutationObserver = { + observe: jasmine.createSpy('observe'), + disconnect: jasmine.createSpy('disconnect'), + takeRecords: jasmine.createSpy('takeRecords'), + }; - spyOn(TestBed.inject(SkyMutationObserverService), 'create').and.callFake( - (cb) => { + spyOn( + TestBed.inject(SkyMutationObserverService), + 'create', + ).and.callFake((cb) => { mutateCallback = cb; return fakeMutationObserver; - }, - ); + }); - setModernTheme(); + const modalInstance1 = openModal(ModalTestComponent); - const modalInstance1 = openModal(ModalTestComponent); + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; - const modalFooterEl = document.querySelector( - '.sky-modal-footer', - ) as HTMLElement; + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; - const fixtureContentEl = document.querySelector( - '.modal-fixture-content', - ) as HTMLElement; + const childEl = document.createElement('div'); + childEl.style.height = `${window.innerHeight + 100}px`; + childEl.style.backgroundColor = 'red'; - const childEl = document.createElement('div'); - childEl.style.height = `${window.innerHeight + 100}px`; - childEl.style.backgroundColor = 'red'; + fixtureContentEl.appendChild(childEl); - fixtureContentEl.appendChild(childEl); + triggerMutation(mutateCallback, fakeMutationObserver); - triggerMutation(mutateCallback, fakeMutationObserver); + tick(); + getApplicationRef().tick(); - tick(); - getApplicationRef().tick(); + validateShadow(modalFooterEl); - validateShadow(modalFooterEl, 0.3); + fixtureContentEl.removeChild(childEl); - fixtureContentEl.removeChild(childEl); + triggerMutation(mutateCallback, fakeMutationObserver); - triggerMutation(mutateCallback, fakeMutationObserver); + tick(); + getApplicationRef().tick(); - tick(); - getApplicationRef().tick(); + validateShadow(modalFooterEl); - validateShadow(modalFooterEl); + closeModal(modalInstance1); + })); + }); - closeModal(modalInstance1); - })); + describe('when modern theme', () => { + it('should progressively show a drop shadow as the modal content scrolls', fakeAsync(() => { + setModernTheme(); + + const modalInstance1 = openModal(ModalTestComponent); + + const modalHeaderEl = document.querySelector( + '.sky-modal-header', + ) as HTMLElement; + const modalContentEl = document.querySelector( + '.sky-modal-content', + ) as HTMLElement; + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + fixtureContentEl.style.height = `${window.innerHeight + 100}px`; + + scrollContent(modalContentEl, 0); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 15); + validateShadow(modalHeaderEl, 0.15); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 30); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 31); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.3); + + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.15); - it('should not create multiple mutation observers', fakeAsync(() => { - const modalInstance1 = openModal(ModalTestComponent); + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl); - const mutationObserverCreateSpy = spyOn( - TestBed.inject(SkyMutationObserverService), - 'create', - ).and.callThrough(); + closeModal(modalInstance1); + })); - setModernTheme(); - setModernTheme(); - setModernTheme(); + it('should check for shadow when elements are added to the modal content', fakeAsync(() => { + let mutateCallback: MutationCallback | undefined; - expect(mutationObserverCreateSpy.calls.count()).toBe(1); + const fakeMutationObserver: MutationObserver = { + observe: jasmine.createSpy('observe'), + disconnect: jasmine.createSpy('disconnect'), + takeRecords: jasmine.createSpy('takeRecords'), + }; - closeModal(modalInstance1); - })); + spyOn( + TestBed.inject(SkyMutationObserverService), + 'create', + ).and.callFake((cb) => { + mutateCallback = cb; + + return fakeMutationObserver; + }); + + setModernTheme(); + + const modalInstance1 = openModal(ModalTestComponent); + + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + + const childEl = document.createElement('div'); + childEl.style.height = `${window.innerHeight + 100}px`; + childEl.style.backgroundColor = 'red'; + + fixtureContentEl.appendChild(childEl); + + triggerMutation(mutateCallback, fakeMutationObserver); + + tick(); + getApplicationRef().tick(); + + validateShadow(modalFooterEl, 0.3); + + fixtureContentEl.removeChild(childEl); + + triggerMutation(mutateCallback, fakeMutationObserver); + + tick(); + getApplicationRef().tick(); + + validateShadow(modalFooterEl); + + closeModal(modalInstance1); + })); + }); }); it('should pass accessibility with scrolling content', async () => { diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.ts b/libs/components/modals/src/lib/modules/modal/modal.component.ts index ffc9c3e308..51af8d7004 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.component.ts @@ -21,9 +21,12 @@ import { SkyIdModule, SkyLiveAnnouncerService, SkyResizeObserverMediaQueryService, + SkyScrollShadowDirective, + SkyScrollShadowEventArgs, } from '@skyux/core'; import { SkyHelpInlineModule } from '@skyux/help-inline'; import { SkyIconModule } from '@skyux/icon'; +import { SkyTheme, SkyThemeService } from '@skyux/theme'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -36,8 +39,6 @@ import { SkyModalError } from './modal-error'; import { SkyModalErrorsService } from './modal-errors.service'; import { SkyModalHeaderComponent } from './modal-header.component'; import { SkyModalHostService } from './modal-host.service'; -import { SkyModalScrollShadowEventArgs } from './modal-scroll-shadow-event-args'; -import { SkyModalScrollShadowDirective } from './modal-scroll-shadow.directive'; const ARIA_ROLE_DEFAULT = 'dialog'; @@ -62,7 +63,7 @@ const ARIA_ROLE_DEFAULT = 'dialog'; SkyIconModule, SkyIdModule, SkyModalHeaderComponent, - SkyModalScrollShadowDirective, + SkyScrollShadowDirective, SkyModalsResourcesModule, ], }) @@ -163,13 +164,18 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { public modalZIndex: number | undefined; - public scrollShadow: SkyModalScrollShadowEventArgs | undefined; + public scrollShadow: SkyScrollShadowEventArgs = { + bottomShadow: 'none', + topShadow: 'none', + }; public size: string; @ViewChild('modalContentWrapper', { read: ElementRef }) public modalContentWrapperElement: ElementRef | undefined; + protected scrollShadowEnabled = false; + #ngUnsubscribe = new Subject(); #_ariaDescribedBy: string | undefined; @@ -197,6 +203,7 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { readonly #config = inject(SkyModalConfiguration, { optional: true }) ?? new SkyModalConfiguration(); + readonly #themeSvc = inject(SkyThemeService, { optional: true }); constructor() { this.ariaDescribedBy = this.#config.ariaDescribedBy; @@ -280,6 +287,15 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { this.#changeDetector.markForCheck(); } }); + + if (this.#themeSvc) { + this.#themeSvc.settingsChange + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((themeSettings) => { + this.scrollShadowEnabled = + themeSettings.currentSettings.theme === SkyTheme.presets.modern; + }); + } } public ngAfterViewInit(): void { @@ -331,7 +347,7 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { this.#componentAdapter.handleWindowChange(this.#elRef); } - public scrollShadowChange(args: SkyModalScrollShadowEventArgs): void { + public scrollShadowChange(args: SkyScrollShadowEventArgs): void { this.scrollShadow = args; } diff --git a/libs/components/modals/testing/project.json b/libs/components/modals/testing/project.json index 4884438413..a9f3c9827e 100644 --- a/libs/components/modals/testing/project.json +++ b/libs/components/modals/testing/project.json @@ -5,7 +5,13 @@ "sourceRoot": "libs/components/modals/testing/src", "prefix": "sky", "tags": ["testing"], - "implicitDependencies": ["core-testing", "modals", "testing", "theme"], + "implicitDependencies": [ + "core-testing", + "help-inline-testing", + "modals", + "testing", + "theme" + ], "targets": { "build": { "command": "echo ' 🏗️ build modals-testing'", diff --git a/libs/components/modals/testing/src/modal/controller/modal-testing.controller.spec.ts b/libs/components/modals/testing/src/modal/controller/modal-testing.controller.spec.ts index 262653db06..d0d68cfdb6 100644 --- a/libs/components/modals/testing/src/modal/controller/modal-testing.controller.spec.ts +++ b/libs/components/modals/testing/src/modal/controller/modal-testing.controller.spec.ts @@ -82,6 +82,10 @@ class TestComponent implements OnDestroy { this.#instances.forEach((i) => i.close()); } + public getInstanceAt(index: number): SkyModalInstance | undefined { + return this.#instances.at(index); + } + public openModal(): void { const instance = this.#modalSvc.open(ModalTestComponent, { providers: [ @@ -143,6 +147,24 @@ describe('modal-testing.controller', () => { modalController.expectNone(); }); + it('should close a modal with args', () => { + const { fixture, modalController } = setupTest(); + + fixture.componentInstance.openModal(); + fixture.detectChanges(); + + const closeSpy = spyOn( + fixture.componentInstance.getInstanceAt(0)!, + 'close', + ).and.callThrough(); + + modalController.closeTopModal({ reason: 'save', data: { foo: 'bar' } }); + + fixture.detectChanges(); + + expect(closeSpy).toHaveBeenCalledWith({ foo: 'bar' }, 'save'); + }); + it('should throw if topmost modal does not match criteria', () => { const { fixture, modalController } = setupTest(); diff --git a/libs/components/modals/testing/src/modal/controller/modal-testing.service.ts b/libs/components/modals/testing/src/modal/controller/modal-testing.service.ts index 09d0859f43..25252a9879 100644 --- a/libs/components/modals/testing/src/modal/controller/modal-testing.service.ts +++ b/libs/components/modals/testing/src/modal/controller/modal-testing.service.ts @@ -40,7 +40,7 @@ export class SkyModalTestingService ); } - modal.instance.close(args); + modal.instance.close(args?.data, args?.reason); } public expectCount(value: number): void { diff --git a/libs/components/navbar/package.json b/libs/components/navbar/package.json index bf16ac5b7a..4de268532e 100644 --- a/libs/components/navbar/package.json +++ b/libs/components/navbar/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12" }, "dependencies": { "tslib": "^2.6.2" diff --git a/libs/components/omnibar-interop/package.json b/libs/components/omnibar-interop/package.json index b9ecc09513..64b3e14bb5 100644 --- a/libs/components/omnibar-interop/package.json +++ b/libs/components/omnibar-interop/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12" }, "dependencies": { "tslib": "^2.6.2" diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index 71a2fef96b..69cb6b9f62 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -59,7 +59,7 @@ "@skyux/grids": "0.0.0-PLACEHOLDER", "@skyux/help-inline": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", - "@skyux/icons": "^7.3.0", + "@skyux/icons": "^7.8.0", "@skyux/indicators": "0.0.0-PLACEHOLDER", "@skyux/inline-form": "0.0.0-PLACEHOLDER", "@skyux/layout": "0.0.0-PLACEHOLDER", @@ -85,15 +85,15 @@ "@skyux/tiles": "0.0.0-PLACEHOLDER", "@skyux/toast": "0.0.0-PLACEHOLDER", "@skyux/validation": "0.0.0-PLACEHOLDER", - "ag-grid-angular": "~31.2.0", - "ag-grid-community": "~31.2.0", - "ag-grid-enterprise": "~31.2.0", + "ag-grid-angular": "^31.3.4", + "ag-grid-community": "^31.3.4", + "ag-grid-enterprise": "^31.3.4", "autonumeric": "^4.10.5" } }, "peerDependencies": { - "@angular/cli": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/cli": "^17.3.9", + "@angular/core": "^17.3.12" }, "dependencies": { "fs-extra": "11.2.0", diff --git a/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.spec.ts b/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.spec.ts index 4048064899..27247a6933 100644 --- a/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.spec.ts +++ b/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.spec.ts @@ -19,20 +19,63 @@ interface TestSetup { schematic: (options: Schema) => Rule; } -const UPDATE_TO_VERSION = '31.2.0'; +const UPDATE_TO_VERSION = '31.3.4'; +const UPDATE_TO_MIGRATION = '31.3.0'; describe('ag-grid-migrate.schematic', () => { - async function setupTest(): Promise { + const defaultSetup = { + fileList: '', + sourceRoot: '.', + startingVersion: UPDATE_TO_VERSION, + debug: false, + }; + type Setup = typeof defaultSetup; + async function setupTest(setupOptions?: Partial): Promise { + const setup: Required = { ...defaultSetup, ...setupOptions }; const os = { platform: jest.fn().mockReturnValue('test'), }; const childProcess = { - spawnSync: jest.fn().mockReturnValue({ stdout: { toString: () => '' } }), + spawnSync: jest + .fn() + .mockImplementation((cmd: string, args?: string[]) => { + if ( + cmd === 'git' && + args?.join(' ') === + // eslint-disable-next-line @cspell/spellchecker + `cat-file --textconv HEAD:${setup.sourceRoot}/package-lock.json` + ) { + return { + stdout: JSON.stringify({ + packages: { + 'node_modules/ag-grid-community': { + version: setup.startingVersion, + }, + }, + }), + }; + } + if ( + cmd === 'git' && + args?.join(' ') === `ls-files ${setup.sourceRoot}/**/*.ts` + ) { + return { + stdout: setup.fileList, + }; + } + if (cmd.startsWith('npm') || cmd === 'node') { + return { + stdout: '', + }; + } + throw new Error(`Unexpected command: ${cmd} ${args?.join(' ')}`); + }), }; const context = { logger: { info: jest.fn(), } as unknown as logging.LoggerApi, + debug: setup.debug, } as SchematicContext; jest.doMock('os', () => os); @@ -58,10 +101,13 @@ describe('ag-grid-migrate.schematic', () => { }); it('should run successfully', async () => { - const { os, childProcess, context, schematic } = await setupTest(); + const { os, childProcess, context, schematic } = await setupTest({ + sourceRoot: 'sourceRoot', + }); const tree = new UnitTestTree(Tree.empty()); const options = { + from: '29.1.0', sourceRoot: 'sourceRoot', }; @@ -80,10 +126,20 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).not.toHaveBeenCalled(); }); + it('should noop if the current version is current', async () => { + const { context, schematic } = await setupTest(); + const tree = new UnitTestTree(Tree.empty()); + await schematic({})(tree, context); + expect(context.logger.info).toHaveBeenCalledWith( + `✅ Already on AG Grid ${UPDATE_TO_VERSION}. No migration needed.`, + ); + }); + it('should run migrate command on win32', async () => { - const { os, childProcess, context, schematic } = await setupTest(); - childProcess.spawnSync.mockReturnValueOnce({ - stdout: { toString: () => 'file.ts' }, + const { os, childProcess, context, schematic } = await setupTest({ + fileList: 'file.ts', + sourceRoot: 'sourceRoot', + startingVersion: '29.1.0', }); os.platform.mockReturnValue('win32'); @@ -105,18 +161,24 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).toHaveBeenCalled(); expect(childProcess.spawnSync).toHaveBeenCalledWith( 'npm.cmd', - ['install', '--no-save', `@ag-grid-community/cli@${UPDATE_TO_VERSION}`], + ['install', '--no-save', `@ag-grid-devtools/cli@~${UPDATE_TO_MIGRATION}`], { stdio: 'ignore', windowsVerbatimArguments: true, }, ); + expect(childProcess.spawnSync).toHaveBeenCalledWith( + 'npm.cmd', + ['remove', `@ag-grid-devtools/cli`], + { stdio: 'ignore' }, + ); }); it('should run migrate command on non-win32 machines', async () => { - const { os, childProcess, context, schematic } = await setupTest(); - childProcess.spawnSync.mockReturnValueOnce({ - stdout: { toString: () => 'file.ts' }, + const { os, childProcess, context, schematic } = await setupTest({ + fileList: 'file.ts', + sourceRoot: 'sourceRoot', + startingVersion: '29.1.0', }); const tree = new UnitTestTree(Tree.empty()); @@ -137,18 +199,63 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).toHaveBeenCalled(); expect(childProcess.spawnSync).toHaveBeenCalledWith( 'npm', - ['install', '--no-save', `@ag-grid-community/cli@${UPDATE_TO_VERSION}`], + ['install', '--no-save', `@ag-grid-devtools/cli@~${UPDATE_TO_MIGRATION}`], { stdio: 'ignore', windowsVerbatimArguments: true, }, ); + expect(childProcess.spawnSync).toHaveBeenCalledWith( + 'npm', + ['remove', `@ag-grid-devtools/cli`], + { stdio: 'ignore' }, + ); + }); + + it('should run migrate command with debug', async () => { + const { os, childProcess, context, schematic } = await setupTest({ + fileList: 'file.ts', + sourceRoot: 'sourceRoot', + startingVersion: '29.1.0', + debug: true, + }); + + const tree = new UnitTestTree(Tree.empty()); + tree.create('file.ts', 'content ag-grid'); + const options = { + sourceRoot: 'sourceRoot', + }; + + await schematic(options)(tree, context); + + expect(context.logger.info).toHaveBeenCalledWith( + '🏁 Migrating AG Grid code in sourceRoot...', + ); + expect(childProcess.spawnSync).toHaveBeenCalledWith('git', [ + 'ls-files', + 'sourceRoot/**/*.ts', + ]); + expect(os.platform).toHaveBeenCalled(); + expect(childProcess.spawnSync).toHaveBeenCalledWith( + 'npm', + ['install', '--no-save', `@ag-grid-devtools/cli@~${UPDATE_TO_MIGRATION}`], + { + stdio: 'ignore', + windowsVerbatimArguments: true, + }, + ); + expect(childProcess.spawnSync).toHaveBeenCalledWith( + 'npm', + ['remove', `@ag-grid-devtools/cli`], + { stdio: 'ignore' }, + ); }); it('should not run if no files match', async () => { - const { os, childProcess, context, schematic } = await setupTest(); - childProcess.spawnSync.mockReturnValueOnce({ - stdout: { toString: () => 'file.ts' }, + const { os, childProcess, context, schematic } = await setupTest({ + fileList: 'file.ts', + sourceRoot: 'sourceRoot', + startingVersion: '29.1.0', }); const tree = new UnitTestTree(Tree.empty()); @@ -170,4 +277,20 @@ describe('ag-grid-migrate.schematic', () => { ); expect(os.platform).not.toHaveBeenCalled(); }); + + it('should not run if unable to read previous version', async () => { + const { childProcess, context, schematic } = await setupTest(); + + const tree = new UnitTestTree(Tree.empty()); + const options = { + sourceRoot: 'sourceRoot', + }; + childProcess.spawnSync.mockImplementation(() => { + throw new Error('error'); + }); + + await schematic(options)(tree, context); + + expect(context.logger.info).not.toHaveBeenCalled(); + }); }); diff --git a/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.ts b/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.ts index 08cc962589..79c332090d 100644 --- a/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.ts +++ b/libs/components/packages/src/schematics/ag-grid-migrate/ag-grid-migrate.schematic.ts @@ -5,15 +5,43 @@ import { platform } from 'os'; import { Schema } from './schema'; -const AG_GRID_MIGRATIONS = ['31.0.0', '31.1.0', '31.2.0']; -const AG_GRID_VERSION = AG_GRID_MIGRATIONS.slice().pop(); +const AG_GRID_MIGRATION = '31.3.0'; +const AG_GRID_VERSION = '31.3.4'; + +function getStartingVersion(sourceRoot: string): string | undefined { + try { + const content = spawnSync( + 'git', + // eslint-disable-next-line @cspell/spellchecker + ['cat-file', '--textconv', `HEAD:${sourceRoot}/package-lock.json`], + { + encoding: 'utf-8', + stdio: 'pipe', + }, + ); + const packageJson = JSON.parse(content.stdout); + return packageJson.packages?.['node_modules/ag-grid-community']?.version; + } catch (e) { + return undefined; + } +} export default function (options: Schema): Rule { return (tree: Tree, context: SchematicContext) => { let { sourceRoot } = options; sourceRoot ||= '.'; - context.logger.info(`🏁 Migrating AG Grid code in ${sourceRoot}...`); + const startingVersion = options.from ?? getStartingVersion(sourceRoot); + if (!startingVersion) { + return; + } + if (startingVersion === AG_GRID_VERSION) { + context.logger.info( + `✅ Already on AG Grid ${AG_GRID_VERSION}. No migration needed.`, + ); + return; + } + context.logger.info(`🏁 Migrating AG Grid code in ${sourceRoot}...`); const files = spawnSync('git', ['ls-files', `${sourceRoot}/**/*.ts`]) .stdout.toString() .split('\n') @@ -35,31 +63,29 @@ export default function (options: Schema): Rule { const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'; spawnSync( npm, - ['install', '--no-save', `@ag-grid-community/cli@${AG_GRID_VERSION}`], + ['install', '--no-save', `@ag-grid-devtools/cli@~${AG_GRID_MIGRATION}`], { stdio: 'ignore', windowsVerbatimArguments: true, }, ); - for (const migration of AG_GRID_MIGRATIONS) { - const cmdArgs = [ - 'node_modules/@ag-grid-community/cli/index.cjs', - 'migrate', - `--to=${migration}`, - '--allow-dirty', - ...agGridFiles, - ]; - context.logger.info(``); - context.logger.info(`⏳ Migrating to AG Grid ${migration}`); - context.logger.info(``); - spawnSync('node', cmdArgs, { - shell: true, - stdio: 'inherit', - windowsVerbatimArguments: true, - argv0: 'npx', - }); - } - spawnSync(npm, ['remove', `@ag-grid-community/cli`], { + const cmdArgs = [ + 'node_modules/@ag-grid-devtools/cli/index.cjs', + 'migrate', + `--from=${startingVersion}`, + `--to=${AG_GRID_MIGRATION}`, + '--allow-dirty', + ...agGridFiles, + ]; + context.logger.info(`⏳ Migrating to AG Grid ${AG_GRID_VERSION}...`); + const output = context.debug ? 'inherit' : 'ignore'; + spawnSync('node', cmdArgs, { + shell: true, + stdio: ['ignore', output, output], + windowsVerbatimArguments: true, + argv0: 'npx', + }); + spawnSync(npm, ['remove', `@ag-grid-devtools/cli`], { stdio: 'ignore', }); context.logger.info( diff --git a/libs/components/packages/src/schematics/ag-grid-migrate/schema.json b/libs/components/packages/src/schematics/ag-grid-migrate/schema.json index 1d2f277583..abbec4cef5 100644 --- a/libs/components/packages/src/schematics/ag-grid-migrate/schema.json +++ b/libs/components/packages/src/schematics/ag-grid-migrate/schema.json @@ -4,7 +4,8 @@ "properties": { "sourceRoot": { "type": "string", - "description": "The path to the source files." + "description": "The path to the source files.", + "default": "./" } }, "required": ["sourceRoot"] diff --git a/libs/components/packages/src/schematics/ag-grid-migrate/schema.ts b/libs/components/packages/src/schematics/ag-grid-migrate/schema.ts index f62cd3d55c..8ab7fa75a2 100644 --- a/libs/components/packages/src/schematics/ag-grid-migrate/schema.ts +++ b/libs/components/packages/src/schematics/ag-grid-migrate/schema.ts @@ -3,7 +3,11 @@ */ export interface Schema { /** - * The name of the project to add polyfills to. + * Path to the source root of the project. Defaults to the current directory. */ - sourceRoot: string; + sourceRoot?: string; + /** + * The version of AG Grid to migrate from. Defaults to the version found in the project's package-lock.json. + */ + from?: string; } diff --git a/libs/components/packages/src/schematics/migrations/migration-collection.json b/libs/components/packages/src/schematics/migrations/migration-collection.json index 64d34374b7..d79d63f439 100644 --- a/libs/components/packages/src/schematics/migrations/migration-collection.json +++ b/libs/components/packages/src/schematics/migrations/migration-collection.json @@ -59,6 +59,11 @@ "version": "10.32.0", "factory": "./update-10/add-indicators-help-inline-peer-dependency/add-indicators-help-inline-peer-dependency.schematic", "description": "Add @skyux/help-inline peer dependency for @skyux/indicators." + }, + "add-help-inline-popovers-peer-dependency": { + "version": "10.37.1", + "factory": "./update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic", + "description": "Add @skyux/popovers peer dependency for @skyux/help-inline." } } } diff --git a/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.spec.ts b/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.spec.ts new file mode 100644 index 0000000000..7e1cd8e9e4 --- /dev/null +++ b/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.spec.ts @@ -0,0 +1,78 @@ +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; + +import { join } from 'path'; + +import { createTestApp } from '../../../testing/scaffold'; + +describe('Migrations > add help-inline/popovers peer dependency', () => { + const runner = new SchematicTestRunner( + 'migrations', + join(__dirname, '../../migration-collection.json'), + ); + + async function setupTest() { + const tree = await createTestApp(runner, { + projectName: 'my-app', + }); + + return { + runSchematic: () => + runner.runSchematic( + 'add-help-inline-popovers-peer-dependency', + {}, + tree, + ), + tree, + }; + } + + it('should add @skyux/popovers if @skyux/help-inline is installed in dependencies', async () => { + const { runSchematic, tree } = await setupTest(); + + tree.overwrite( + '/package.json', + '{"dependencies": {"@skyux/help-inline": "10.26.0"}}', + ); + + await runSchematic(); + + expect(tree.readJson('/package.json')).toEqual({ + dependencies: { + '@skyux/help-inline': '10.26.0', + '@skyux/popovers': '10.26.0', + }, + }); + }); + + it('should add @skyux/popovers if @skyux/help-inline is installed in devDependencies', async () => { + const { runSchematic, tree } = await setupTest(); + + tree.overwrite( + '/package.json', + '{"devDependencies": {"@skyux/help-inline": "10.26.0"}}', + ); + + await runSchematic(); + + expect(tree.readJson('/package.json')).toEqual({ + dependencies: { + '@skyux/popovers': '10.26.0', + }, + devDependencies: { + '@skyux/help-inline': '10.26.0', + }, + }); + }); + + it('should not add @skyux/popovers if @skyux/help-inline is not installed', async () => { + const { runSchematic, tree } = await setupTest(); + + tree.overwrite('/package.json', '{"dependencies": {}}'); + + await runSchematic(); + + expect(tree.readJson('/package.json')).toEqual({ + dependencies: {}, + }); + }); +}); diff --git a/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.ts b/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.ts new file mode 100644 index 0000000000..62afc204d0 --- /dev/null +++ b/libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.ts @@ -0,0 +1,14 @@ +import { Rule } from '@angular-devkit/schematics'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; + +import { ensurePeersInstalled } from '../../../rules/ensure-peers-installed'; + +export default function (): Rule { + return ensurePeersInstalled('@skyux/help-inline', [ + { + matchVersion: true, + name: '@skyux/popovers', + type: NodeDependencyType.Default, + }, + ]); +} diff --git a/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.spec.ts b/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.spec.ts index a9e0d2d3e8..876fc77502 100644 --- a/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.spec.ts +++ b/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.spec.ts @@ -5,7 +5,7 @@ import fs from 'fs-extra'; import { joinPathFragments } from 'nx/src/utils/path'; import { workspaceRoot } from 'nx/src/utils/workspace-root'; -const UPDATE_TO_VERSION = '31.2.0'; +const UPDATE_TO_VERSION = '31.3.4'; describe('ag-grid.schematic', () => { const runner = new SchematicTestRunner( @@ -70,7 +70,7 @@ describe('ag-grid.schematic', () => { await runner.runSchematic('ag-grid', {}, tree); expect(JSON.parse(tree.readText('/package.json'))).toEqual({ dependencies: { - 'ag-grid-community': `~${UPDATE_TO_VERSION}`, + 'ag-grid-community': `^${UPDATE_TO_VERSION}`, 'ag-grid-angular': UPDATE_TO_VERSION, }, }); diff --git a/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.ts b/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.ts index 94df8665f8..fac4e32a50 100644 --- a/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.ts +++ b/libs/components/packages/src/schematics/migrations/update-10/ag-grid/ag-grid.schematic.ts @@ -19,7 +19,7 @@ const AG_GRID_ENT = 'ag-grid-enterprise'; const AG_GRID_NG = 'ag-grid-angular'; const AG_GRID_SKY = '@skyux/ag-grid'; -const AG_GRID_VERSION = '~31.2.0'; +const AG_GRID_VERSION = '^31.3.4'; /** * Check package.json for AG Grid dependencies. diff --git a/libs/components/pages/package.json b/libs/components/pages/package.json index 361744f42c..af76d7344f 100644 --- a/libs/components/pages/package.json +++ b/libs/components/pages/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/router": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/router": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", "@skyux/indicators": "0.0.0-PLACEHOLDER", diff --git a/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.spec.ts b/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.spec.ts index 8e4aefde56..7e88e13b24 100644 --- a/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.spec.ts +++ b/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.spec.ts @@ -1,3 +1,5 @@ +import { CSP_NONCE, Provider } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { expect } from '@skyux-sdk/testing'; import { SkyPageThemeAdapterService } from './page-theme-adapter.service'; @@ -5,19 +7,41 @@ import { SkyPageThemeAdapterService } from './page-theme-adapter.service'; describe('Page theme service', () => { let pageThemeSvc!: SkyPageThemeAdapterService; - function getHeadStyleCount(): number { - return document.head.querySelectorAll('style').length; + function setupTest(nonce?: string): void { + const providers: Provider[] = [SkyPageThemeAdapterService]; + + if (nonce) { + providers.push({ + provide: CSP_NONCE, + useValue: nonce, + }); + } + + TestBed.configureTestingModule({ + providers, + }); + + pageThemeSvc = TestBed.inject(SkyPageThemeAdapterService); } - beforeEach(() => { - pageThemeSvc = new SkyPageThemeAdapterService(document); - }); + function getHeadStyleCount(nonce?: string): number { + const styleEls = document.head.querySelectorAll('style'); + + if (nonce) { + return Array.from(styleEls).filter((styleEl) => styleEl.nonce === nonce) + .length; + } + + return styleEls.length; + } afterEach(() => { pageThemeSvc.removeTheme(); }); it('should not add the theme stylesheet twice', () => { + setupTest(); + const styleCount = getHeadStyleCount(); pageThemeSvc.addTheme(); @@ -27,6 +51,8 @@ describe('Page theme service', () => { }); it('should not remove the theme stylesheet twice', () => { + setupTest(); + pageThemeSvc.addTheme(); const styleCount = getHeadStyleCount(); @@ -36,4 +62,16 @@ describe('Page theme service', () => { expect(getHeadStyleCount()).toBe(styleCount - 1); }); + + it('should include the CSP nonce on the style element', () => { + const testNonce = 'page-theme-adapter-test'; + + setupTest(testNonce); + + expect(getHeadStyleCount(testNonce)).toBe(0); + + pageThemeSvc.addTheme(); + + expect(getHeadStyleCount(testNonce)).toBe(1); + }); }); diff --git a/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.ts b/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.ts index 0ae1083be7..6227f9f921 100644 --- a/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.ts +++ b/libs/components/pages/src/lib/modules/page/page-theme-adapter.service.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable } from '@angular/core'; +import { CSP_NONCE, Inject, Injectable, inject } from '@angular/core'; /** * @internal @@ -9,6 +9,7 @@ export class SkyPageThemeAdapterService { #styleEl: HTMLStyleElement | undefined; #document: Document; + #nonce = inject(CSP_NONCE, { optional: true }); constructor(@Inject(DOCUMENT) document: Document) { this.#document = document; @@ -22,6 +23,11 @@ export class SkyPageThemeAdapterService { public addTheme(): void { if (!this.#styleEl) { this.#styleEl = this.#document.createElement('style'); + + if (this.#nonce) { + this.#styleEl.nonce = this.#nonce; + } + this.#styleEl.appendChild( this.#document.createTextNode( 'body:not(.sky-theme-modern) { background-color: #fff; }', diff --git a/libs/components/phone-field/package.json b/libs/components/phone-field/package.json index ef3e63e8d1..196f7c80d9 100644 --- a/libs/components/phone-field/package.json +++ b/libs/components/phone-field/package.json @@ -16,12 +16,12 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.html b/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.html index 4f16e50e06..4fa2a0fe89 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.html +++ b/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.html @@ -1,9 +1,9 @@ - + diff --git a/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.ts b/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.ts index 5478e1a1a4..5eb1e15f68 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/fixtures/phone-field-input-box.component.fixture.ts @@ -1,5 +1,7 @@ import { Component } from '@angular/core'; +import { SkyPhoneFieldCountry } from '../types/country'; + @Component({ selector: 'sky-test-cmp', templateUrl: './phone-field-input-box.component.fixture.html', @@ -7,4 +9,5 @@ import { Component } from '@angular/core'; export class PhoneFieldInputBoxTestComponent { public hintText: string | undefined; public modelValue: string | undefined; + public selectedCountry: SkyPhoneFieldCountry | undefined; } diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field-adapter.service.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field-adapter.service.ts index b540202bea..da5a21a203 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field-adapter.service.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field-adapter.service.ts @@ -26,6 +26,17 @@ export class SkyPhoneFieldAdapterService implements OnDestroy { this.#renderer.addClass(elementRef.nativeElement, className); } + public getInputValue(elementRef: ElementRef): string | undefined { + const el = elementRef.nativeElement as HTMLElement | null; + + if (el && 'value' in el) { + return (el as HTMLInputElement).value; + } + + /* istanbul ignore next: safety check */ + return undefined; + } + public setElementDisabledState( elementRef: ElementRef, disabled: boolean, diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field-input.directive.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field-input.directive.ts index 364bdca496..21dcbc5605 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field-input.directive.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field-input.directive.ts @@ -1,15 +1,12 @@ -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { - AfterViewInit, - ChangeDetectorRef, Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, - Optional, - forwardRef, + booleanAttribute, + inject, } from '@angular/core'; import { AbstractControl, @@ -21,24 +18,11 @@ import { } from '@angular/forms'; import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { debounceTime, takeUntil } from 'rxjs/operators'; +import { Subject, take, takeUntil } from 'rxjs'; import { SkyPhoneFieldAdapterService } from './phone-field-adapter.service'; import { SkyPhoneFieldComponent } from './phone-field.component'; -const SKY_PHONE_FIELD_VALUE_ACCESSOR = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SkyPhoneFieldInputDirective), - multi: true, -}; - -const SKY_PHONE_FIELD_VALIDATOR = { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SkyPhoneFieldInputDirective), - multi: true, -}; - /** * Creates a button, search input, and text input for entering and validating * international phone numbers. Place this attribute on an `input` element, and wrap @@ -54,24 +38,35 @@ const SKY_PHONE_FIELD_VALIDATOR = { */ @Directive({ selector: '[skyPhoneFieldInput]', - providers: [SKY_PHONE_FIELD_VALUE_ACCESSOR, SKY_PHONE_FIELD_VALIDATOR], + providers: [ + { + provide: NG_VALIDATORS, + useExisting: SkyPhoneFieldInputDirective, + multi: true, + }, + { + provide: NG_VALUE_ACCESSOR, + useExisting: SkyPhoneFieldInputDirective, + multi: true, + }, + ], }) export class SkyPhoneFieldInputDirective - implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor, Validator + implements OnInit, OnDestroy, ControlValueAccessor, Validator { /** * Whether to disable the phone field on template-driven forms. Don't use this input on reactive forms because they may overwrite the input or leave the control out of sync. * To set the disabled state on reactive forms, use the `FormControl` instead. * @default false */ - @Input() - public set disabled(value: boolean | undefined) { - const coercedValue = coerceBooleanProperty(value); + @Input({ transform: booleanAttribute }) + public set disabled(value: boolean) { if (this.#phoneFieldComponent) { - this.#phoneFieldComponent.countrySelectDisabled = coercedValue; - this.#adapterService?.setElementDisabledState(this.#elRef, coercedValue); + this.#phoneFieldComponent.countrySelectDisabled = value; + this.#adapterSvc?.setElementDisabledState(this.#elRef, value); } - this.#_disabled = coercedValue; + + this.#_disabled = value; } public get disabled(): boolean { @@ -85,57 +80,28 @@ export class SkyPhoneFieldInputDirective * set this property to `true`. * @default false */ - @Input() - public skyPhoneFieldNoValidate: boolean | undefined = false; - - set #modelValue(value: string | undefined) { - const valueOrDefault = value ?? ''; - this.#_modelValue = valueOrDefault; - this.#adapterService?.setElementValue(this.#elRef, valueOrDefault); - - if (valueOrDefault) { - const formattedValue = this.#formatNumber(valueOrDefault.toString()); - - this.#onChange(formattedValue); - } else { - this.#onChange(valueOrDefault); - } - this.#validatorChange(); - } - - get #modelValue(): string { - return this.#_modelValue; - } + @Input({ transform: booleanAttribute }) + public skyPhoneFieldNoValidate = false; + #_disabled = false; + #_value = ''; #control: AbstractControl | undefined; - - #textChanges: BehaviorSubject | undefined; - #ngUnsubscribe = new Subject(); - + #notifyChange: ((value: string) => void) | undefined; + #notifyTouched: (() => void) | undefined; #phoneUtils = PhoneNumberUtil.getInstance(); - #_disabled!: boolean; + readonly #adapterSvc = inject(SkyPhoneFieldAdapterService, { + host: true, + optional: true, + skipSelf: true, + }); - #_modelValue = ''; - - #changeDetector: ChangeDetectorRef; - #elRef: ElementRef; - - #adapterService: SkyPhoneFieldAdapterService | undefined; - #phoneFieldComponent: SkyPhoneFieldComponent | undefined; - - constructor( - changeDetector: ChangeDetectorRef, - elRef: ElementRef, - @Optional() adapterService?: SkyPhoneFieldAdapterService, - @Optional() phoneFieldComponent?: SkyPhoneFieldComponent, - ) { - this.#changeDetector = changeDetector; - this.#elRef = elRef; - this.#adapterService = adapterService; - this.#phoneFieldComponent = phoneFieldComponent; - } + readonly #elRef = inject(ElementRef); + readonly #phoneFieldComponent = inject(SkyPhoneFieldComponent, { + host: true, + optional: true, + }); public ngOnInit(): void { if (!this.#phoneFieldComponent) { @@ -145,24 +111,24 @@ export class SkyPhoneFieldInputDirective ); } - this.#adapterService?.setElementType(this.#elRef); - this.#adapterService?.addElementClass(this.#elRef, 'sky-form-control'); - } + this.#adapterSvc?.setElementType(this.#elRef); + this.#adapterSvc?.addElementClass(this.#elRef, 'sky-form-control'); - public ngAfterViewInit(): void { this.#phoneFieldComponent?.selectedCountryChange .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe(() => { - this.#modelValue = this.#elRef.nativeElement.value; + const value = this.#adapterSvc?.getInputValue(this.#elRef); + this.#setValue(value); + this.#control?.updateValueAndValidity(); }); - // This is needed to address a bug in Angular 4, where the value is not changed on the view. - // See: https://github.com/angular/angular/issues/13792 - /* istanbul ignore else */ - if (this.#control && this.#modelValue) { - this.#control.setValue(this.#modelValue, { emitEvent: false }); - this.#changeDetector.detectChanges(); - } + this.#phoneFieldComponent.countrySearchForm + .get('countrySearch') + ?.valueChanges.pipe(takeUntil(this.#ngUnsubscribe), take(1)) + .subscribe(() => { + this.#control?.markAsDirty(); + this.#control?.markAsTouched(); + }); } public ngOnDestroy(): void { @@ -170,198 +136,168 @@ export class SkyPhoneFieldInputDirective this.#ngUnsubscribe.complete(); } - /** - * Writes the new value for reactive forms on change events on the input element - * @param event The change event that was received - */ - @HostListener('change', ['$event']) - public onInputChange(event: any): void { - if (!this.#textChanges) { - this.#setupTextChangeSubscription(event.target.value); - } else { - this.#textChanges.next(event.target.value); - } - } - - /** - * Marks reactive form controls as touched on input blur events - */ - @HostListener('blur') - public onInputBlur(): void { - this.#onTouched(); - } - - @HostListener('input', ['$event']) - public onInputTyping(event: any): void { - if (!this.#textChanges) { - this.#setupTextChangeSubscription(event.target.value); - } else { - this.#textChanges.next(event.target.value); - } - } - - /** - * Writes the new value for reactive forms - * @param value The new value for the input - */ - public writeValue(value: string): void { - this.#phoneFieldComponent?.setCountryByDialCode(value); - - this.#modelValue = value; - } - - public registerOnChange(fn: (value: string | undefined) => void): void { - this.#onChange = fn; + public registerOnChange(fn: (value: string) => void): void { + this.#notifyChange = fn; } public registerOnTouched(fn: () => void): void { - this.#onTouched = fn; - } - - public registerOnValidatorChange(fn: () => void): void { - this.#validatorChange = fn; + this.#notifyTouched = fn; } - /** - * Sets the disabled state on the input - * @param isDisabled the new value of the input's disabled state - */ public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } - /** - * Validate's the form control's current value - * @param control the form control for the input - */ public validate(control: AbstractControl): ValidationErrors | null { - if (!this.#control) { - this.#control = control; - } - - if (this.skyPhoneFieldNoValidate) { - return null; - } + this.#control ??= control; const value = control.value; - if (!value) { + if (!value || this.skyPhoneFieldNoValidate) { return null; } - if ( - this.#phoneFieldComponent?.selectedCountry && - !this.#validateNumber(value) - ) { - if (!this.#textChanges) { - // Mark the invalid control as touched so that the input's invalid CSS styles appear. - // (This is only required when the invalid value is set by the FormControl constructor.) - // We don't do this if the input is the active element so that we don't show validation - // errors unless it is invalid on initialization or the input has been blurred. - control.markAsTouched(); - } - + if (!this.#isValidPhoneNumber(value)) { return { skyPhoneField: { invalid: value, }, }; } + return null; } - #setupTextChangeSubscription(text: string): void { - this.#textChanges = new BehaviorSubject(text); + public writeValue(value: unknown): void { + const rawValue = typeof value === 'string' ? value : ''; + + this.#phoneFieldComponent?.setCountryByDialCode(rawValue); + this.#adapterSvc?.setElementValue(this.#elRef, rawValue); + + this.#setValue(rawValue); + const newValue = this.#getValue(); + + if (rawValue !== newValue) { + // If the value is set before the control is initialized, wait for the + // first cycle to complete before triggering a value change event. + // (This occurs when the control is initialized with an unformatted value + // but is formatted into a new value immediately in the `writeValue` + // method.) + if (!this.#control) { + setTimeout(() => { + this.#notifyChange?.(newValue); + }); + } else { + this.#notifyChange?.(newValue); + } + } + } - this.#textChanges - .pipe(debounceTime(500), takeUntil(this.#ngUnsubscribe)) - .subscribe((newValue) => { - this.writeValue(newValue); - this.#changeDetector.markForCheck(); - }); + @HostListener('blur') + protected onBlur(): void { + this.#notifyTouched?.(); } - #validateNumber(phoneNumber: string): boolean { - try { - const numberObj = this.#phoneUtils.parseAndKeepRawInput( - phoneNumber, - this.#phoneFieldComponent?.selectedCountry?.iso2, - ); + @HostListener('change') + protected onChange(): void { + const value = this.#adapterSvc?.getInputValue(this.#elRef); + this.#setValue(value); + this.#notifyChange?.(this.#getValue()); + } - if ( - this.#phoneFieldComponent && - !this.#phoneFieldComponent.allowExtensions && - numberObj.getExtension() - ) { - return false; - } + @HostListener('input') + protected onInput(): void { + const value = this.#adapterSvc?.getInputValue(this.#elRef); + this.#phoneFieldComponent?.setCountryByDialCode(value); + } - return this.#phoneUtils.isValidNumberForRegion( - numberObj, - this.#phoneFieldComponent?.selectedCountry?.iso2, - ); - } catch (e) { - return false; + #formatPhoneNumber(value: string | undefined): string | undefined { + if (!value) { + return; } - } - /** - * Format's the given phone number based on the currently selected country. - * @param phoneNumber The number to format - */ - #formatNumber(phoneNumber: string): string { + const defaultCountry = this.#getDefaultCountry(); + const regionCode = this.#getRegionCode(); + const returnFormat = this.#phoneFieldComponent?.returnFormat; + try { - const numberObj = this.#phoneUtils.parseAndKeepRawInput( - phoneNumber, - this.#phoneFieldComponent?.selectedCountry?.iso2, + const phoneNumber = this.#phoneUtils.parseAndKeepRawInput( + value, + regionCode ?? defaultCountry, ); - if (this.#phoneUtils.isPossibleNumber(numberObj)) { - switch (this.#phoneFieldComponent?.returnFormat) { + + if (this.#phoneUtils.isPossibleNumber(phoneNumber)) { + switch (returnFormat) { case 'international': return this.#phoneUtils.format( - numberObj, + phoneNumber, PhoneNumberFormat.INTERNATIONAL, ); + case 'national': return this.#phoneUtils.format( - numberObj, + phoneNumber, PhoneNumberFormat.NATIONAL, ); + case 'default': default: - if ( - this.#phoneFieldComponent?.selectedCountry?.iso2 !== - this.#phoneFieldComponent?.defaultCountry - ) { - return this.#phoneUtils.format( - numberObj, - PhoneNumberFormat.INTERNATIONAL, - ); - } else { - return this.#phoneUtils.format( - numberObj, - PhoneNumberFormat.NATIONAL, - ); - } + return regionCode && regionCode !== defaultCountry + ? this.#phoneUtils.format( + phoneNumber, + PhoneNumberFormat.INTERNATIONAL, + ) + : this.#phoneUtils.format( + phoneNumber, + PhoneNumberFormat.NATIONAL, + ); } - } else { - return phoneNumber; } - } catch (e) { - /* sanity check */ - /* istanbul ignore next */ - return phoneNumber; + } catch (err) { + /* */ } + + return; } - // eslint-disable-next-line @typescript-eslint/no-empty-function , @typescript-eslint/no-unused-vars - #onChange = (_: string | undefined) => {}; + #getDefaultCountry(): string | undefined { + return this.#phoneFieldComponent?.defaultCountry; + } - // istanbul ignore next - // eslint-disable-next-line @typescript-eslint/no-empty-function - #onTouched = () => {}; + #getRegionCode(): string | undefined { + return this.#phoneFieldComponent?.selectedCountry?.iso2; + } + + #getValue(): string { + return this.#_value; + } - // istanbul ignore next - // eslint-disable-next-line @typescript-eslint/no-empty-function - #validatorChange = () => {}; + #isValidPhoneNumber(value: string): boolean { + const defaultCountry = this.#getDefaultCountry(); + const regionCode = this.#getRegionCode() ?? defaultCountry; + const allowExtensions = !!this.#phoneFieldComponent?.allowExtensions; + + try { + const phoneNumber = this.#phoneUtils.parseAndKeepRawInput( + value, + regionCode, + ); + + if (!allowExtensions && phoneNumber.getExtension()) { + return false; + } + + return this.#phoneUtils.isValidNumberForRegion(phoneNumber, regionCode); + } catch (e) { + return false; + } + } + + #setValue(value: string | undefined): void { + /* istanbul ignore else */ + if (value !== undefined) { + const formatted = this.#formatPhoneNumber(value); + this.#_value = formatted ?? value; + } + } } diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html index ab33d3360a..3aec627cd6 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html @@ -1,10 +1,10 @@ - +@if (!inputBoxHostSvc) {
    - - - + + +
    -
    +}
    - - - - - - + @if (phoneInputShown) { + + + + } @else if (countrySearchShown) { + + + + } -
    - -
    + +
    + }
    diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts index 6a3c2e0c68..69214c4c13 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts @@ -147,22 +147,29 @@ describe('Phone Field Component', () => { | PhoneFieldReactiveTestComponent | PhoneFieldInputBoxTestComponent >, + programmaticIso2 = '', ): void { - const countryInput = getCountrySearchToggleButton(compFixture); - countryInput.click(); - detectChangesAndTick(compFixture); + if (programmaticIso2) { + compFixture.componentInstance.selectedCountry = { + iso2: programmaticIso2, + }; + } else { + const countryInput = getCountrySearchToggleButton(compFixture); + countryInput.click(); + detectChangesAndTick(compFixture); - const countrySearchInput = getCountrySearchInput(compFixture); + const countrySearchInput = getCountrySearchInput(compFixture); - countrySearchInput.value = countryName; + countrySearchInput.value = countryName; - SkyAppTestUtility.fireDomEvent(countrySearchInput, 'input'); - detectChangesAndTick(compFixture); + SkyAppTestUtility.fireDomEvent(countrySearchInput, 'input'); + detectChangesAndTick(compFixture); - SkyAppTestUtility.fireDomEvent( - document.querySelector('.sky-autocomplete-result:first-child'), - 'click', - ); + SkyAppTestUtility.fireDomEvent( + document.querySelector('.sky-autocomplete-result:first-child'), + 'click', + ); + } detectChangesAndTick(compFixture); } @@ -210,8 +217,8 @@ describe('Phone Field Component', () => { } function validateInputAndModel( - modelValue: string, - formattedValue: string, + inputValue: string, + modelValue: string | undefined, isValid: boolean, isTouched: boolean, model: NgModel | UntypedFormControl | undefined, @@ -222,9 +229,9 @@ describe('Phone Field Component', () => { >, ): void { fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('input').value).toBe(modelValue); + expect(fixture.nativeElement.querySelector('input').value).toBe(inputValue); - expect(model?.value).toBe(formattedValue); + expect(model?.value).toBe(modelValue); expect(model?.valid).toBe(isValid); @@ -233,7 +240,7 @@ describe('Phone Field Component', () => { } else { expect(model?.errors).toEqual({ skyPhoneField: { - invalid: formattedValue, + invalid: modelValue, }, }); } @@ -539,7 +546,7 @@ describe('Phone Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); - validateInputAndModel('1234', '1234', false, true, ngModel, fixture); + validateInputAndModel('1234', '1234', false, false, ngModel, fixture); blurInput(fixture.nativeElement, fixture, true); @@ -558,7 +565,7 @@ describe('Phone Field Component', () => { '667-555-530', '667-555-530', false, - true, + false, ngModel, fixture, ); @@ -615,7 +622,7 @@ describe('Phone Field Component', () => { fixture.detectChanges(); await fixture.whenStable(); - validateInputAndModel('1234', '1234', false, true, ngModel, fixture); + validateInputAndModel('1234', '1234', false, false, ngModel, fixture); }); it('should validate properly when input changed to empty string', async () => { @@ -702,7 +709,7 @@ describe('Phone Field Component', () => { '667-555-5309ext3', '(667) 555-5309 ext. 3', false, - true, + false, ngModel, fixture, ); @@ -826,7 +833,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, ngModel, @@ -867,7 +874,7 @@ describe('Phone Field Component', () => { '+3556675555309', '+3556675555309', false, - true, + false, ngModel, fixture, ); @@ -1238,7 +1245,7 @@ describe('Phone Field Component', () => { '+12045555555', '+1 204-555-5555', true, - true, + false, ngModel, fixture, ); @@ -1267,7 +1274,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, ngModel, @@ -1287,6 +1294,49 @@ describe('Phone Field Component', () => { ); })); + it('should validate correctly after country is changed programmatically', fakeAsync(() => { + fixture.detectChanges(); + const inputElement = fixture.debugElement.query(By.css('input')); + const ngModel = inputElement.injector.get(NgModel); + + component.defaultCountry = 'us'; + fixture.detectChanges(); + component.modelValue = '6675555309'; + detectChangesAndTick(fixture); + + validateInputAndModel( + '6675555309', + '(667) 555-5309', + true, + false, + ngModel, + fixture, + ); + + setCountry('Albania', fixture, 'al'); + + validateInputAndModel( + '6675555309', + '(667) 555-5309', + false, + false, + ngModel, + fixture, + ); + + component.modelValue = '024569874'; + detectChangesAndTick(fixture); + + validateInputAndModel( + '024569874', + '+355 24 569 874', + true, + false, + ngModel, + fixture, + ); + })); + it('should add the country code to non-default country data', fakeAsync(() => { fixture.detectChanges(); const inputElement = fixture.debugElement.query(By.css('input')); @@ -1297,7 +1347,7 @@ describe('Phone Field Component', () => { fixture.detectChanges(); tick(); - validateInputAndModel('', '', true, false, ngModel, fixture); + validateInputAndModel('', undefined, true, false, ngModel, fixture); setCountry('Albania', fixture); @@ -1308,7 +1358,7 @@ describe('Phone Field Component', () => { '024569874', '+355 24 569 874', true, - false, + true, ngModel, fixture, ); @@ -1606,7 +1656,7 @@ describe('Phone Field Component', () => { '1234', '1234', false, - true, + false, component.phoneControl, fixture, ); @@ -1632,7 +1682,7 @@ describe('Phone Field Component', () => { '667-555-530', '667-555-530', false, - true, + false, component.phoneControl, fixture, ); @@ -1686,7 +1736,7 @@ describe('Phone Field Component', () => { '1234', '1234', false, - true, + false, component.phoneControl, fixture, ); @@ -1778,7 +1828,7 @@ describe('Phone Field Component', () => { '667-555-5309ext3', '(667) 555-5309 ext. 3', false, - true, + false, component.phoneControl, fixture, ); @@ -1793,7 +1843,7 @@ describe('Phone Field Component', () => { '8675558309', '(867) 555-8309', false, - true, + false, component.phoneControl, fixture, ); @@ -1936,7 +1986,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, component.phoneControl, @@ -1974,7 +2024,7 @@ describe('Phone Field Component', () => { '+3556675555309', '+3556675555309', false, - true, + false, component.phoneControl, fixture, ); @@ -2144,7 +2194,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, component.phoneControl, @@ -2164,6 +2214,46 @@ describe('Phone Field Component', () => { ); })); + it('should validate correctly after country is changed programmatically', fakeAsync(() => { + fixture.detectChanges(); + component.defaultCountry = 'us'; + fixture.detectChanges(); + component.phoneControl?.setValue('6675555309'); + detectChangesAndTick(fixture); + + validateInputAndModel( + '6675555309', + '(667) 555-5309', + true, + false, + component.phoneControl, + fixture, + ); + + setCountry('Albania', fixture, 'al'); + + validateInputAndModel( + '6675555309', + '(667) 555-5309', + false, + false, + component.phoneControl, + fixture, + ); + + component.phoneControl?.setValue('024569874'); + detectChangesAndTick(fixture); + + validateInputAndModel( + '024569874', + '+355 24 569 874', + true, + false, + component.phoneControl, + fixture, + ); + })); + it('should add the country code to non-default country data', fakeAsync(() => { fixture.detectChanges(); component.defaultCountry = 'us'; @@ -2178,7 +2268,7 @@ describe('Phone Field Component', () => { '024569874', '+355 24 569 874', true, - false, + true, component.phoneControl, fixture, ); diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts index e58cf4d4d8..63275a1990 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts @@ -43,6 +43,8 @@ import { SkyPhoneFieldAdapterService } from './phone-field-adapter.service'; import { SkyPhoneFieldCountry } from './types/country'; import { SkyPhoneFieldNumberReturnFormat } from './types/number-return-format'; +const DEFAULT_COUNTRY_CODE = 'us'; + // NOTE: The no-op animation is here in order to block the input's "fade in" animation // from firing on initial load. For more information on this technique you can see // https://www.bennadel.com/blog/3417-using-no-op-transitions-to-prevent-animation-during-the-initial-render-of-ngfor-in-angular-5-2-6.htm @@ -159,16 +161,14 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { */ @Input() public set defaultCountry(value: string | undefined) { - if (value && value !== this.#_defaultCountry) { - this.#_defaultCountry = value.toLowerCase(); - - this.#defaultCountryData = this.countries.find( - (country) => country.iso2 === this.#_defaultCountry, - ); + if (value !== this.#_defaultCountry) { + value ??= DEFAULT_COUNTRY_CODE; + this.#_defaultCountry = value.toLocaleLowerCase(); + this.#defaultCountryData = this.#getDefaultCountryData(); } } - public get defaultCountry(): string | undefined { + public get defaultCountry(): string { return this.#_defaultCountry; } @@ -199,7 +199,6 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { * Emits a `SkyPhoneFieldCountry` object when the selected country in the country search * input changes. */ - @Output() public selectedCountryChange = new EventEmitter(); @@ -234,6 +233,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { newCountry.iso2, PhoneNumberType.FIXED_LINE, ); + this.#_selectedCountry.exampleNumber = this.#phoneUtils.format( numberObj, PhoneNumberFormat.NATIONAL, @@ -241,7 +241,6 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { } this.#populateInputBoxHelpText(); - this.selectedCountryChange.emit(this.#_selectedCountry); } } @@ -278,7 +277,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { #longestDialCodeLength = 0; - #_defaultCountry: string | undefined; + #_defaultCountry = DEFAULT_COUNTRY_CODE; #_selectedCountry: SkyPhoneFieldCountry | undefined; @@ -314,6 +313,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { // eslint-disable-next-line @typescript-eslint/no-explicit-any JSON.stringify((window as any).intlTelInputGlobals.getCountryData()), ); + for (const country of this.countries) { country.dialCode = '+' + country.dialCode; @@ -322,6 +322,8 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { } } + this.#defaultCountryData = this.#getDefaultCountryData(); + this.countrySearchForm = this.#formBuilder.group({ countrySearch: this.#countrySearchFormControl, }); @@ -347,13 +349,8 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { }); } - if (!this.defaultCountry) { - this.defaultCountry = 'us'; - } + this.selectedCountry ??= this.#defaultCountryData; - if (!this.selectedCountry) { - this.selectedCountry = this.#defaultCountryData; - } this.#changeDetector.markForCheck(); }, 0); @@ -428,7 +425,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { this.#changeDetector.markForCheck(); } - public setCountryByDialCode(phoneNumberRaw: string): boolean { + public setCountryByDialCode(phoneNumberRaw: string | undefined): boolean { if (!phoneNumberRaw || !phoneNumberRaw.startsWith('+')) { return false; } @@ -495,6 +492,12 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { return false; } + #getDefaultCountryData(): SkyPhoneFieldCountry | undefined { + return this.countries.find( + (country) => country.iso2 === this.#_defaultCountry, + ); + } + #populateInputBoxHelpText(): void { if (this.inputBoxHostSvc && this.inputTemplateRef) { this.inputBoxHostSvc?.setHintText( diff --git a/libs/components/popovers/package.json b/libs/components/popovers/package.json index 8317b50870..0dec1644c1 100644 --- a/libs/components/popovers/package.json +++ b/libs/components/popovers/package.json @@ -16,11 +16,11 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/progress-indicator/package.json b/libs/components/progress-indicator/package.json index 2ee7578b2f..947bc46fb4 100644 --- a/libs/components/progress-indicator/package.json +++ b/libs/components/progress-indicator/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/help-inline": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/router/package.json b/libs/components/router/package.json index 6beddfe0fe..6a5d9db5ff 100644 --- a/libs/components/router/package.json +++ b/libs/components/router/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/router": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/router": "^17.3.12", "@skyux/config": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER" }, diff --git a/libs/components/router/testing/src/href/fixtures/href-harness-test.component.html b/libs/components/router/testing/src/href/fixtures/href-harness-test.component.html index b5f7e0202b..e1250c7e98 100644 --- a/libs/components/router/testing/src/href/fixtures/href-harness-test.component.html +++ b/libs/components/router/testing/src/href/fixtures/href-harness-test.component.html @@ -6,4 +6,7 @@ >Link 1 Link 2 + Link 3

    diff --git a/libs/components/router/testing/src/href/fixtures/href-harness-test.component.ts b/libs/components/router/testing/src/href/fixtures/href-harness-test.component.ts index b81a332eb4..1ce3d43935 100644 --- a/libs/components/router/testing/src/href/fixtures/href-harness-test.component.ts +++ b/libs/components/router/testing/src/href/fixtures/href-harness-test.component.ts @@ -4,4 +4,6 @@ import { Component } from '@angular/core'; selector: 'sky-href-harness-test', templateUrl: './href-harness-test.component.html', }) -export class HrefHarnessTestComponent {} +export class HrefHarnessTestComponent { + protected baseHref = 'my-base-href'; +} diff --git a/libs/components/router/testing/src/href/href-harness.spec.ts b/libs/components/router/testing/src/href/href-harness.spec.ts index b390928e0a..9d3c7394ef 100644 --- a/libs/components/router/testing/src/href/href-harness.spec.ts +++ b/libs/components/router/testing/src/href/href-harness.spec.ts @@ -80,4 +80,15 @@ describe('SkyHrefHarness', () => { await expectAsync(hrefHarness.getText()).toBeResolvedTo(''); await expectAsync(hrefHarness.isVisible()).toBeResolvedTo(false); }); + + it('should find elements when skyHref is bound to a variable', async () => { + const { hrefHarness } = await setupTest({ + dataSkyId: 'my-href-3', + userHasAccess: true, + }); + + await expectAsync(hrefHarness.getHref()).toBeResolvedTo( + 'https://example.com/my-base-href', + ); + }); }); diff --git a/libs/components/router/testing/src/href/href-harness.ts b/libs/components/router/testing/src/href/href-harness.ts index 9e700e1a49..729fea7058 100644 --- a/libs/components/router/testing/src/href/href-harness.ts +++ b/libs/components/router/testing/src/href/href-harness.ts @@ -7,7 +7,7 @@ import { SkyHrefHarnessFilters } from './href-harness-filters'; * Allows interaction with a SkyHref directive during testing. */ export class SkyHrefHarness extends SkyComponentHarness { - public static hostSelector = '[skyHref]'; + public static hostSelector = '.sky-href'; /** * Gets a `HarnessPredicate` that can be used to search for a diff --git a/libs/components/select-field/package.json b/libs/components/select-field/package.json index 5d149167f3..708a13fc1a 100644 --- a/libs/components/select-field/package.json +++ b/libs/components/select-field/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", "@skyux/indicators": "0.0.0-PLACEHOLDER", diff --git a/libs/components/split-view/package.json b/libs/components/split-view/package.json index 42b8e305cd..754d246ab8 100644 --- a/libs/components/split-view/package.json +++ b/libs/components/split-view/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/storybook/package.json b/libs/components/storybook/package.json index ebfdd69e1d..e7d3e4bcec 100644 --- a/libs/components/storybook/package.json +++ b/libs/components/storybook/package.json @@ -2,10 +2,10 @@ "name": "@skyux/storybook", "version": "0.0.1", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux/theme": "0.0.0-PLACEHOLDER", "@storybook/angular": "^8.1.10" }, diff --git a/libs/components/tabs/package.json b/libs/components/tabs/package.json index dfd81ad4d6..addfcdc9d9 100644 --- a/libs/components/tabs/package.json +++ b/libs/components/tabs/package.json @@ -16,11 +16,11 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", - "@angular/router": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", + "@angular/router": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/animations": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", diff --git a/libs/components/text-editor/package.json b/libs/components/text-editor/package.json index 626a00c2ae..68e3903d48 100644 --- a/libs/components/text-editor/package.json +++ b/libs/components/text-editor/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux/colorpicker": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html index cc92ac9f09..d3857e136a 100644 --- a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html +++ b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html @@ -92,7 +92,7 @@ {{ hintText }}
    { }); }); - it('should not display if a parent component requires label text and it is not provided', () => { - fixture = createComponent(TextEditorFixtureComponent, [ - SkyFormFieldLabelTextRequiredService, - ]); - - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - const textEditor = fixture.nativeElement.querySelector('sky-text-editor'); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(textEditor).not.toBeVisible(); - }); - describe('with ngModel', () => { let ngModel: NgModel; let testComponent: TextEditorWithNgModel; @@ -2087,7 +2069,9 @@ describe('Text editor', () => { validateIframeDocumentAttribute('aria-invalid', 'true'); validateIframeDocumentAttribute( 'aria-errormessage', - fixture.nativeElement.querySelector('sky-form-errors').id, + fixture.nativeElement.querySelector( + 'sky-form-errors.sky-text-editor-errors', + ).id, ); })); diff --git a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts index 0e98514bfe..3837e04f76 100644 --- a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts +++ b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts @@ -9,7 +9,6 @@ import { Input, NgZone, OnDestroy, - OnInit, TemplateRef, ViewChild, ViewEncapsulation, @@ -26,7 +25,6 @@ import { import { SKY_FORM_ERRORS_ENABLED, SkyFormErrorsModule, - SkyFormFieldLabelTextRequiredService, SkyInputBoxHostService, SkyRequiredStateDirective, } from '@skyux/forms'; @@ -91,7 +89,7 @@ import { SkyTextEditorToolbarActionType } from './types/toolbar-action-type'; ], }) export class SkyTextEditorComponent - implements AfterViewInit, OnInit, OnDestroy, ControlValueAccessor + implements AfterViewInit, OnDestroy, ControlValueAccessor { /** * Whether to put focus on the editor after it renders. @@ -387,9 +385,6 @@ export class SkyTextEditorComponent optional: true, }); - @HostBinding('style.display') - public display: string | undefined; - protected editorFocused = false; #defaultId: string; @@ -418,10 +413,6 @@ export class SkyTextEditorComponent readonly #sanitizationService = inject(SkyTextSanitizationService); readonly #zone = inject(NgZone); - readonly #labelTextRequired = inject(SkyFormFieldLabelTextRequiredService, { - optional: true, - }); - protected readonly errorId = this.#idSvc.generateId(); protected readonly ngControl = inject(NgControl); protected readonly requiredState = inject(SkyRequiredStateDirective); @@ -435,12 +426,6 @@ export class SkyTextEditorComponent this.#initIframe(); } - public ngOnInit(): void { - if (this.#labelTextRequired && !this.labelText) { - this.display = 'none'; - } - this.#labelTextRequired?.validateLabelText(this.labelText); - } public ngOnDestroy(): void { this.#adapterService.removeObservers(this.#editorService.editor); this.#ngUnsubscribe.next(); diff --git a/libs/components/theme/package.json b/libs/components/theme/package.json index 1373813002..1ed848ef28 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/package.json @@ -16,12 +16,12 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4" + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12" }, "dependencies": { "@blackbaud/skyux-design-tokens": "0.0.28", - "@skyux/icons": "7.3.0", + "@skyux/icons": "7.8.0", "fontfaceobserver": "2.3.0", "tslib": "^2.6.2" } diff --git a/libs/components/theme/src/index.ts b/libs/components/theme/src/index.ts index 3b108d0559..2b356838c0 100644 --- a/libs/components/theme/src/index.ts +++ b/libs/components/theme/src/index.ts @@ -4,6 +4,7 @@ export { SkyThemeIconManifestService } from './lib/icons/icon-manifest.service'; export { SkyAppStyleLoader } from './lib/style-loader'; export { SkyThemeModule } from './lib/theme.module'; export { SkyTheme } from './lib/theming/theme'; +export { SkyThemeComponentClassDirective } from './lib/theming/theme-component-class.directive'; export { SkyThemeMode } from './lib/theming/theme-mode'; export { SkyThemeSettings } from './lib/theming/theme-settings'; export { SkyThemeSettingsChange } from './lib/theming/theme-settings-change'; diff --git a/libs/components/theme/src/lib/styles/_public-api/_compat/_mixins.scss b/libs/components/theme/src/lib/styles/_public-api/_compat/_mixins.scss index f4294ace45..77873f0545 100644 --- a/libs/components/theme/src/lib/styles/_public-api/_compat/_mixins.scss +++ b/libs/components/theme/src/lib/styles/_public-api/_compat/_mixins.scss @@ -59,7 +59,26 @@ color: var(--sky-highlight-color-danger); } -@mixin sky-dropdown-item() { +@mixin sky-dropdown-item-btn { + background-color: transparent; + border: none; + color: var(--sky-text-color-default); + cursor: pointer; + display: block; + padding: 3px compatVars.$sky-padding-double; + text-align: left; + width: 100%; + + &[disabled] { + color: var(--sky-text-color-deemphasized); + + &:hover { + cursor: default; + } + } +} + +@mixin sky-dropdown-item($encapsulate: true) { background-color: transparent; border: none; display: block; @@ -84,22 +103,13 @@ } } - ::ng-deep > button { - background-color: transparent; - border: none; - color: var(--sky-text-color-default); - cursor: pointer; - display: block; - padding: 3px compatVars.$sky-padding-double; - text-align: left; - width: 100%; - - &[disabled] { - color: var(--sky-text-color-deemphasized); - - &:hover { - cursor: default; - } + @if ($encapsulate == true) { + ::ng-deep > button { + @include sky-dropdown-item-btn(); + } + } @else { + & > button { + @include sky-dropdown-item-btn(); } } } diff --git a/libs/components/theme/src/lib/styles/_public-api/_mixins.scss b/libs/components/theme/src/lib/styles/_public-api/_mixins.scss index 1d6a13803e..958ce5edca 100644 --- a/libs/components/theme/src/lib/styles/_public-api/_mixins.scss +++ b/libs/components/theme/src/lib/styles/_public-api/_mixins.scss @@ -100,6 +100,109 @@ } } +@mixin sky-component($theme, $selector, $encapsulate: true, $breakpoint: '') { + @if $breakpoint == '' { + @include sky-component-theme($theme, $selector) { + @content; + } + } @else if $breakpoint == 'xs' { + @include sky-host-responsive-container-xs-min($encapsulate) { + @include sky-component-theme($theme, $selector) { + @content; + } + } + } @else if $breakpoint == 'sm' { + @include sky-host-responsive-container-sm-min($encapsulate) { + @include sky-component-theme($theme, $selector) { + @content; + } + } + } @else if $breakpoint == 'md' { + @include sky-host-responsive-container-md-min($encapsulate) { + @include sky-component-theme($theme, $selector) { + @content; + } + } + } @else if $breakpoint == 'lg' { + @include sky-host-responsive-container-lg-min($encapsulate) { + @include sky-component-theme($theme, $selector) { + @content; + } + } + } +} + +@mixin sky-component-theme($theme, $selector) { + @if $theme == 'modern' { + :is(.sky-theme-modern #{$selector}) { + @content; + } + } @else { + #{$selector}:not(.sky-theme-modern *) { + @content; + } + } +} + +@mixin sky-component-host( + $theme, + $selector: ':host', + $encapsulate: true, + $breakpoint: '' +) { + @if $breakpoint == '' { + @include sky-component-host-theme($theme, $selector, $encapsulate) { + @content; + } + } @else if $breakpoint == 'xs' { + @include sky-host-responsive-container-xs-min($encapsulate) { + @include sky-component-host-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'sm' { + @include sky-host-responsive-container-sm-min($encapsulate) { + @include sky-component-host-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'md' { + @include sky-host-responsive-container-md-min($encapsulate) { + @include sky-component-host-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'lg' { + @include sky-host-responsive-container-lg-min($encapsulate) { + @include sky-component-host-theme($theme, $selector, $encapsulate) { + @content; + } + } + } +} + +@mixin sky-component-host-theme( + $theme, + $selector: ':host', + $encapsulate: true +) { + @if $theme == 'default' { + #{$selector}.sky-cmp-theme-default { + @content; + } + } @else { + @if $encapsulate { + :host-context(.sky-theme-modern) #{$selector}.sky-cmp-theme-modern { + @content; + } + } @else { + .sky-theme-modern #{$selector}.sky-cmp-theme-modern { + @content; + } + } + } +} + @mixin sky-theme-modern { :host-context(.sky-theme-modern) { @content; diff --git a/libs/components/theme/src/lib/styles/_switch.scss b/libs/components/theme/src/lib/styles/_switch.scss index ccfd0dd6b9..97893aac8e 100644 --- a/libs/components/theme/src/lib/styles/_switch.scss +++ b/libs/components/theme/src/lib/styles/_switch.scss @@ -1,119 +1,119 @@ @use 'mixins' as mixins; @use 'variables' as *; -.sky-switch { +@include mixins.sky-component('default', '.sky-switch') { cursor: pointer; display: inline-flex; position: relative; - &:hover .sky-switch-control { - border-color: var(--sky-highlight-color-info); - border-width: 2px; - } -} - -.sky-switch-disabled { - cursor: default; - - input { + &.sky-switch-disabled { cursor: default; + + input { + cursor: default; + } } -} -.sky-switch-input { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - outline: 0; - -webkit-appearance: none; - - &.sky-switch-invalid + .sky-switch-control { - @include mixins.sky-field-status('invalid'); + &.sky-control-label-required { + .sky-switch-label { + margin-right: 0; + } } - &:checked:not(:disabled) + .sky-switch-control, - &[type='checkbox']:indeterminate:not(:disabled) + .sky-switch-control { - background-color: var(--sky-background-color-input-selected); + &:hover .sky-switch-control { border-color: var(--sky-highlight-color-info); border-width: 2px; + } - &.sky-switch-control-success { - background-color: lighten($sky-background-color-success, 10%); - border-color: var(--sky-highlight-color-success); + .sky-switch-input { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + outline: 0; + -webkit-appearance: none; + + &.sky-switch-invalid + .sky-switch-control { + @include mixins.sky-field-status('invalid'); } - &.sky-switch-control-warning { - background-color: lighten($sky-background-color-warning, 10%); - border-color: var(--sky-highlight-color-warning); + &:checked:not(:disabled) + .sky-switch-control, + &[type='checkbox']:indeterminate:not(:disabled) + .sky-switch-control { + background-color: var(--sky-background-color-input-selected); + border-color: var(--sky-highlight-color-info); + border-width: 2px; + + &.sky-switch-control-success { + background-color: lighten($sky-background-color-success, 10%); + border-color: var(--sky-highlight-color-success); + } + + &.sky-switch-control-warning { + background-color: lighten($sky-background-color-warning, 10%); + border-color: var(--sky-highlight-color-warning); + } + + &.sky-switch-control-danger { + background-color: lighten($sky-background-color-danger, 10%); + border-color: var(--sky-highlight-color-danger); + } } - &.sky-switch-control-danger { - background-color: lighten($sky-background-color-danger, 10%); - border-color: var(--sky-highlight-color-danger); + &:disabled + .sky-switch-control { + background-color: var(--sky-background-color-disabled); } - } - &:disabled + .sky-switch-control { - background-color: var(--sky-background-color-disabled); - } - - &:focus + .sky-switch-control { - @include mixins.sky-focus-outline; + &:focus + .sky-switch-control { + @include mixins.sky-focus-outline; + } } -} -.sky-switch-control { - width: var(--sky-switch-size); - max-width: var(--sky-switch-size); - height: var(--sky-switch-size); - flex: 1 0 var(--sky-switch-size); - margin: 0 var(--sky-switch-margin) auto auto; - display: inline-flex; - position: relative; - border: 1px solid $sky-background-color-disabled; - background-color: $sky-color-white; - color: var(--sky-text-color-default); - text-align: center; - line-height: 1; - align-items: center; - justify-content: center; - - &.sky-switch-control-icon { - max-width: none; - width: 35px; - height: 35px; - flex: 1 0 35px; - font-size: 18px; - } + .sky-switch-control { + width: var(--sky-switch-size); + max-width: var(--sky-switch-size); + height: var(--sky-switch-size); + flex: 1 0 var(--sky-switch-size); + margin: 0 var(--sky-switch-margin) auto auto; + display: inline-flex; + position: relative; + border: 1px solid $sky-background-color-disabled; + background-color: $sky-color-white; + color: var(--sky-text-color-default); + text-align: center; + line-height: 1; + align-items: center; + justify-content: center; + + &.sky-switch-control-icon { + max-width: none; + width: 35px; + height: 35px; + flex: 1 0 35px; + font-size: 18px; + } - &::before { - content: ''; + &::before { + content: ''; + } } -} - -.sky-switch-label { - line-height: var(--sky-switch-size); - flex: 1 1 auto; - width: 100%; - margin-right: $sky-margin; - // Prevent truncated text from spilling out of bounds. - // See: https://css-tricks.com/flexbox-truncated-text/ - min-width: 0; -} - -.sky-control-label-required { .sky-switch-label { - margin-right: 0; + line-height: var(--sky-switch-size); + flex: 1 1 auto; + width: 100%; + margin-right: $sky-margin; + + // Prevent truncated text from spilling out of bounds. + // See: https://css-tricks.com/flexbox-truncated-text/ + min-width: 0; } } -.sky-switch-icon-group { +@include mixins.sky-component('default', '.sky-switch-icon-group') { .sky-switch-control-icon { margin-left: 0; margin-right: 0; diff --git a/libs/components/theme/src/lib/styles/sky.scss b/libs/components/theme/src/lib/styles/sky.scss index 73ebca0ae8..1b52c5c383 100644 --- a/libs/components/theme/src/lib/styles/sky.scss +++ b/libs/components/theme/src/lib/styles/sky.scss @@ -23,4 +23,4 @@ @forward 'type'; @import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'); -@import url('https://sky.blackbaudcdn.net/static/skyux-icons/7.3.0/assets/css/skyux-icons.min.css'); +@import url('https://sky.blackbaudcdn.net/static/skyux-icons/7.8.0/assets/css/skyux-icons.min.css'); diff --git a/libs/components/theme/src/lib/styles/themes/modern/_switch.scss b/libs/components/theme/src/lib/styles/themes/modern/_switch.scss index b39e093122..aae867e8dd 100644 --- a/libs/components/theme/src/lib/styles/themes/modern/_switch.scss +++ b/libs/components/theme/src/lib/styles/themes/modern/_switch.scss @@ -1,102 +1,198 @@ @use '../../variables' as *; +@use '../../mixins' as mixins; -.sky-theme-modern { - .sky-switch-input:not(:disabled) { - &.sky-switch-invalid + .sky-switch-control { - border: solid 2px var(--sky-highlight-color-danger); - box-shadow: none; - } - - &:checked + .sky-switch-control, - &[type='checkbox']:indeterminate + .sky-switch-control, - &:hover + .sky-switch-control { - border: solid 1px var(--sky-background-color-primary-dark); - } - - &:focus + .sky-switch-control, - &:active + .sky-switch-control { - border: solid 2px var(--sky-background-color-primary-dark); - } +@include mixins.sky-component('modern', '.sky-switch', false) { + cursor: pointer; + display: inline-flex; + position: relative; - &:focus:not(:active) + .sky-switch-control { - box-shadow: $sky-theme-modern-elevation-3-shadow-size - $sky-theme-modern-elevation-3-shadow-color; - outline: none; - } + &.sky-switch-disabled { + cursor: not-allowed; } - .sky-switch-control { - transition: $sky-form-border-and-color-transitions; - border: 1px solid var(--sky-border-color-neutral-medium-dark); + &.sky-control-label-required { + .sky-switch-label { + margin-right: 0; + } } - .sky-switch-input:disabled + .sky-switch-control { - background-color: $sky-theme-modern-background-color-neutral-medium; + &:hover .sky-switch-control { + border-color: var(--sky-highlight-color-info); + border-width: 2px; } - .sky-switch-disabled { - cursor: not-allowed; - } + .sky-switch-input { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + outline: 0; + -webkit-appearance: none; - .sky-switch-icon-group { - .sky-switch-control { - background-color: transparent; + &.sky-switch-invalid + .sky-switch-control { + @include mixins.sky-field-status('invalid'); } - .sky-switch-input { - &:checked + .sky-switch-control, - &[type='checkbox']:indeterminate + .sky-switch-control { - background-color: var(--sky-background-color-input-selected); + &:checked:not(:disabled) + .sky-switch-control, + &[type='checkbox']:indeterminate:not(:disabled) + .sky-switch-control { + background-color: var(--sky-background-color-input-selected); + + &.sky-switch-control-success { + background-color: lighten($sky-background-color-success, 10%); + } + + &.sky-switch-control-warning { + background-color: lighten($sky-background-color-warning, 10%); } - &:disabled + .sky-switch-control { - background-color: $sky-theme-modern-background-color-neutral-medium; - border: none; - color: var(--sky-text-color-deemphasized); + &.sky-switch-control-danger { + background-color: lighten($sky-background-color-danger, 10%); + } + } + + &:disabled + .sky-switch-control { + background-color: var(--sky-background-color-disabled); + } + + &:focus + .sky-switch-control { + @include mixins.sky-focus-outline; + } + + &:not(:disabled) { + &.sky-switch-invalid + .sky-switch-control { + border: solid 2px var(--sky-highlight-color-danger); + box-shadow: none; } &:checked + .sky-switch-control, - &:disabled:checked + .sky-switch-control, &[type='checkbox']:indeterminate + .sky-switch-control, - &[type='checkbox']:disabled:indeterminate + .sky-switch-control { - color: var(--sky-background-color-primary-dark); + &:hover + .sky-switch-control { + border: solid 1px var(--sky-background-color-primary-dark); } - } - .sky-switch-control { - border: none; - border-radius: $sky-theme-modern-box-border-radius-default; + &:focus + .sky-switch-control, + &:active + .sky-switch-control { + border: solid 2px var(--sky-background-color-primary-dark); + } - &.sky-switch-control-icon { - font-size: 20px; - height: 40px; - width: 40px; + &:focus:not(:active) + .sky-switch-control { + box-shadow: $sky-theme-modern-elevation-3-shadow-size + $sky-theme-modern-elevation-3-shadow-color; + outline: none; } } + } + + .sky-switch-control { + width: var(--sky-switch-size); + max-width: var(--sky-switch-size); + height: var(--sky-switch-size); + flex: 1 0 var(--sky-switch-size); + margin: 0 var(--sky-switch-margin) auto auto; + display: inline-flex; + position: relative; + border: 1px solid var(--sky-border-color-neutral-medium-dark); + background-color: $sky-color-white; + color: var(--sky-text-color-default); + text-align: center; + line-height: 1; + align-items: center; + justify-content: center; + transition: $sky-form-border-and-color-transitions; + + &.sky-switch-control-icon { + max-width: none; + width: 35px; + height: 35px; + flex: 1 0 35px; + font-size: 18px; + } - sky-checkbox .sky-switch-control-icon { - margin-right: $sky-theme-modern-space-sm; + &::before { + content: ''; } + } - sky-checkbox:last-of-type .sky-switch-control-icon { - margin-right: 0; + .sky-switch-label { + line-height: var(--sky-switch-size); + flex: 1 1 auto; + width: 100%; + margin-right: $sky-margin; + + // Prevent truncated text from spilling out of bounds. + // See: https://css-tricks.com/flexbox-truncated-text/ + min-width: 0; + } +} + +@include mixins.sky-component('modern', '.sky-switch-icon-group', false) { + .sky-switch-control-icon { + margin-left: 0; + margin-right: 0; + border-radius: 0; + } + + .sky-switch-control { + background-color: transparent; + } + + .sky-switch-input { + &:checked + .sky-switch-control, + &[type='checkbox']:indeterminate + .sky-switch-control { + background-color: var(--sky-background-color-input-selected); + } + + &:disabled + .sky-switch-control { + background-color: $sky-theme-modern-background-color-neutral-medium; + border: none; + color: var(--sky-text-color-deemphasized); } - sky-radio .sky-switch-control-icon { - border-radius: $sky-theme-modern-box-border-radius-default; + &:checked + .sky-switch-control, + &:disabled:checked + .sky-switch-control, + &[type='checkbox']:indeterminate + .sky-switch-control, + &[type='checkbox']:disabled:indeterminate + .sky-switch-control { + color: var(--sky-background-color-primary-dark); } } - &.sky-theme-mode-dark { - .sky-switch-input:disabled + .sky-switch-control { - background-color: $sky-theme-modern-mode-dark-background-color-elevation-3; - border-color: $sky-theme-modern-mode-dark-border-color-neutral-medium; - color: $sky-theme-modern-mode-dark-font-deemphasized-color; + .sky-switch-control { + border: none; + border-radius: $sky-theme-modern-box-border-radius-default; + + &.sky-switch-control-icon { + font-size: 20px; + height: 40px; + width: 40px; } - .sky-switch-icon-group { - .sky-switch-control { - color: $sky-theme-modern-mode-dark-font-body-default-color; - } + } + + sky-checkbox .sky-switch-control-icon { + margin-right: $sky-theme-modern-space-sm; + } + + sky-checkbox:last-of-type .sky-switch-control-icon { + margin-right: 0; + } + + sky-radio .sky-switch-control-icon { + border-radius: $sky-theme-modern-box-border-radius-default; + } +} + +.sky-theme-modern.sky-theme-mode-dark { + .sky-switch-input:disabled + .sky-switch-control { + background-color: $sky-theme-modern-mode-dark-background-color-elevation-3; + border-color: $sky-theme-modern-mode-dark-border-color-neutral-medium; + color: $sky-theme-modern-mode-dark-font-deemphasized-color; + } + .sky-switch-icon-group { + .sky-switch-control { + color: $sky-theme-modern-mode-dark-font-body-default-color; } } } diff --git a/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.html b/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.html new file mode 100644 index 0000000000..8c64b41d2b --- /dev/null +++ b/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.html @@ -0,0 +1 @@ +
    test component
    diff --git a/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.ts b/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.ts new file mode 100644 index 0000000000..b97318e02d --- /dev/null +++ b/libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +import { SkyThemeComponentClassDirective } from '../theme-component-class.directive'; + +@Component({ + selector: 'app-theme-component-class-test', + templateUrl: './theme-component-class-test.component.html', + standalone: true, + hostDirectives: [SkyThemeComponentClassDirective], +}) +export class SkyThemeComponentClassTestComponent {} diff --git a/libs/components/theme/src/lib/theming/theme-component-class.directive.spec.ts b/libs/components/theme/src/lib/theming/theme-component-class.directive.spec.ts new file mode 100644 index 0000000000..dac850b219 --- /dev/null +++ b/libs/components/theme/src/lib/theming/theme-component-class.directive.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@skyux-sdk/testing'; + +import { BehaviorSubject } from 'rxjs'; + +import { SkyThemeModule } from '../theme.module'; + +import { MockThemeService } from './fixtures/mock-theme.service'; +import { SkyThemeComponentClassTestComponent } from './fixtures/theme-component-class-test.component'; +import { SkyTheme } from './theme'; +import { SkyThemeMode } from './theme-mode'; +import { SkyThemeSettings } from './theme-settings'; +import { SkyThemeSettingsChange } from './theme-settings-change'; +import { SkyThemeService } from './theme.service'; + +const DEFAULT_THEME = new SkyThemeSettings( + SkyTheme.presets.default, + SkyThemeMode.presets.light, +); +const MODERN_THEME = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, +); + +describe('ThemeComponentClass directive', () => { + //#region helpers + async function changeTheme( + fixture: ComponentFixture, + mockThemeSvc: MockThemeService, + theme: SkyThemeSettings, + ): Promise { + mockThemeSvc.settingsChange!.next({ + currentSettings: theme, + previousSettings: mockThemeSvc.settingsChange!.getValue().currentSettings, + }); + fixture.detectChanges(); + await fixture.whenStable(); + return; + } + //#endregion + + describe('without SkyThemeService provider', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [SkyThemeComponentClassTestComponent, SkyThemeModule], + }); + fixture = TestBed.createComponent(SkyThemeComponentClassTestComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should default to the default class', () => { + expect(fixture.nativeElement).toHaveClass('sky-cmp-theme-default'); + }); + }); + + describe('with SkyThemeService provider', () => { + let fixture: ComponentFixture; + let mockThemeSvc: MockThemeService; + + beforeEach(async () => { + mockThemeSvc = new MockThemeService(); + mockThemeSvc.settingsChange = new BehaviorSubject( + { + currentSettings: DEFAULT_THEME, + previousSettings: undefined, + }, + ); + + TestBed.configureTestingModule({ + imports: [SkyThemeComponentClassTestComponent, SkyThemeModule], + providers: [{ provide: SkyThemeService, useValue: mockThemeSvc }], + }); + fixture = TestBed.createComponent(SkyThemeComponentClassTestComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should have the default theme class when theme is default', () => { + expect(fixture.nativeElement).toHaveClass('sky-cmp-theme-default'); + }); + + it('should have the modern theme class when theme is modern', async () => { + await changeTheme(fixture, mockThemeSvc, MODERN_THEME); + expect(fixture.nativeElement).toHaveClass('sky-cmp-theme-modern'); + }); + }); +}); diff --git a/libs/components/theme/src/lib/theming/theme-component-class.directive.ts b/libs/components/theme/src/lib/theming/theme-component-class.directive.ts new file mode 100644 index 0000000000..07ad675cbc --- /dev/null +++ b/libs/components/theme/src/lib/theming/theme-component-class.directive.ts @@ -0,0 +1,30 @@ +import { + ChangeDetectorRef, + Directive, + HostBinding, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { SkyThemeService } from '../theming/theme.service'; + +@Directive({ + selector: '[skyThemeClass]', + standalone: true, +}) +export class SkyThemeComponentClassDirective { + @HostBinding('class') + public theme = 'sky-cmp-theme-default'; + + #changeDetector = inject(ChangeDetectorRef); + #themeService = inject(SkyThemeService, { optional: true }); + + constructor() { + this.#themeService?.settingsChange + .pipe(takeUntilDestroyed()) + .subscribe((change) => { + this.theme = `sky-cmp-theme-${change.currentSettings.theme.name}`; + this.#changeDetector.markForCheck(); + }); + } +} diff --git a/libs/components/tiles/package.json b/libs/components/tiles/package.json index 37ea6684ce..3cfb332fae 100644 --- a/libs/components/tiles/package.json +++ b/libs/components/tiles/package.json @@ -16,8 +16,8 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/animations": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/help-inline": "0.0.0-PLACEHOLDER", diff --git a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content-section.component.scss b/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content-section.component.scss index aefd647b43..bb4b7fa2f9 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content-section.component.scss +++ b/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content-section.component.scss @@ -1,8 +1,32 @@ @use 'libs/components/theme/src/lib/styles/mixins' as mixins; @use 'libs/components/theme/src/lib/styles/variables' as *; +:host:not(:last-child) .sky-tile-content-section { + @include mixins.sky-border(light); +} + +:host-context(.sky-theme-modern sky-tile-content) { + .sky-tile-content-section { + border-bottom: 1px dotted $sky-theme-modern-color-gray-30; + } + + :host:first-child { + .sky-tile-content-section { + margin-top: $sky-space-lg; + } + } + + :host:last-child { + .sky-tile-content-section { + padding-bottom: 0; + border-bottom: none; + } + } +} + @include mixins.sky-theme-modern { .sky-tile-content-section { + border-bottom: 1px dotted $sky-theme-modern-color-gray-30; padding: $sky-space-lg 0; } } diff --git a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.scss b/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.scss deleted file mode 100644 index e9038957b7..0000000000 --- a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; - -:host - ::ng-deep - sky-tile-content-section:not(:last-child) - .sky-tile-content-section { - @include mixins.sky-border(light); -} diff --git a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.ts b/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.ts index 2a55fa8c1a..33f951b20c 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.ts +++ b/libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.ts @@ -6,7 +6,6 @@ import { Component } from '@angular/core'; @Component({ standalone: true, selector: 'sky-tile-content', - styleUrls: ['./tile-content.component.scss'], templateUrl: 'tile-content.component.html', }) export class SkyTileContentComponent {} diff --git a/libs/components/tiles/src/lib/modules/tiles/tile-dashboard/tile-dashboard.component.scss b/libs/components/tiles/src/lib/modules/tiles/tile-dashboard/tile-dashboard.component.scss index b656f4ebba..319c261a96 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile-dashboard/tile-dashboard.component.scss +++ b/libs/components/tiles/src/lib/modules/tiles/tile-dashboard/tile-dashboard.component.scss @@ -12,20 +12,9 @@ .sky-tile-dashboard-layout-single { display: block; } - - /* NOTE: This style is here as we only want it when inside a tile dashboard */ - .sky-tile-dashboard-layout-single ::ng-deep .sky-tile, - .sky-tile-dashboard-layout-multi ::ng-deep .sky-tile { - margin-bottom: 0; - } } :host-context(.sky-tile-dashboard-gt-xs) { - .sky-tile-dashboard-layout-single ::ng-deep .sky-tile, - .sky-tile-dashboard-layout-multi ::ng-deep .sky-tile { - margin-bottom: $sky-margin-double; - } - :host-context(.sky-theme-default) { padding-top: $sky-padding-double; } @@ -53,12 +42,3 @@ :host-context(.sky-theme-default) { background-color: $sky-background-color-neutral-light; } - -@include mixins.sky-theme-modern { - :host-context(.sky-tile-dashboard-gt-xs) { - .sky-tile-dashboard-layout-single ::ng-deep .sky-tile, - .sky-tile-dashboard-layout-multi ::ng-deep .sky-tile { - margin-bottom: $sky-theme-modern-space-xl; - } - } -} diff --git a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html index 9999043cda..08098117b8 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html +++ b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html @@ -8,7 +8,7 @@ }" >
    -
    +
    @if (tileName && (helpKey || helpPopoverContent)) { @@ -104,7 +104,7 @@ role="region" skyId [attr.aria-label]="tileName" - [attr.aria-labelledBy]="!tileName && titleRef ? tileTitleId : undefined" + [attr.aria-labelledby]="!tileName && titleRef ? tileTitleId : undefined" [@skyAnimationSlide]="isCollapsed ? 'up' : 'down'" #tileContent="skyId" > diff --git a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.scss b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.scss index 9839231efa..1bb397d362 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.scss +++ b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.scss @@ -6,6 +6,28 @@ margin-bottom: $sky-margin-double; } +/* NOTE: This style is here as we only want it when inside a tile dashboard */ +:host-context( + .sky-tile-dashboard-layout-single, + .sky-tile-dashboard-layout-multi + ) + .sky-tile { + margin-bottom: 0; +} + +:host-context(.sky-tile-dashboard-gt-xs) { + :host-context(.sky-tile-dashboard-layout-single), + :host-context(.sky-tile-dashboard-layout-multi) { + .sky-tile { + margin-bottom: $sky-margin-double; + } + } + + :host-context(.sky-theme-default) { + padding-top: $sky-padding-double; + } +} + .sky-tile-header { border-color: $sky-border-color-neutral-medium; border-style: solid solid none; @@ -108,25 +130,6 @@ font-size: 16px; } - .sky-tile-content { - ::ng-deep .sky-tile-content-section { - border-bottom: 1px dotted $sky-theme-modern-color-gray-30; - } - - ::ng-deep sky-tile-content-section:first-child { - .sky-tile-content-section { - margin-top: $sky-space-lg; - } - } - - ::ng-deep sky-tile-content-section:last-child { - .sky-tile-content-section { - padding-bottom: 0; - border-bottom: none; - } - } - } - @include mixins.sky-host-responsive-container-xs-min() { .sky-tile { border-radius: 0px; @@ -139,3 +142,12 @@ } } } + +:host-context(.sky-theme-modern .sky-tile-dashboard-gt-xs) { + :host-context(.sky-tile-dashboard-layout-single), + :host-context(.sky-tile-dashboard-layout-multi) { + .sky-tile { + margin-bottom: $sky-theme-modern-space-xl; + } + } +} diff --git a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.spec.ts b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.spec.ts index ff14ff7a70..eadd4cf9be 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.spec.ts +++ b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.spec.ts @@ -286,7 +286,7 @@ describe('Tile component', () => { }); }); - describe('help button', () => { + describe('help button (legacy)', () => { it('should be absent if a callback is not provided', () => { const html = ` @@ -489,4 +489,21 @@ describe('Tile component', () => { expect(getHelpInlineButton(fixture)).toBeNull(); }); + + it('should not expand/collapse the content when the help inline button is clicked', () => { + const fixture = TestBed.createComponent(TileTestComponent); + const el = fixture.nativeElement; + + fixture.componentInstance.tileName = 'Tile 1'; + fixture.componentInstance.helpPopoverContent = 'Example popover content.'; + fixture.detectChanges(); + + const contentAttrs = el.querySelector('.sky-tile-content').attributes; + + expect(contentAttrs['hidden']).toBeUndefined(); + + getHelpInlineButton(fixture)?.click(); + + expect(contentAttrs['hidden']).toBeUndefined(); + }); }); diff --git a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts index f7d9ab7992..0d8b5513c4 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts +++ b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts @@ -242,8 +242,13 @@ export class SkyTileComponent implements OnChanges, OnDestroy { return this.helpClick.observers.length > 0 && this.showHelp; } - public titleClick(): void { - this.isCollapsed = !this.isCollapsed; + public titleClick(evt: MouseEvent): void { + const targetEl = evt.target as HTMLElement | null; + + // Don't expand/collapse if the help inline button is clicked. + if (targetEl?.closest('sky-help-inline') === null) { + this.isCollapsed = !this.isCollapsed; + } } public chevronDirectionChange(direction: string): void { diff --git a/libs/components/toast/package.json b/libs/components/toast/package.json index 0c077cf2a3..92e6756331 100644 --- a/libs/components/toast/package.json +++ b/libs/components/toast/package.json @@ -16,10 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/animations": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/animations": "^17.3.12", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/animations": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", diff --git a/libs/components/validation/package.json b/libs/components/validation/package.json index 3c2385d18f..286101173a 100644 --- a/libs/components/validation/package.json +++ b/libs/components/validation/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4" + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12" }, "dependencies": { "tslib": "^2.6.2", diff --git a/libs/sdk/e2e-schematics/package.json b/libs/sdk/e2e-schematics/package.json index 6c2ea41b94..ee8deff2ad 100644 --- a/libs/sdk/e2e-schematics/package.json +++ b/libs/sdk/e2e-schematics/package.json @@ -2,14 +2,14 @@ "name": "@skyux-sdk/e2e-schematics", "version": "1.0.0", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/cli": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/cli": "^17.3.9", "@percy/cypress": "^3.1.2", "@nx/devkit": "^19.3.1", "@nx/workspace": "^19.3.1", "@nx/storybook": "^19.3.1", "@nx/angular": "^19.3.1", - "@percy/sdk-utils": "^1.28.8", + "@percy/sdk-utils": "^1.29.3", "typescript": "^5.3.3", "@nx/eslint": "^19.3.1" }, diff --git a/libs/sdk/eslint-config/package.json b/libs/sdk/eslint-config/package.json index f7852016d5..d040219293 100644 --- a/libs/sdk/eslint-config/package.json +++ b/libs/sdk/eslint-config/package.json @@ -28,7 +28,7 @@ } }, "peerDependencies": { - "@angular/cli": "^17.3.4", + "@angular/cli": "^17.3.9", "eslint-plugin-deprecation": "^2.0.0" }, "dependencies": { diff --git a/libs/sdk/prettier-schematics/package.json b/libs/sdk/prettier-schematics/package.json index c32a56ab00..090e532bc9 100644 --- a/libs/sdk/prettier-schematics/package.json +++ b/libs/sdk/prettier-schematics/package.json @@ -30,7 +30,7 @@ } }, "peerDependencies": { - "@angular/cli": "^17.3.4" + "@angular/cli": "^17.3.9" }, "dependencies": { "comment-json": "4.2.3" diff --git a/libs/sdk/testing/package.json b/libs/sdk/testing/package.json index f01d6caeec..bff2e0d0a9 100644 --- a/libs/sdk/testing/package.json +++ b/libs/sdk/testing/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/platform-browser": "^17.3.4", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/platform-browser": "^17.3.12", "@skyux/i18n": "0.0.0-PLACEHOLDER", "axe-core": "^3.5.6 || ~4.6.3 || ~4.7.2 || ~4.9" }, diff --git a/libs/sdk/testing/src/lib/matchers/matchers.spec.ts b/libs/sdk/testing/src/lib/matchers/matchers.spec.ts index cc049f10fa..bc1f2be656 100644 --- a/libs/sdk/testing/src/lib/matchers/matchers.spec.ts +++ b/libs/sdk/testing/src/lib/matchers/matchers.spec.ts @@ -59,6 +59,11 @@ describe('Jasmine matchers', () => { document.body.innerHTML = ''; }); + it('should allow use of main jasmine matchers', waitForAsync(() => { + expect(2).toBe(2); + expectAsync(Promise.resolve(2)).toBeResolved(); + })); + describe('toBeVisible', () => { let child: HTMLDivElement; let parent: HTMLDivElement; diff --git a/libs/sdk/testing/src/lib/matchers/matchers.ts b/libs/sdk/testing/src/lib/matchers/matchers.ts index 95d84a1262..3ad62d2df6 100644 --- a/libs/sdk/testing/src/lib/matchers/matchers.ts +++ b/libs/sdk/testing/src/lib/matchers/matchers.ts @@ -576,11 +576,11 @@ windowRef.beforeEach(() => { /** * Interface for "asynchronous" custom Sky matchers which cannot be paired with a `.not` operator. */ -export interface SkyAsyncMatchers { +export interface SkyAsyncMatchers extends jasmine.AsyncMatchers { /** * Invert the matcher following this `expect` */ - not: SkyAsyncMatchers; + not: SkyAsyncMatchers; /** * `expect` an element to be accessible based on Web Content Accessibility @@ -769,6 +769,6 @@ export function expect(actual: T): SkyMatchers { * Create an async expectation for a spec. * @param actual Actual computed value to test expectations against. */ -export function expectAsync(actual: T): SkyAsyncMatchers { +export function expectAsync(actual: T): SkyAsyncMatchers { return windowRef.expectAsync(actual); } diff --git a/nx.json b/nx.json index 7d54ba77da..c3c5e88858 100644 --- a/nx.json +++ b/nx.json @@ -122,6 +122,6 @@ "!{projectRoot}/tsconfig.storybook.json" ] }, - "nxCloudAccessToken": "NzE5ZWYwYzUtMGU0OC00OTU3LTk4ZDYtOTc1Zjk3MTExMzY5fHJlYWQtd3JpdGU=", - "defaultBase": "main" + "nxCloudAccessToken": "Y2EyNjQ2OTctN2ZkMS00NTA2LWIxZDEtZTQyZDc3MjIwNDQyfHJlYWQ=", + "defaultBase": "10.x.x" } diff --git a/package-lock.json b/package-lock.json index b7ce3d247d..dfa6b6274f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,30 @@ { "name": "skyux", - "version": "10.33.0", + "version": "10.42.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.33.0", + "version": "10.42.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "17.3.4", - "@angular/cdk": "17.3.4", - "@angular/common": "17.3.4", - "@angular/compiler": "17.3.4", - "@angular/core": "17.3.4", - "@angular/forms": "17.3.4", - "@angular/platform-browser": "17.3.4", - "@angular/platform-browser-dynamic": "17.3.4", - "@angular/router": "17.3.4", + "@angular/animations": "17.3.12", + "@angular/cdk": "17.3.10", + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/forms": "17.3.12", + "@angular/platform-browser": "17.3.12", + "@angular/platform-browser-dynamic": "17.3.12", + "@angular/router": "17.3.12", "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.3.0", - "ag-grid-angular": "31.2.0", - "ag-grid-community": "31.2.0", + "@skyux/icons": "7.8.0", + "ag-grid-angular": "31.3.4", + "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", "axe-core": "4.9.0", "comment-json": "4.2.3", @@ -46,15 +46,15 @@ "zone.js": "0.14.4" }, "devDependencies": { - "@angular-devkit/build-angular": "17.3.4", - "@angular-devkit/core": "17.3.4", - "@angular-devkit/schematics": "17.3.4", + "@angular-devkit/build-angular": "17.3.9", + "@angular-devkit/core": "17.3.9", + "@angular-devkit/schematics": "17.3.9", "@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/template-parser": "17.3.0", - "@angular/cli": "17.3.4", - "@angular/compiler-cli": "17.3.4", - "@angular/language-service": "17.3.4", + "@angular/cli": "17.3.9", + "@angular/compiler-cli": "17.3.12", + "@angular/language-service": "17.3.12", "@cspell/eslint-plugin": "8.6.1", "@istanbuljs/nyc-config-typescript": "1.0.2", "@nx/cypress": "19.3.1", @@ -68,14 +68,14 @@ "@nx/storybook": "19.3.1", "@nx/web": "19.3.1", "@nx/workspace": "19.3.1", - "@percy/cli": "1.28.8", - "@percy/core": "1.28.8", + "@percy/cli": "1.29.3", + "@percy/core": "1.29.3", "@percy/cypress": "3.1.2", - "@percy/sdk-utils": "1.28.8", + "@percy/sdk-utils": "1.29.3", "@ryansonshine/commitizen": "4.2.8", "@ryansonshine/cz-conventional-changelog": "3.3.4", - "@schematics/angular": "17.3.2", - "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.5", + "@schematics/angular": "17.3.9", + "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.10", "@storybook/addon-a11y": "8.1.10", "@storybook/addon-actions": "8.1.10", "@storybook/addon-controls": "8.1.10", @@ -117,7 +117,7 @@ "jest-environment-jsdom": "29.7.0", "jest-environment-node": "29.7.0", "jest-preset-angular": "14.1.1", - "karma": "6.4.3", + "karma": "6.4.4", "karma-chrome-launcher": "3.2.0", "karma-coverage": "2.2.1", "karma-jasmine": "5.1.0", @@ -167,11 +167,11 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.4.tgz", - "integrity": "sha512-o+XCMOiMh8tmQGEwcxjAj2/lmUVT7CGSUAM31ydDomVOFFw4CnBvsoyKqQNRC+/AUXvovb2dCegQl/lTAnrwOg==", + "version": "0.1703.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.9.tgz", + "integrity": "sha512-kEPfTOVnzrJxPGTvaXy8653HU9Fucxttx9gVfQR1yafs+yIEGx3fKGKe89YPmaEay32bIm7ZUpxDF1FO14nkdQ==", "dependencies": { - "@angular-devkit/core": "17.3.4", + "@angular-devkit/core": "17.3.9", "rxjs": "7.8.1" }, "engines": { @@ -181,14 +181,14 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.4.tgz", - "integrity": "sha512-8KieoPrsJcFPoza0gLQ6yebtIb3WdH3j/V1TnAihk4tVpgtdch8tOBE3FP1TnSW3RF+iCsA0I5NO9/4YbEsWtw==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.9.tgz", + "integrity": "sha512-EuAPSC4c2DSJLlL4ieviKLx1faTyY+ymWycq6KFwoxu1FgWly/dqBeWyXccYinLhPVZmoh6+A/5S4YWXlOGSnA==", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.4", - "@angular-devkit/build-webpack": "0.1703.4", - "@angular-devkit/core": "17.3.4", + "@angular-devkit/architect": "0.1703.9", + "@angular-devkit/build-webpack": "0.1703.9", + "@angular-devkit/core": "17.3.9", "@babel/core": "7.24.0", "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", @@ -199,7 +199,7 @@ "@babel/preset-env": "7.24.0", "@babel/runtime": "7.24.0", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.4", + "@ngtools/webpack": "17.3.9", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.18", @@ -243,7 +243,7 @@ "undici": "6.11.1", "vite": "5.1.7", "watchpack": "2.4.0", - "webpack": "5.90.3", + "webpack": "5.94.0", "webpack-dev-middleware": "6.1.2", "webpack-dev-server": "4.15.1", "webpack-merge": "5.10.0", @@ -692,11 +692,11 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.4.tgz", - "integrity": "sha512-9Vsl6rfIH8kF02W7i3tW/aMOT2Ld1zpcok7n7JdL3Pb7oW0SOjt73FN6Ykm/hVig12gsOGJtEsDfQRsnCddmfQ==", + "version": "0.1703.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.9.tgz", + "integrity": "sha512-3b0LND39Nc+DwCQ0N7Tbsd7RAFWTeIc4VDwk/7RO8EMYTP5Kfgr/TK66nwTBypHsjmD69IMKHZZaZuiDfGfx2A==", "dependencies": { - "@angular-devkit/architect": "0.1703.4", + "@angular-devkit/architect": "0.1703.9", "rxjs": "7.8.1" }, "engines": { @@ -710,9 +710,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.4.tgz", - "integrity": "sha512-vE69/Db555NTRPh+LUFO3rAQBbv7QGrK59F7chRggDZKamtCq/FfhEg2O+0BXQnUitOQN6WgQ79+payFYWyCCg==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.9.tgz", + "integrity": "sha512-/iKyn5YT7NW5ylrg9yufUydS8byExeQ2HHIwFC4Ebwb/JYYCz+k4tBf2LdP+zXpemDpLznXTQGWia0/yJjG8Vg==", "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", @@ -736,11 +736,11 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.4.tgz", - "integrity": "sha512-Z6801QhIwrMTcKPzdo9si+ZtJkPz8fys0ftOTfTM66+tDECasU7pvk8Dr54WkDY29mdSHzPxpSxAsooEwfxvQQ==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.9.tgz", + "integrity": "sha512-9qg+uWywgAtaQlvbnCQv47hcL6ZuA+d9ucgZ0upZftBllZ2vp5WIthCPb2mB0uBkj84Csmtz9MsErFjOQtTj4g==", "dependencies": { - "@angular-devkit/core": "17.3.4", + "@angular-devkit/core": "17.3.9", "jsonc-parser": "3.2.1", "magic-string": "0.30.8", "ora": "5.4.1", @@ -819,9 +819,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.4.tgz", - "integrity": "sha512-2nBgXRdTSVPZMueV6ZJjajDRucwJBLxwiVhGafk/nI5MJF0Yss/Jfp2Kfzk5Xw2AqGhz0rd00IyNNUQIzO2mlw==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz", + "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==", "dependencies": { "tslib": "^2.3.0" }, @@ -829,13 +829,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.4" + "@angular/core": "17.3.12" } }, "node_modules/@angular/cdk": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.4.tgz", - "integrity": "sha512-/wbKUbc0YC3HGE2TCgW7D07Q99PZ/5uoRvMyWw0/wHa8VLNavXZPecbvtyLs//3HnqoCMSUFE7E2Mrd7jAWfcA==", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", + "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", "dependencies": { "tslib": "^2.3.0" }, @@ -849,15 +849,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.4.tgz", - "integrity": "sha512-o4oIA2stUwXOur/T/kP3Zr8ZUCB4VYmvjACbsQ3tpzVCFYPeaW9psQagBNJfaBVVDSYL+EacVYBYJR9ZImvcGw==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.9.tgz", + "integrity": "sha512-b5RGu5RO4VKZlMQDatwABAn1qocgD9u4IrGN2dvHDcrz5apTKYftUdGyG42vngyDNBCg1mWkSDQEWK4f2HfuGg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.4", - "@angular-devkit/core": "17.3.4", - "@angular-devkit/schematics": "17.3.4", - "@schematics/angular": "17.3.4", + "@angular-devkit/architect": "0.1703.9", + "@angular-devkit/core": "17.3.9", + "@angular-devkit/schematics": "17.3.9", + "@schematics/angular": "17.3.9", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -882,26 +882,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/@schematics/angular": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.4.tgz", - "integrity": "sha512-Rqhp5l76Ej6BOZCHPrvHlA2SBkjv1aHFWAfW9gREke826j46D+fuA0eDAdgeVTz0Fx9e7XM3LdtWsz7CBlV4Ug==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.3.4", - "@angular-devkit/schematics": "17.3.4", - "jsonc-parser": "3.2.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@angular/common": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.4.tgz", - "integrity": "sha512-rEsmtwUMJaNvaimh9hwaHdDLXaOIrjEnYdhmJUvDaKPQaFfSbH3CGGVz9brUyzVJyiWJYkYM0ssxavczeiEe8g==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz", + "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==", "dependencies": { "tslib": "^2.3.0" }, @@ -909,14 +893,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.4", + "@angular/core": "17.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.4.tgz", - "integrity": "sha512-YrDClIzgj6nQwiYHrfV6AkT1C5LCDgJh+LICus/2EY1w80j1Qf48Zh4asictReePdVE2Tarq6dnpDh4RW6LenQ==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz", + "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==", "dependencies": { "tslib": "^2.3.0" }, @@ -924,7 +908,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.4" + "@angular/core": "17.3.12" }, "peerDependenciesMeta": { "@angular/core": { @@ -933,9 +917,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.4.tgz", - "integrity": "sha512-TVWjpZSI/GIXTYsmVgEKYjBckcW8Aj62DcxLNehRFR+c7UB95OY3ZFjU8U4jL0XvWPgTkkVWQVq+P6N4KCBsyw==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz", + "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==", "dependencies": { "@babel/core": "7.23.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -955,7 +939,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.3.4", + "@angular/compiler": "17.3.12", "typescript": ">=5.2 <5.5" } }, @@ -1002,9 +986,9 @@ } }, "node_modules/@angular/core": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.4.tgz", - "integrity": "sha512-fvhBkfa/DDBzp1UcNzSxHj+Z9DebSS/o9pZpZlbu/0uEiu9hScmScnhaty5E0EbutzHB0SVUCz7zZuDeAywvWg==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz", + "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==", "dependencies": { "tslib": "^2.3.0" }, @@ -1017,9 +1001,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.4.tgz", - "integrity": "sha512-XWA/FAs0r7VRdztMIfGU9EE0Chj+1U/sDnzJK3ZPO0n8F8oDAEWGJyiw8GIyWTLs+mz43thVIED3DhbRNsXbWw==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz", + "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -1027,25 +1011,25 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.4", - "@angular/core": "17.3.4", - "@angular/platform-browser": "17.3.4", + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.4.tgz", - "integrity": "sha512-CgLg/7P0+NEeGU+vqvoG0rh2ns5iyfi/UO4JTxN1iMjuFBAUhGHxjiItPy8cN2XK/dWgOhXAFe4oqxA4dMBp/Q==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.12.tgz", + "integrity": "sha512-MVmEXonXwdhFtIpU4q8qbXHsrAsdTjZcPPuWCU0zXVQ+VaB/y6oF7BVpmBtfyBcBCums1guEncPP+AZVvulXmQ==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.4.tgz", - "integrity": "sha512-W2nH9WSQJfdNG4HH9B1Cvj5CTmy9gF3321I+65Tnb8jFmpeljYDBC/VVUhTZUCRpg8udMWeMHEQHuSb8CbozmQ==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz", + "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1053,9 +1037,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.3.4", - "@angular/common": "17.3.4", - "@angular/core": "17.3.4" + "@angular/animations": "17.3.12", + "@angular/common": "17.3.12", + "@angular/core": "17.3.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1064,9 +1048,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.4.tgz", - "integrity": "sha512-S53jPyQtInVYkjdGEFt4dxM1NrHNkWCvXGRsCO7Uh+laDf1OpIDp9YHf49OZohYLajJradN6y4QfdZL6IUwXKA==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.12.tgz", + "integrity": "sha512-DQwV7B2x/DRLRDSisngZRdLqHdYbbrqZv2Hmu4ZbnNYaWPC8qvzgE/0CvY+UkDat3nCcsfwsMnlDeB6TL7/IaA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1074,16 +1058,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.4", - "@angular/compiler": "17.3.4", - "@angular/core": "17.3.4", - "@angular/platform-browser": "17.3.4" + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12" } }, "node_modules/@angular/router": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.4.tgz", - "integrity": "sha512-B1zjUYyhN66dp47zdF96NRwo0dEdM5In4Ob8HN64PAbnaK3y1EPp31aN6EGernPvKum1ibgwSZw+Uwnbkuv7Ww==", + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.12.tgz", + "integrity": "sha512-dg7PHBSW9fmPKTVzwvHEeHZPZdpnUqW/U7kj8D29HTP9ur8zZnx9QcnbplwPeYb8yYa62JMnZSEel2X4PxdYBg==", "dependencies": { "tslib": "^2.3.0" }, @@ -1091,9 +1075,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.4", - "@angular/core": "17.3.4", - "@angular/platform-browser": "17.3.4", + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -5967,9 +5951,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.3.4", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.4.tgz", - "integrity": "sha512-3uNX4tRTKPm91mSQcnmQtqDMMKLGDevJERSPJU7hlOXZZ05QrT4et1mwvXNYYMpXqi2OkC7D4ryIS2YxAiItBA==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.9.tgz", + "integrity": "sha512-2+NvEQuYKRWdZaJbRJWEnR48tpW0uYbhwfHBHLDI9Kazb3mb0oAwYBVXdq+TtDLBypXnMsFpCewjRHTvkVx4/A==", "engines": { "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", @@ -6416,12 +6400,12 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/scope-manager": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", - "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6432,12 +6416,12 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/type-utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", - "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dependencies": { - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/utils": "7.12.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6458,9 +6442,9 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", - "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -6470,12 +6454,12 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", - "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6497,9 +6481,9 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6511,14 +6495,14 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", - "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6532,11 +6516,11 @@ } }, "node_modules/@nx/angular/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", - "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dependencies": { - "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7681,9 +7665,9 @@ } }, "node_modules/@nx/webpack/node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", "funding": [ { "type": "opencollective", @@ -7700,7 +7684,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -7883,20 +7867,20 @@ } }, "node_modules/@percy/cli": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.28.8.tgz", - "integrity": "sha512-sbbiC7Kfs1i+4AsLpHgEx3f6rntXiRShuQa3aNuAQMugXuKgZrr2kI6wzygfO352mYeplblyZNxC0zhXPxtyFA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.29.3.tgz", + "integrity": "sha512-j+LEHQrrtQV0uOe1u38U6RExPl86rwof+qWtkV8cOvPwucAo0DmeVfperLhZFIU/qrZjA9lHkDdYHZyzRndOBw==", "dev": true, "dependencies": { - "@percy/cli-app": "1.28.8", - "@percy/cli-build": "1.28.8", - "@percy/cli-command": "1.28.8", - "@percy/cli-config": "1.28.8", - "@percy/cli-exec": "1.28.8", - "@percy/cli-snapshot": "1.28.8", - "@percy/cli-upload": "1.28.8", - "@percy/client": "1.28.8", - "@percy/logger": "1.28.8" + "@percy/cli-app": "1.29.3", + "@percy/cli-build": "1.29.3", + "@percy/cli-command": "1.29.3", + "@percy/cli-config": "1.29.3", + "@percy/cli-exec": "1.29.3", + "@percy/cli-snapshot": "1.29.3", + "@percy/cli-upload": "1.29.3", + "@percy/client": "1.29.3", + "@percy/logger": "1.29.3" }, "bin": { "percy": "bin/run.cjs" @@ -7906,39 +7890,39 @@ } }, "node_modules/@percy/cli-app": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.28.8.tgz", - "integrity": "sha512-UQzO/I2WKhhh18M5zKzOdqSOGLzViIKDX/77r+pEvz3DC/wlEB4bVTrEKFVvLdj1F4WGLfYCpGh2yIf/+OoDEg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.29.3.tgz", + "integrity": "sha512-7yGEDFIRLMsJ6nzl6aMem9nMj5rfkxODEQIciuIcuao5ZD1x23KhuN3u4QLLwQFOFgy7h4WAePnUTCR6ZtpGCQ==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8", - "@percy/cli-exec": "1.28.8" + "@percy/cli-command": "1.29.3", + "@percy/cli-exec": "1.29.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-build": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.28.8.tgz", - "integrity": "sha512-4PiMffATEsr3CKaHUU9dXPKUu5gWfpVnp2YLawsSjxZj4XsOPqkWC8MsBv7CFVpNBbfjYufSzMe9YLHVcPS14A==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.29.3.tgz", + "integrity": "sha512-fvDr4mUFIG/TQmMWnzQqWi2ga57SWPzXwlh65a4/0PPRKo0dKybFhvZvhCFYhcnVWqXEVYRHM21/oUvFhgnsCw==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8" + "@percy/cli-command": "1.29.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-command": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.28.8.tgz", - "integrity": "sha512-pXV0RqFQYK8bcYvMmf04Bp0HFprq+gqF0B8MrevhqA2YJldISln6TrOEpymQI89Sfj261xdOe6tl0WMIbawAiw==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.29.3.tgz", + "integrity": "sha512-jSltYf97E5u4jjTaW6gLU+5T/sRBwS0RlvraE21gKa0N9SH1l5nWs4YFfsIwamYg3EnCmIrwAf0gupQcQgAuaA==", "dev": true, "dependencies": { - "@percy/config": "1.28.8", - "@percy/core": "1.28.8", - "@percy/logger": "1.28.8" + "@percy/config": "1.29.3", + "@percy/core": "1.29.3", + "@percy/logger": "1.29.3" }, "bin": { "percy-cli-readme": "bin/readme.js" @@ -7948,25 +7932,25 @@ } }, "node_modules/@percy/cli-config": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.28.8.tgz", - "integrity": "sha512-uD13cLYZAhCFdh2zBCCk73LqG/Dx4MUBybs4lLMlAwQZsWRnn4X//99dgVyzOUDzLS8oRWzouFx7Z/5SOcMPvA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.29.3.tgz", + "integrity": "sha512-lVrOqeS1ACaQp9tM4LE7joW/cuGaSJcM/182ci5D3Zr9Yz6Ik/oe1MQnIM9dqnsHmnwasuGBsICsflbVqqcXHQ==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8" + "@percy/cli-command": "1.29.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-exec": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.28.8.tgz", - "integrity": "sha512-a2EhPuuykz9WTIdEEdf9zmqGOu1MATz+Ss4rzIIpXlto5k40K42hn/lzRRIQzGNok6ADXJ0vZ0lLgoBX+SMEPw==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.29.3.tgz", + "integrity": "sha512-AfZ2hI/snahjgXHuI0bKhRLJoEOfod8Uph6fu1UdjI2r6gacgBurvJmrwZOT5yChp2i8+B99e+qFqWNUi9AI7Q==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8", - "@percy/logger": "1.28.8", + "@percy/cli-command": "1.29.3", + "@percy/logger": "1.29.3", "cross-spawn": "^7.0.3", "which": "^2.0.2" }, @@ -7975,12 +7959,12 @@ } }, "node_modules/@percy/cli-snapshot": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.28.8.tgz", - "integrity": "sha512-mIOI9uD0GiOqo4jhaJPRShrgy8isAir3ID9vMQ96B+g2ziUhRWceNdFr+N/OwHFRJ8wyUmJnji2Fr0bNagOWGA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.29.3.tgz", + "integrity": "sha512-5buoW+tSdfCu0Df7LYzOpjJlb9u+4aCTDrYlmwBcyEVUq01E39LN1kRWCYL6jU/APNB+ybUKtLr9w0RtcPDYTQ==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8", + "@percy/cli-command": "1.29.3", "yaml": "^2.0.0" }, "engines": { @@ -7988,12 +7972,12 @@ } }, "node_modules/@percy/cli-upload": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.28.8.tgz", - "integrity": "sha512-qgj610GPo3a5cbRXoP6LPfqp9+Qrgo8Sg7aYswomrt6j7zdoV1RAf419eNfP4pYtLoMji4EIGcSMQ91tWv2DoA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.29.3.tgz", + "integrity": "sha512-KYAtcAzE50lbZ8NTqao/GSVurESUi2iQFCJ0zRwEccxViOJy/dfb5j1i10VPlqP5glCZS+eXXY2YYoxjxVCz3w==", "dev": true, "dependencies": { - "@percy/cli-command": "1.28.8", + "@percy/cli-command": "1.29.3", "fast-glob": "^3.2.11", "image-size": "^1.0.0" }, @@ -8002,13 +7986,13 @@ } }, "node_modules/@percy/client": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.28.8.tgz", - "integrity": "sha512-icBiRmLwODrnbxSDmhhN1KLz6W4xE+0bM8rEF/qcUC8SRKecBrz0RA60kyg/jn3H01iv0TOe4NXEMHHk+f7s7w==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.29.3.tgz", + "integrity": "sha512-BTP/wfgs2/Hjj650tGmp+jkATEIOdNNZFZSRWPXGXF+PFG4zK5jTejBEZlBl3NUqhwMqtfLX/uyvsfKFaWfYDA==", "dev": true, "dependencies": { - "@percy/env": "1.28.8", - "@percy/logger": "1.28.8", + "@percy/env": "1.29.3", + "@percy/logger": "1.29.3", "pako": "^2.1.0" }, "engines": { @@ -8016,12 +8000,12 @@ } }, "node_modules/@percy/config": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.28.8.tgz", - "integrity": "sha512-jsH1CdJQDHfnqRNiR+apugxz3HuMq129LH2+qrf1Ow3nFLRbYfeGD25xiCGMaeDfYCHkY26oSo6409/asBdvpA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.29.3.tgz", + "integrity": "sha512-Jk79XGpiCNI7gmdCoWkn5V7HVa6FFfcYvFg3H1OMd2BqZEDKkPq9bbk0e4AZ93xc2BOjmYWHHj69w7VCu1peug==", "dev": true, "dependencies": { - "@percy/logger": "1.28.8", + "@percy/logger": "1.29.3", "ajv": "^8.6.2", "cosmiconfig": "^8.0.0", "yaml": "^2.0.0" @@ -8031,17 +8015,17 @@ } }, "node_modules/@percy/core": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.28.8.tgz", - "integrity": "sha512-6YfhDoTqUhn3Zs8L9xLNHyGVcRzTjCdi51gwZtf9by/+pDCe8eaqpd5QrkoLocdEcV7Oi+FJcoZxE4OZlQNlyQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.29.3.tgz", + "integrity": "sha512-5RwQyezq/i4fqSMeoKJole/kZ7n7lABITS6py2e9een6svNFRKI8VqWBlMuFsL50Z5mUcbSJDhIl8O8NbIpdwQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@percy/client": "1.28.8", - "@percy/config": "1.28.8", - "@percy/dom": "1.28.8", - "@percy/logger": "1.28.8", - "@percy/webdriver-utils": "1.28.8", + "@percy/client": "1.29.3", + "@percy/config": "1.29.3", + "@percy/dom": "1.29.3", + "@percy/logger": "1.29.3", + "@percy/webdriver-utils": "1.29.3", "content-disposition": "^0.5.4", "cross-spawn": "^7.0.3", "extract-zip": "^2.0.1", @@ -8071,49 +8055,49 @@ } }, "node_modules/@percy/dom": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.28.8.tgz", - "integrity": "sha512-0e/357X13C5lt/49ZiB7WHcYDgRs9nEz0hPFA7qnBa+Q0N5pemZCgUZzY1ltge2v9XYT0IAwYgO7P0V0xObRVQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.29.3.tgz", + "integrity": "sha512-wz5PV5IW/ooYTmeiq4qFDWyZrVoyp4x+cOQ4ndYStDMkiFMnN5zvvqJlSsUOJ9/YKh/BeMn+ed8hlfKOWW3zEQ==", "dev": true }, "node_modules/@percy/env": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.28.8.tgz", - "integrity": "sha512-ZuyOPaaQxpCIVs1lgFeb2DRFAfhbAX7LaK8RAKtcAzoKW86YUosL/OWUtmmzQUvnGMWZb611WLIcV48lspkrQQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.29.3.tgz", + "integrity": "sha512-DwWsnrGWsBQkIuNvw//CNQpyd5LY2rzc6wqB/2GMpVf4iuzKvm5ND55GX8j0FYhf0kJan0aS/+mDKEgZfea1LA==", "dev": true, "dependencies": { - "@percy/logger": "1.28.8" + "@percy/logger": "1.29.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/logger": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.28.8.tgz", - "integrity": "sha512-yw5O8ZJjA9wNaHv/AgzvPDxDEWpHVFBmF6BDKZ+bT2xErgJt8UFgm0+Zv7pRbqXdoEAEHQbBeaYMrI7QkFaebg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.29.3.tgz", + "integrity": "sha512-nNslGmznG5ChKHFtPtRFcjAeuG/Zhr1OgRapLLeikyXyxy8bT929kUgBuGh4ADhp95iovcN7zlHQmpuwbOPQ2Q==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@percy/sdk-utils": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.28.8.tgz", - "integrity": "sha512-eqeIloD3OvHtPVUn08jDKt8m46oakKliEWDGliPdeWTtrfnoTJZtk8yNKe7jn8N3gzNYgU8iytwWFQV00zHBjQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.29.3.tgz", + "integrity": "sha512-ITUZinf+50O/Izs/X3HaRxnZvLv4Fw8lV2mSqVD/500au6bApUNeMHnoaAHOC57FgyUOUaYldiAAXNtE/zANtw==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@percy/webdriver-utils": { - "version": "1.28.8", - "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.28.8.tgz", - "integrity": "sha512-xrL3zBw+coeBs1nrKxHo6Fa3xqLjVACnR9KYc+GR/KrltK3TZF4gmGOHUtFHq7Y+OK9iKFyWqpl0RRwq0Ne2eg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.29.3.tgz", + "integrity": "sha512-+0qyRGKLfYdtzhc9U1m7RCBk6c3+aGy8DPLM6FdlvCrX5+Z93PLLMpoQcXid9cUFFTGAnxOKtYUWS2kc6Q32mg==", "dev": true, "dependencies": { - "@percy/config": "1.28.8", - "@percy/sdk-utils": "1.28.8" + "@percy/config": "1.29.3", + "@percy/sdk-utils": "1.29.3" }, "engines": { "node": ">=14" @@ -8970,12 +8954,12 @@ } }, "node_modules/@schematics/angular": { - "version": "17.3.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.2.tgz", - "integrity": "sha512-zPINvow0Qo6ionnDl25ZzSSLDyDxBjqRPEJWGHU70expbjXK4A2caQT9P/8ImhapbJAXJCfxg4GF9z1d/sWe4w==", + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.9.tgz", + "integrity": "sha512-q6N8mbcYC6cgPyjTrMH7ehULQoUUwEYN4g7uo4ylZ/PFklSLJvpSp4BuuxANgW449qHSBvQfdIoui9ayAUXQzA==", "dependencies": { - "@angular-devkit/core": "17.3.2", - "@angular-devkit/schematics": "17.3.2", + "@angular-devkit/core": "17.3.9", + "@angular-devkit/schematics": "17.3.9", "jsonc-parser": "3.2.1" }, "engines": { @@ -8984,49 +8968,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "17.3.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz", - "integrity": "sha512-1vxKo9+pdSwTOwqPDSYQh84gZYmCJo6OgR5+AZoGLGMZSeqvi9RG5RiUcOMLQYOnuYv0arlhlWxz0ZjyR8ApKw==", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { - "version": "17.3.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.2.tgz", - "integrity": "sha512-AYO6oc6QpFGigc1KiDzEVT1CeLnwvnIedU5Q/U3JDZ/Yqmvgc09D64g9XXER2kg6tV7iEgLxiYnonIAQOHq7eA==", - "dependencies": { - "@angular-devkit/core": "17.3.2", - "jsonc-parser": "3.2.1", - "magic-string": "0.30.8", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@sigstore/bundle": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.1.tgz", @@ -9146,12 +9087,12 @@ } }, "node_modules/@skyux/dev-infra-private": { - "version": "10.0.0-alpha.5", - "resolved": "git+ssh://git@github.com/blackbaud/skyux-dev-infra-private-builds.git#a0b36ddd4de5723ddff68659439e305544f4456e", + "version": "10.0.0-alpha.10", + "resolved": "git+ssh://git@github.com/blackbaud/skyux-dev-infra-private-builds.git#db0dcfa25e0de900d72162e4919602346006a554", "dev": true, "license": "MIT", "dependencies": { - "axios": "1.6.7", + "axios": "1.7.4", "cross-spawn": "7.0.3", "fs-extra": "11.2.0", "glob": "10.3.10", @@ -9301,9 +9242,9 @@ "dev": true }, "node_modules/@skyux/icons": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@skyux/icons/-/icons-7.3.0.tgz", - "integrity": "sha512-6Wpdp5K/iynnleaTLSNck8286Egw+pfbwwWuZ7TYe0KbhcqFYM5NvVOJEcbJlZ0UzXiGGaHI/QhZJA1wGiiPqg==" + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@skyux/icons/-/icons-7.8.0.tgz", + "integrity": "sha512-p5YBmDHLV0Woe5sYTUShp/HwX7wvQTnZhZbNypJr0FxgvHBxcdzBo6DXs4Jna74UZUdsJkjM5zFYDKqmcxOwJA==" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.1", @@ -10941,24 +10882,6 @@ "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", "dev": true }, - "node_modules/@types/eslint": { - "version": "8.56.9", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", - "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -12578,10 +12501,10 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "peerDependencies": { "acorn": "^8" } @@ -12642,22 +12565,22 @@ } }, "node_modules/ag-grid-angular": { - "version": "31.2.0", - "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.2.0.tgz", - "integrity": "sha512-dVRB9bQzbt3LBKQgHDjJ4NG9s8b1kXh/h0iS0jcrjgPzCLg4GTHomd5FLvbDWBiKOwCpurT4dy/O5+NKtGrGdg==", + "version": "31.3.4", + "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.3.4.tgz", + "integrity": "sha512-ELDqSc0R1fZRQBPTJgYWWF3Gbe7EbenmwzH3cNaQp38HbBQFkUcAvDqKHgSfmESe1GM76PoMHMxpCdqaWM3SmQ==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": ">= 14.0.0", "@angular/core": ">= 14.0.0", - "ag-grid-community": "31.2.0" + "ag-grid-community": "31.3.4" } }, "node_modules/ag-grid-community": { - "version": "31.2.0", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.2.0.tgz", - "integrity": "sha512-Ija6X171Iq3mFZASZlriQIIdEFqA71rZIsjQD6KHy5lMmxnoseZTX2neThBav1gvr6SA6n5B2PD6eUHdZnrUfw==" + "version": "31.3.4", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.4.tgz", + "integrity": "sha512-jOxQO86C6eLnk1GdP24HB6aqaouFzMWizgfUwNY5MnetiWzz9ZaAmOGSnW/XBvdjXvC5Fpk3gSbvVKKQ7h9kBw==" }, "node_modules/agent-base": { "version": "7.1.1", @@ -13126,11 +13049,11 @@ } }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -17234,9 +17157,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -23106,9 +23029,9 @@ } }, "node_modules/karma": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", - "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "devOptional": true, "dependencies": { "@colors/colors": "1.5.0", @@ -24333,9 +24256,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -26758,9 +26681,9 @@ "devOptional": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "4.0.1", @@ -32619,25 +32542,24 @@ } }, "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", @@ -32645,7 +32567,7 @@ "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -32915,6 +32837,18 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/package.json b/package.json index 49af080df0..9e38296d6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.33.0", + "version": "10.42.0", "license": "MIT", "scripts": { "ng": "nx", @@ -88,21 +88,21 @@ }, "private": true, "dependencies": { - "@angular/animations": "17.3.4", - "@angular/cdk": "17.3.4", - "@angular/common": "17.3.4", - "@angular/compiler": "17.3.4", - "@angular/core": "17.3.4", - "@angular/forms": "17.3.4", - "@angular/platform-browser": "17.3.4", - "@angular/platform-browser-dynamic": "17.3.4", - "@angular/router": "17.3.4", + "@angular/animations": "17.3.12", + "@angular/cdk": "17.3.10", + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/forms": "17.3.12", + "@angular/platform-browser": "17.3.12", + "@angular/platform-browser-dynamic": "17.3.12", + "@angular/router": "17.3.12", "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.3.0", - "ag-grid-angular": "31.2.0", - "ag-grid-community": "31.2.0", + "@skyux/icons": "7.8.0", + "ag-grid-angular": "31.3.4", + "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", "axe-core": "4.9.0", "comment-json": "4.2.3", @@ -124,15 +124,15 @@ "zone.js": "0.14.4" }, "devDependencies": { - "@angular-devkit/build-angular": "17.3.4", - "@angular-devkit/core": "17.3.4", - "@angular-devkit/schematics": "17.3.4", + "@angular-devkit/build-angular": "17.3.9", + "@angular-devkit/core": "17.3.9", + "@angular-devkit/schematics": "17.3.9", "@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/template-parser": "17.3.0", - "@angular/cli": "17.3.4", - "@angular/compiler-cli": "17.3.4", - "@angular/language-service": "17.3.4", + "@angular/cli": "17.3.9", + "@angular/compiler-cli": "17.3.12", + "@angular/language-service": "17.3.12", "@cspell/eslint-plugin": "8.6.1", "@istanbuljs/nyc-config-typescript": "1.0.2", "@nx/cypress": "19.3.1", @@ -146,14 +146,14 @@ "@nx/storybook": "19.3.1", "@nx/web": "19.3.1", "@nx/workspace": "19.3.1", - "@percy/cli": "1.28.8", - "@percy/core": "1.28.8", + "@percy/cli": "1.29.3", + "@percy/core": "1.29.3", "@percy/cypress": "3.1.2", - "@percy/sdk-utils": "1.28.8", + "@percy/sdk-utils": "1.29.3", "@ryansonshine/commitizen": "4.2.8", "@ryansonshine/cz-conventional-changelog": "3.3.4", - "@schematics/angular": "17.3.2", - "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.5", + "@schematics/angular": "17.3.9", + "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.10", "@storybook/addon-a11y": "8.1.10", "@storybook/addon-actions": "8.1.10", "@storybook/addon-controls": "8.1.10", @@ -195,7 +195,7 @@ "jest-environment-jsdom": "29.7.0", "jest-environment-node": "29.7.0", "jest-preset-angular": "14.1.1", - "karma": "6.4.3", + "karma": "6.4.4", "karma-chrome-launcher": "3.2.0", "karma-coverage": "2.2.1", "karma-jasmine": "5.1.0",