From d6d09d275a0b07d3f698fe6c41b1b185fac851b0 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:57:28 -0400 Subject: [PATCH 01/95] chore: create 10.x.x LTS branch --- .github/workflows/cherry-pick.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release-please.yml | 2 +- .skyuxdev.json | 2 +- nx.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 95f84ab94a..091020596a 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12344fb043..afa70d621c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - main + - 10.x.x env: NX_CLOUD_DISTRIBUTED_EXECUTION: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fa94842ff0..2f68aab3e9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,7 +6,7 @@ on: pull_request_target: push: branches: - - main + - 10.x.x workflow_dispatch: env: 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/.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/nx.json b/nx.json index 7d54ba77da..dbbb749490 100644 --- a/nx.json +++ b/nx.json @@ -123,5 +123,5 @@ ] }, "nxCloudAccessToken": "NzE5ZWYwYzUtMGU0OC00OTU3LTk4ZDYtOTc1Zjk3MTExMzY5fHJlYWQtd3JpdGU=", - "defaultBase": "main" + "defaultBase": "10.x.x" } From 94677c154ea986849717233791a1566e1b7c6148 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:54:14 -0400 Subject: [PATCH 02/95] chore: split alert, checkbox, and action button styles into default and modern (#2405) --- .../checkbox-label-text-label.component.scss | 3 - .../checkbox-label-text-label.component.ts | 5 +- ...ox-label-text-label.default.component.scss | 6 + ...box-label-text-label.modern.component.scss | 6 + .../modules/checkbox/checkbox.component.ts | 7 +- .../checkbox/checkbox.default.component.scss | 37 +++ ...nt.scss => checkbox.modern.component.scss} | 45 ++-- .../modules/radio/radio-label.component.scss | 0 .../modules/radio/radio-label.component.ts | 1 - .../lib/modules/radio/radio.component.scss | 7 +- .../lib/modules/alert/alert.component.scss | 193 --------------- .../src/lib/modules/alert/alert.component.ts | 7 +- .../alert/alert.default.component.scss | 132 ++++++++++ .../modules/alert/alert.modern.component.scss | 145 +++++++++++ .../action-button-container.component.scss | 98 -------- .../action-button-container.component.ts | 8 +- ...on-button-container.default.component.scss | 69 ++++++ ...ion-button-container.modern.component.scss | 72 ++++++ .../action-button-header.component.scss | 20 -- .../action-button-header.component.ts | 7 +- ...ction-button-header.default.component.scss | 20 ++ ...action-button-header.modern.component.scss | 6 + .../action-button-icon.component.scss | 37 --- .../action-button-icon.component.ts | 5 +- .../action-button-icon.default.component.scss | 24 ++ .../action-button-icon.modern.component.scss | 22 ++ .../action-button.component.scss | 73 ------ .../action-button/action-button.component.ts | 5 +- .../action-button.default.component.scss | 38 +++ .../action-button.modern.component.scss | 41 ++++ libs/components/theme/src/index.ts | 1 + .../src/lib/styles/_public-api/_mixins.scss | 108 ++++++++ .../theme/src/lib/styles/_switch.scss | 172 ++++++------- .../src/lib/styles/themes/modern/_switch.scss | 230 +++++++++++++----- .../theme-component-class-test.component.html | 1 + .../theme-component-class-test.component.ts | 11 + .../theme-component-class.directive.spec.ts | 90 +++++++ .../theme-component-class.directive.ts | 30 +++ 38 files changed, 1173 insertions(+), 609 deletions(-) delete mode 100644 libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.scss create mode 100644 libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.default.component.scss create mode 100644 libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.modern.component.scss create mode 100644 libs/components/forms/src/lib/modules/checkbox/checkbox.default.component.scss rename libs/components/forms/src/lib/modules/checkbox/{checkbox.component.scss => checkbox.modern.component.scss} (60%) delete mode 100644 libs/components/forms/src/lib/modules/radio/radio-label.component.scss delete mode 100644 libs/components/indicators/src/lib/modules/alert/alert.component.scss create mode 100644 libs/components/indicators/src/lib/modules/alert/alert.default.component.scss create mode 100644 libs/components/indicators/src/lib/modules/alert/alert.modern.component.scss delete mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-container.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-container.default.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-container.modern.component.scss delete mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-header.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-header.default.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-header.modern.component.scss delete mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-icon.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-icon.default.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button-icon.modern.component.scss delete mode 100644 libs/components/layout/src/lib/modules/action-button/action-button.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button.default.component.scss create mode 100644 libs/components/layout/src/lib/modules/action-button/action-button.modern.component.scss create mode 100644 libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.html create mode 100644 libs/components/theme/src/lib/theming/fixtures/theme-component-class-test.component.ts create mode 100644 libs/components/theme/src/lib/theming/theme-component-class.directive.spec.ts create mode 100644 libs/components/theme/src/lib/theming/theme-component-class.directive.ts diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.scss b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.scss deleted file mode 100644 index 3f55331c58..0000000000 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.sky-switch-label { - margin-right: 0; -} diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.ts index b9b3f2d131..ba2e759bf1 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.component.ts @@ -5,7 +5,10 @@ import { Component, Input } from '@angular/core'; */ @Component({ selector: 'sky-checkbox-label-text-label', - styleUrl: './checkbox-label-text-label.component.scss', + styleUrls: [ + './checkbox-label-text-label.default.component.scss', + './checkbox-label-text-label.modern.component.scss', + ], templateUrl: './checkbox-label-text-label.component.html', }) export class SkyCheckboxLabelTextLabelComponent { diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.default.component.scss b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.default.component.scss new file mode 100644 index 0000000000..a4da3bb243 --- /dev/null +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.default.component.scss @@ -0,0 +1,6 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('default', '.sky-switch-label') { + margin-right: 0; +} diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.modern.component.scss b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.modern.component.scss new file mode 100644 index 0000000000..4d2beaf1ff --- /dev/null +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-label-text-label.modern.component.scss @@ -0,0 +1,6 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('modern', '.sky-switch-label') { + margin-right: 0; +} diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts index 0b7405259f..eada79d0be 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts @@ -22,6 +22,7 @@ import { Validators, } from '@angular/forms'; import { SkyFormsUtility, SkyIdService, SkyLogService } from '@skyux/core'; +import { SkyThemeComponentClassDirective } from '@skyux/theme'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -37,7 +38,11 @@ import { SkyCheckboxChange } from './checkbox-change'; @Component({ selector: 'sky-checkbox', templateUrl: './checkbox.component.html', - styleUrls: ['./checkbox.component.scss'], + styleUrls: [ + './checkbox.default.component.scss', + './checkbox.modern.component.scss', + ], + hostDirectives: [SkyThemeComponentClassDirective], providers: [ { provide: NG_VALIDATORS, useExisting: SkyCheckboxComponent, multi: true }, { diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.default.component.scss b/libs/components/forms/src/lib/modules/checkbox/checkbox.default.component.scss new file mode 100644 index 0000000000..a9a22502ce --- /dev/null +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.default.component.scss @@ -0,0 +1,37 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('default', '.sky-checkbox-outer-wrapper') { + display: inline-flex; + + .sky-checkbox-help-inline { + display: inline-flex; + } + + .sky-checkbox-icon-indeterminate { + font-size: 14px; + } +} + +@include mixins.sky-component('default', '.sky-checkbox-hint-text') { + margin-top: var(--sky-margin-stacked-xs); +} + +@include mixins.sky-component('default', '.sky-checkbox-form-margin') { + margin-left: calc(var(--sky-switch-size) + var(--sky-switch-margin)); +} + +@include mixins.sky-component-host('default', ':host.sky-margin-stacked-lg') { + display: block; +} + +@include mixins.sky-component-host( + 'default', + ':host-context(sky-checkbox-group .sky-checkbox-group-stacked) :host' +) { + display: block; + + &:not(:last-child) { + margin-bottom: var(--sky-margin-stacked-sm); + } +} diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.scss b/libs/components/forms/src/lib/modules/checkbox/checkbox.modern.component.scss similarity index 60% rename from libs/components/forms/src/lib/modules/checkbox/checkbox.component.scss rename to libs/components/forms/src/lib/modules/checkbox/checkbox.modern.component.scss index 0860323882..cae9df617b 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.scss +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.modern.component.scss @@ -1,31 +1,17 @@ @use 'libs/components/theme/src/lib/styles/mixins' as mixins; @use 'libs/components/theme/src/lib/styles/variables' as *; -.sky-checkbox-outer-wrapper { +@include mixins.sky-component('modern', '.sky-checkbox-outer-wrapper') { display: inline-flex; -} - -:host.sky-margin-stacked-lg { - display: block; -} - -.sky-checkbox-help-inline { - display: inline-flex; -} - -.sky-checkbox-icon-indeterminate { - font-size: 14px; -} -.sky-checkbox-hint-text { - margin-top: var(--sky-margin-stacked-xs); -} + .sky-checkbox-help-inline { + display: inline-flex; + } -.sky-checkbox-form-margin { - margin-left: calc(var(--sky-switch-size) + var(--sky-switch-margin)); -} + .sky-checkbox-icon-indeterminate { + font-size: 14px; + } -@include mixins.sky-theme-modern { .sky-checkbox-icon-modern-checked, .sky-checkbox-icon-modern-indeterminate { color: $sky-theme-modern-background-color-primary-dark; @@ -43,7 +29,22 @@ } } -:host-context(sky-checkbox-group .sky-checkbox-group-stacked) :host { +@include mixins.sky-component('modern', '.sky-checkbox-hint-text') { + margin-top: var(--sky-margin-stacked-xs); +} + +@include mixins.sky-component('modern', '.sky-checkbox-form-margin') { + margin-left: calc(var(--sky-switch-size) + var(--sky-switch-margin)); +} + +@include mixins.sky-component-host('modern', ':host.sky-margin-stacked-lg') { + display: block; +} + +@include mixins.sky-component-host( + 'modern', + ':host-context(sky-checkbox-group .sky-checkbox-group-stacked) :host' +) { display: block; &:not(:last-child) { diff --git a/libs/components/forms/src/lib/modules/radio/radio-label.component.scss b/libs/components/forms/src/lib/modules/radio/radio-label.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/components/forms/src/lib/modules/radio/radio-label.component.ts b/libs/components/forms/src/lib/modules/radio/radio-label.component.ts index ce8c736181..7ae879b6aa 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-label.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-label.component.ts @@ -10,7 +10,6 @@ import { SkyLogService } from '@skyux/core'; @Component({ selector: 'sky-radio-label', templateUrl: './radio-label.component.html', - styleUrls: ['./radio-label.component.scss'], }) export class SkyRadioLabelComponent { // When clicking on a checkbox label, Angular registers two click events. diff --git a/libs/components/forms/src/lib/modules/radio/radio.component.scss b/libs/components/forms/src/lib/modules/radio/radio.component.scss index 279297861c..83debaf447 100644 --- a/libs/components/forms/src/lib/modules/radio/radio.component.scss +++ b/libs/components/forms/src/lib/modules/radio/radio.component.scss @@ -13,7 +13,12 @@ display: inline-flex; } -.sky-switch-label { +@include mixins.sky-component('default', '.sky-switch-label') { + margin-right: var(--sky-margin-inline-xs); +} + +// TODO: move to the modern stylesheet when splitting out radio button +@include mixins.sky-component('modern', '.sky-switch-label') { margin-right: var(--sky-margin-inline-xs); } diff --git a/libs/components/indicators/src/lib/modules/alert/alert.component.scss b/libs/components/indicators/src/lib/modules/alert/alert.component.scss deleted file mode 100644 index c2fda4794b..0000000000 --- a/libs/components/indicators/src/lib/modules/alert/alert.component.scss +++ /dev/null @@ -1,193 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; -@use 'libs/components/theme/src/lib/styles/variables' as *; - -:host { - display: block; -} - -/* Default theme */ -.sky-alert { - --sky-alert-before-display: initial; - --sky-alert-border-left-width: 30px; - --sky-alert-close-btn-active-border: none; - --sky-alert-close-btn-border-radius: #{$sky-border-radius}; - --sky-alert-close-btn-focus-border: none; - --sky-alert-close-btn-focus-box-shadow: none; - --sky-alert-close-btn-opacity: 0.8; - --sky-alert-content-padding-top-bottom: #{$sky-padding}; - --sky-alert-content-padding-left-right: 0; - --sky-alert-icon-default-display: initial; - --sky-alert-icon-modern-display: none; - --sky-alert-info-background-color: var(--sky-background-color-info); -} - -/* Modern theme */ -@include mixins.sky-theme-modern { - .sky-alert { - --sky-alert-before-display: none; - --sky-alert-border-left-width: 7px; - --sky-alert-close-btn-active-border: var( - --sky-background-color-primary-dark - ) - solid 2px; - --sky-alert-close-btn-border-radius: #{$sky-theme-modern-border-radius-md}; - --sky-alert-close-btn-focus-border: var(--sky-background-color-primary-dark) - solid 1px; - --sky-alert-close-btn-focus-box-shadow: #{$sky-theme-modern-elevation-1-shadow-size} - #{$sky-theme-modern-elevation-3-shadow-color}; - --sky-alert-close-btn-opacity: 1; - --sky-alert-content-padding-left-right: var(--sky-padding-even-md); - --sky-alert-content-padding-top-bottom: var(--sky-padding-even-md); - --sky-alert-icon-default-display: none; - --sky-alert-icon-modern-display: initial; - --sky-alert-info-background-color: var(--sky-background-color-info-light); - } -} - -.sky-alert { - padding: 0 $sky-padding; - border-left: solid var(--sky-alert-border-left-width); - color: var(--sky-text-color-default); - display: flex; - flex-direction: row; - align-items: center; - - .sky-alert-content { - padding: var(--sky-alert-content-padding-top-bottom) - var(--sky-alert-content-padding-left-right); - width: 100%; - - ::ng-deep a { - color: change-color($sky-text-color-default, $alpha: 0.8); - text-decoration: underline; - - &:hover { - color: var(--sky-text-color-default); - } - } - } - - button { - margin-left: auto; - width: 32px; - height: 32px; - } - - &.sky-alert-info, - &.sky-alert-success, - &.sky-alert-warning, - &.sky-alert-danger { - &:before { - font-family: FontAwesome; - margin-left: -32px; - margin-right: 19px; - color: $sky-color-white; - display: var(--sky-alert-before-display); - } - } - - &:not(.sky-alert-danger) { - --sky-icon-stack-top-icon-color-override: #{$sky-text-color-default}; - } - - &.sky-alert-info { - background-color: var(--sky-alert-info-background-color); - border-color: var(--sky-highlight-color-info); - - &:before { - content: '\f06a'; - margin-left: -31px; - margin-right: 20px; - } - } - - &.sky-alert-success { - background-color: var(--sky-background-color-success); - border-color: var(--sky-highlight-color-success); - - &:before { - content: '\f00c'; - } - } - - &.sky-alert-warning { - background-color: var(--sky-background-color-warning); - border-color: var(--sky-highlight-color-warning); - - &:before { - content: '\f071'; - } - } - - &.sky-alert-danger { - background-color: var(--sky-background-color-danger); - border-color: var(--sky-highlight-color-danger); - - &:before { - content: '\f071'; - } - } -} - -.sky-alert-close { - cursor: pointer; - font-weight: bold; - line-height: 1; - margin: 0; - padding: 0; - color: var(--sky-text-color-default); - opacity: var(--sky-alert-close-btn-opacity); - border: none; - border-radius: var(--sky-alert-close-btn-border-radius); - background-color: transparent; - display: none; - flex-shrink: 0; - - &:hover { - opacity: 1; - border: var(--sky-alert-close-btn-focus-border); - } - - &:focus-visible { - border: var(--sky-background-color-primary-dark) solid 2px; - outline: none; - } - - &:active { - border: var(--sky-alert-close-btn-active-border); - } - - &:focus-visible:not(:active) { - box-shadow: var(--sky-alert-close-btn-focus-box-shadow); - } -} - -.sky-alert-closeable { - .sky-alert-close { - display: block; - } -} - -.sky-alert-icon-theme-default { - display: var(--sky-alert-icon-default-display); -} - -.sky-alert-icon-theme-modern { - display: var(--sky-alert-icon-modern-display); -} - -.sky-alert-info .sky-alert-icon-theme-modern { - color: var(--sky-highlight-color-info); -} - -.sky-alert-success .sky-alert-icon-theme-modern { - color: var(--sky-highlight-color-success); -} - -.sky-alert-warning .sky-alert-icon-theme-modern { - color: var(--sky-highlight-color-warning); -} - -.sky-alert-danger .sky-alert-icon-theme-modern { - color: var(--sky-highlight-color-danger); -} diff --git a/libs/components/indicators/src/lib/modules/alert/alert.component.ts b/libs/components/indicators/src/lib/modules/alert/alert.component.ts index 9b16670b08..9e8735b8bb 100644 --- a/libs/components/indicators/src/lib/modules/alert/alert.component.ts +++ b/libs/components/indicators/src/lib/modules/alert/alert.component.ts @@ -11,6 +11,7 @@ import { import { SkyLogService } from '@skyux/core'; import { SkyLibResourcesService } from '@skyux/i18n'; import { SkyIconStackItem } from '@skyux/icon'; +import { SkyThemeComponentClassDirective } from '@skyux/theme'; import { Subscription } from 'rxjs'; @@ -22,8 +23,12 @@ const ALERT_TYPE_DEFAULT = 'warning'; @Component({ selector: 'sky-alert', - styleUrls: ['./alert.component.scss'], + styleUrls: [ + './alert.default.component.scss', + './alert.modern.component.scss', + ], templateUrl: './alert.component.html', + hostDirectives: [SkyThemeComponentClassDirective], }) export class SkyAlertComponent implements AfterViewChecked, OnInit, OnDestroy { /** diff --git a/libs/components/indicators/src/lib/modules/alert/alert.default.component.scss b/libs/components/indicators/src/lib/modules/alert/alert.default.component.scss new file mode 100644 index 0000000000..c9e40e299b --- /dev/null +++ b/libs/components/indicators/src/lib/modules/alert/alert.default.component.scss @@ -0,0 +1,132 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component-host('default') { + display: block; +} + +@include mixins.sky-component('default', '.sky-alert') { + padding: 0 $sky-padding; + border-left: solid 30px; + color: var(--sky-text-color-default); + display: flex; + flex-direction: row; + align-items: center; + + .sky-alert-content { + padding: $sky-padding 0; + width: 100%; + + ::ng-deep a { + color: change-color($sky-text-color-default, $alpha: 0.8); + text-decoration: underline; + + &:hover { + color: var(--sky-text-color-default); + } + } + } + + button { + margin-left: auto; + width: 32px; + height: 32px; + } + + &.sky-alert-info, + &.sky-alert-success, + &.sky-alert-warning, + &.sky-alert-danger { + &:before { + font-family: FontAwesome; + margin-left: -32px; + margin-right: 19px; + color: $sky-color-white; + } + } + + &:not(.sky-alert-danger) { + --sky-icon-stack-top-icon-color-override: #{$sky-text-color-default}; + } + + &.sky-alert-info { + background-color: var(--sky-background-color-info); + border-color: var(--sky-highlight-color-info); + + &:before { + content: '\f06a'; + margin-left: -31px; + margin-right: 20px; + } + } + + &.sky-alert-success { + background-color: var(--sky-background-color-success); + border-color: var(--sky-highlight-color-success); + + &:before { + content: '\f00c'; + } + } + + &.sky-alert-warning { + background-color: var(--sky-background-color-warning); + border-color: var(--sky-highlight-color-warning); + + &:before { + content: '\f071'; + } + } + + &.sky-alert-danger { + background-color: var(--sky-background-color-danger); + border-color: var(--sky-highlight-color-danger); + + &:before { + content: '\f071'; + } + } + + .sky-alert-close { + cursor: pointer; + font-weight: bold; + line-height: 1; + margin: 0; + padding: 0; + color: var(--sky-text-color-default); + opacity: 0.8; + border: none; + border-radius: $sky-border-radius; + background-color: transparent; + display: none; + flex-shrink: 0; + + &:hover { + opacity: 1; + border: none; + } + + &:focus-visible { + border: var(--sky-background-color-primary-dark) solid 2px; + outline: none; + } + + &:active { + border: none; + } + + &:focus-visible:not(:active) { + box-shadow: none; + } + } + + &.sky-alert-closeable { + .sky-alert-close { + display: block; + } + } + + .sky-alert-icon-theme-modern { + display: none; + } +} diff --git a/libs/components/indicators/src/lib/modules/alert/alert.modern.component.scss b/libs/components/indicators/src/lib/modules/alert/alert.modern.component.scss new file mode 100644 index 0000000000..fc6775923a --- /dev/null +++ b/libs/components/indicators/src/lib/modules/alert/alert.modern.component.scss @@ -0,0 +1,145 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component-host('modern') { + display: block; +} + +@include mixins.sky-component('modern', '.sky-alert') { + padding: 0 $sky-padding; + border-left: solid 7px; + color: var(--sky-text-color-default); + display: flex; + flex-direction: row; + align-items: center; + + .sky-alert-content { + padding: var(--sky-padding-even-md); + width: 100%; + + ::ng-deep a { + color: change-color($sky-text-color-default, $alpha: 0.8); + text-decoration: underline; + + &:hover { + color: var(--sky-text-color-default); + } + } + } + + button { + margin-left: auto; + width: 32px; + height: 32px; + } + + &.sky-alert-info, + &.sky-alert-success, + &.sky-alert-warning, + &.sky-alert-danger { + &:before { + display: none; + } + } + + &:not(.sky-alert-danger) { + --sky-icon-stack-top-icon-color-override: #{$sky-text-color-default}; + } + + &.sky-alert-info { + background-color: var(--sky-background-color-info-light); + border-color: var(--sky-highlight-color-info); + + &:before { + content: '\f06a'; + margin-left: -31px; + margin-right: 20px; + } + } + + &.sky-alert-success { + background-color: var(--sky-background-color-success); + border-color: var(--sky-highlight-color-success); + + &:before { + content: '\f00c'; + } + } + + &.sky-alert-warning { + background-color: var(--sky-background-color-warning); + border-color: var(--sky-highlight-color-warning); + + &:before { + content: '\f071'; + } + } + + &.sky-alert-danger { + background-color: var(--sky-background-color-danger); + border-color: var(--sky-highlight-color-danger); + + &:before { + content: '\f071'; + } + } + + .sky-alert-close { + cursor: pointer; + font-weight: bold; + line-height: 1; + margin: 0; + padding: 0; + color: var(--sky-text-color-default); + border: none; + border-radius: $sky-theme-modern-border-radius-md; + background-color: transparent; + display: none; + flex-shrink: 0; + + &:hover { + opacity: 1; + border: var(--sky-background-color-primary-dark) solid 1px; + } + + &:focus-visible { + border: var(--sky-background-color-primary-dark) solid 2px; + outline: none; + } + + &:active { + border: var(--sky-background-color-primary-dark) solid 2px; + } + + &:focus-visible:not(:active) { + box-shadow: $sky-theme-modern-elevation-1-shadow-size + $sky-theme-modern-elevation-3-shadow-color; + } + } + + &.sky-alert-closeable { + .sky-alert-close { + display: block; + } + } + + .sky-alert-icon-theme-modern { + display: block; + } + + &.sky-alert-info .sky-alert-icon-theme-modern { + color: var(--sky-highlight-color-info); + } + + &.sky-alert-success .sky-alert-icon-theme-modern { + color: var(--sky-highlight-color-success); + } + + &.sky-alert-warning .sky-alert-icon-theme-modern { + color: var(--sky-highlight-color-warning); + } + + &.sky-alert-danger .sky-alert-icon-theme-modern { + color: var(--sky-highlight-color-danger); + } +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-container.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-container.component.scss deleted file mode 100644 index b4ed17f2a5..0000000000 --- a/libs/components/layout/src/lib/modules/action-button/action-button-container.component.scss +++ /dev/null @@ -1,98 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; -@use 'libs/components/theme/src/lib/styles/variables' as *; - -// Host element must be a block to allow FF to make proper getBoundingClientRect() calculations. -:host { - display: block; -} - -.sky-action-button-flex { - sky-action-button { - display: block; - } - - .sky-action-button { - height: 100%; - min-width: 236px; - margin-left: $sky-margin; - margin-right: $sky-margin; - } -} - -@include mixins.sky-host-responsive-container-xs-min(false) { - .sky-action-button-flex { - display: block; - padding: 0; - margin: calc( - var(--sky-compat-action-button-flex-margin, $sky-margin-double) * -1 - ) - 0; - - sky-action-button { - margin: $sky-margin-double 0; - } - } -} - -@include mixins.sky-host-responsive-container-sm-min(false) { - .sky-action-button-flex { - display: flex; - flex-flow: row wrap; - padding: var(--sky-compat-action-button-flex-sm-padding, 0) 0; - margin: calc(var(--sky-compat-action-button-flex-margin, $sky-margin) * -1) - 0; - - &.sky-action-button-flex-align-center { - justify-content: center; - } - - &.sky-action-button-flex-align-left { - justify-content: flex-start; - } - - sky-action-button { - margin: $sky-margin 0; - } - } -} - -.sky-theme-modern { - .sky-action-button-container { - margin: 0 auto; - - // Grid based on the assumption that each action button is 446px wide with 30px inner margins. - &.sky-action-button-container-sm { - max-width: 446px; // 1 action button per row. - } - - &.sky-action-button-container-md { - max-width: 912px; // 2 action buttons per row. - } - - &.sky-action-button-container-lg { - max-width: 1378px; // 3 action buttons per row. - } - - .sky-action-button-flex { - .sky-action-button { - height: auto; - min-width: auto; - margin: 0; - } - } - } - - @include mixins.sky-host-responsive-container-xs-min(false) { - .sky-action-button-flex { - display: flex; - flex-flow: row wrap; - padding: 0; - margin: 0 0 -20px -20px; - - sky-action-button { - margin: 0 0 20px 20px; - flex: 0 1 446px; - } - } - } -} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-container.component.ts b/libs/components/layout/src/lib/modules/action-button/action-button-container.component.ts index 7345a70e25..9652cf9785 100644 --- a/libs/components/layout/src/lib/modules/action-button/action-button-container.component.ts +++ b/libs/components/layout/src/lib/modules/action-button/action-button-container.component.ts @@ -14,7 +14,7 @@ import { ViewEncapsulation, } from '@angular/core'; import { SkyCoreAdapterService } from '@skyux/core'; -import { SkyThemeService } from '@skyux/theme'; +import { SkyThemeComponentClassDirective, SkyThemeService } from '@skyux/theme'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -29,10 +29,14 @@ import { SkyActionButtonContainerAlignItemsType } from './types/action-button-co */ @Component({ selector: 'sky-action-button-container', - styleUrls: ['./action-button-container.component.scss'], + styleUrls: [ + './action-button-container.default.component.scss', + './action-button-container.modern.component.scss', + ], templateUrl: './action-button-container.component.html', providers: [SkyActionButtonAdapterService], encapsulation: ViewEncapsulation.None, + hostDirectives: [SkyThemeComponentClassDirective], }) export class SkyActionButtonContainerComponent implements AfterViewInit, OnDestroy, OnInit diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-container.default.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-container.default.component.scss new file mode 100644 index 0000000000..e8dc7079e2 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-container.default.component.scss @@ -0,0 +1,69 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +// Host element must be a block to allow FF to make proper getBoundingClientRect() calculations. +@include mixins.sky-component-host('default') { + display: block; +} + +@include mixins.sky-component('default', '.sky-action-button-container') { + .sky-action-button-flex { + sky-action-button { + display: block; + } + + .sky-action-button { + height: 100%; + min-width: 236px; + margin-left: $sky-margin; + margin-right: $sky-margin; + } + } +} + +@include mixins.sky-component( + 'default', + '.sky-action-button-container', + false, + 'xs' +) { + .sky-action-button-flex { + display: block; + padding: 0; + margin: calc( + var(--sky-compat-action-button-flex-margin, $sky-margin-double) * -1 + ) + 0; + + sky-action-button { + margin: $sky-margin-double 0; + } + } +} + +@include mixins.sky-component( + 'default', + '.sky-action-button-container', + false, + 'sm' +) { + .sky-action-button-flex { + display: flex; + flex-flow: row wrap; + padding: var(--sky-compat-action-button-flex-sm-padding, 0) 0; + margin: calc(var(--sky-compat-action-button-flex-margin, $sky-margin) * -1) + 0; + + &.sky-action-button-flex-align-center { + justify-content: center; + } + + &.sky-action-button-flex-align-left { + justify-content: flex-start; + } + + sky-action-button { + margin: $sky-margin 0; + } + } +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-container.modern.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-container.modern.component.scss new file mode 100644 index 0000000000..f37f0a1f93 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-container.modern.component.scss @@ -0,0 +1,72 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +// Host element must be a block to allow FF to make proper getBoundingClientRect() calculations. +@include mixins.sky-component-host('modern') { + display: block; +} + +@include mixins.sky-component('modern', '.sky-action-button-container', false) { + margin: 0 auto; + + // Grid based on the assumption that each action button is 446px wide with 30px inner margins. + &.sky-action-button-container-sm { + max-width: 446px; // 1 action button per row. + } + + &.sky-action-button-container-md { + max-width: 912px; // 2 action buttons per row. + } + + &.sky-action-button-container-lg { + max-width: 1378px; // 3 action buttons per row. + } + + .sky-action-button-flex { + sky-action-button { + display: block; + } + + .sky-action-button { + height: auto; + min-width: auto; + margin: 0; + } + } +} + +@include mixins.sky-component( + 'modern', + '.sky-action-button-container', + false, + 'xs' +) { + .sky-action-button-flex { + display: flex; + flex-flow: row wrap; + padding: 0; + margin: 0 0 -20px -20px; + + sky-action-button { + margin: 0 0 20px 20px; + flex: 0 1 446px; + } + } +} + +@include mixins.sky-component( + 'modern', + '.sky-action-button-container', + false, + 'sm' +) { + .sky-action-button-flex { + &.sky-action-button-flex-align-center { + justify-content: center; + } + + &.sky-action-button-flex-align-left { + justify-content: flex-start; + } + } +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-header.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-header.component.scss deleted file mode 100644 index 31c49230ca..0000000000 --- a/libs/components/layout/src/lib/modules/action-button/action-button-header.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; -@use 'libs/components/theme/src/lib/styles/variables' as *; - -@include mixins.sky-host-responsive-container-xs-min() { - .sky-action-button-header { - margin: 0 $sky-margin-half; - } -} - -@include mixins.sky-host-responsive-container-sm-min() { - .sky-action-button-header { - margin: 0 0 $sky-margin-double; - } -} - -@include mixins.sky-theme-modern { - .sky-action-button-header { - margin: 0 0 $sky-theme-modern-space-sm 0; - } -} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-header.component.ts b/libs/components/layout/src/lib/modules/action-button/action-button-header.component.ts index 48dd4c97de..45e25314f5 100644 --- a/libs/components/layout/src/lib/modules/action-button/action-button-header.component.ts +++ b/libs/components/layout/src/lib/modules/action-button/action-button-header.component.ts @@ -1,11 +1,16 @@ import { Component } from '@angular/core'; +import { SkyThemeComponentClassDirective } from '@skyux/theme'; /** * Specifies a header to display on an action button. */ @Component({ selector: 'sky-action-button-header', - styleUrls: ['./action-button-header.component.scss'], + styleUrls: [ + './action-button-header.default.component.scss', + './action-button-header.modern.component.scss', + ], templateUrl: './action-button-header.component.html', + hostDirectives: [SkyThemeComponentClassDirective], }) export class SkyActionButtonHeaderComponent {} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-header.default.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-header.default.component.scss new file mode 100644 index 0000000000..e20c74c5e0 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-header.default.component.scss @@ -0,0 +1,20 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component( + 'default', + '.sky-action-button-header', + true, + 'xs' +) { + margin: 0 $sky-margin-half; +} + +@include mixins.sky-component( + 'default', + '.sky-action-button-header', + true, + 'sm' +) { + margin: 0 0 $sky-margin-double; +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-header.modern.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-header.modern.component.scss new file mode 100644 index 0000000000..202b22aa14 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-header.modern.component.scss @@ -0,0 +1,6 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('modern', '.sky-action-button-header') { + margin: 0 0 $sky-theme-modern-space-sm 0; +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.scss deleted file mode 100644 index 4c526a6deb..0000000000 --- a/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; -@use 'libs/components/theme/src/lib/styles/variables' as *; - -@include mixins.sky-host-responsive-container-xs-min() { - .sky-action-button-icon-container { - margin: 0 $sky-margin-half; - } -} - -@include mixins.sky-host-responsive-container-sm-min() { - .sky-action-button-icon-container { - margin: 0 0 $sky-margin-double; - } -} - -.sky-action-button-icon { - color: $sky-text-color-action-primary; -} - -@include mixins.sky-theme-modern { - .sky-action-button-icon-container { - color: $sky-text-color-action-primary; - background: $sky-color-blue-05; - margin: 0 $sky-space-xl 0 0; - border-radius: 50%; - width: 42px; - height: 42px; - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - - ::ng-deep .sky-icon { - font-size: 24px !important; - } - } -} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.ts b/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.ts index 63d4670df0..a27e80c171 100644 --- a/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.ts +++ b/libs/components/layout/src/lib/modules/action-button/action-button-icon.component.ts @@ -11,7 +11,10 @@ const FONTSIZECLASS_LARGE = '3x'; */ @Component({ selector: 'sky-action-button-icon', - styleUrls: ['./action-button-icon.component.scss'], + styleUrls: [ + './action-button-icon.default.component.scss', + './action-button-icon.modern.component.scss', + ], templateUrl: './action-button-icon.component.html', }) export class SkyActionButtonIconComponent implements OnDestroy { diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-icon.default.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-icon.default.component.scss new file mode 100644 index 0000000000..78dd4cb844 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-icon.default.component.scss @@ -0,0 +1,24 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component( + 'default', + '.sky-action-button-icon-container', + true, + 'xs' +) { + margin: 0 $sky-margin-half; + + .sky-action-button-icon { + color: $sky-text-color-action-primary; + } +} + +@include mixins.sky-component( + 'default', + '.sky-action-button-icon-container', + true, + 'sm' +) { + margin: 0 0 $sky-margin-double; +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button-icon.modern.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button-icon.modern.component.scss new file mode 100644 index 0000000000..3141b913fd --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button-icon.modern.component.scss @@ -0,0 +1,22 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('modern', '.sky-action-button-icon-container') { + color: $sky-text-color-action-primary; + background: $sky-color-blue-05; + margin: 0 $sky-space-xl 0 0; + border-radius: 50%; + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + + ::ng-deep .sky-icon { + font-size: 24px !important; + } + + .sky-action-button-icon { + color: $sky-text-color-action-primary; + } +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button.component.scss deleted file mode 100644 index 27c1633ecf..0000000000 --- a/libs/components/layout/src/lib/modules/action-button/action-button.component.scss +++ /dev/null @@ -1,73 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; -@use 'libs/components/theme/src/lib/styles/variables' as *; - -@include mixins.sky-host-responsive-container-xs-min(false) { - .sky-action-button { - --sky-action-button-padding: #{$sky-padding-double $sky-padding-double - $sky-padding-triple}; - --sky-action-button-max-width: none; - --sky-action-button-margin-bottom: #{$sky-margin-double}; - --sky-action-button-icon-header-container-flex-direction: unset; - } -} - -@include mixins.sky-host-responsive-container-sm-min(false) { - .sky-action-button { - --sky-action-button-padding: #{$sky-padding-triple $sky-padding-double}; - --sky-action-button-max-width: 236px; - --sky-action-button-margin-bottom: 0; - --sky-action-button-icon-header-container-flex-direction: column; - } -} - -.sky-action-button { - @include mixins.sky-border(dark, top, bottom, left, right); - cursor: pointer; - text-align: center; - text-decoration: none !important; - display: block; - padding: var(--sky-action-button-padding); - max-width: var(--sky-action-button-max-width); - - &:hover { - border-color: darken($sky-border-color-neutral-light, 12%); - } -} - -.sky-action-button-icon-header-container { - display: flex; - flex-direction: var(--sky-action-button-icon-header-container-flex-direction); - justify-content: center; - margin-bottom: var(--sky-action-button-margin-bottom); -} - -.sky-theme-modern { - .sky-action-button { - display: flex; - flex-flow: row nowrap; - padding: $sky-theme-modern-padding-even-xl; - text-align: left; - border: none; - - .sky-action-button-content { - flex: 1 1 auto; - margin: 0 $sky-space-md 0 0; - white-space: initial; - } - } - - @include mixins.sky-host-responsive-container-xs-min(false) { - .sky-action-button { - padding: $sky-theme-modern-padding-even-xl; - margin: 0; - max-width: 446px; - } - } - - @include mixins.sky-host-responsive-container-sm-min(false) { - .sky-action-button { - padding: $sky-theme-modern-padding-even-xl; - margin: 0; - } - } -} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button.component.ts b/libs/components/layout/src/lib/modules/action-button/action-button.component.ts index c2dc705aa1..f61bf46ce5 100644 --- a/libs/components/layout/src/lib/modules/action-button/action-button.component.ts +++ b/libs/components/layout/src/lib/modules/action-button/action-button.component.ts @@ -17,7 +17,10 @@ import { SkyActionButtonPermalink } from './action-button-permalink'; */ @Component({ selector: 'sky-action-button', - styleUrls: ['./action-button.component.scss'], + styleUrls: [ + './action-button.default.component.scss', + './action-button.modern.component.scss', + ], templateUrl: './action-button.component.html', encapsulation: ViewEncapsulation.None, }) diff --git a/libs/components/layout/src/lib/modules/action-button/action-button.default.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button.default.component.scss new file mode 100644 index 0000000000..b334350a6d --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button.default.component.scss @@ -0,0 +1,38 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('default', '.sky-action-button', false, 'xs') { + --sky-action-button-padding: #{$sky-padding-double $sky-padding-double + $sky-padding-triple}; + --sky-action-button-max-width: none; + --sky-action-button-margin-bottom: #{$sky-margin-double}; + --sky-action-button-icon-header-container-flex-direction: unset; + + @include mixins.sky-border(dark, top, bottom, left, right); + cursor: pointer; + text-align: center; + text-decoration: none !important; + display: block; + padding: var(--sky-action-button-padding); + max-width: var(--sky-action-button-max-width); + + &:hover { + border-color: darken($sky-border-color-neutral-light, 12%); + } + + .sky-action-button-icon-header-container { + display: flex; + flex-direction: var( + --sky-action-button-icon-header-container-flex-direction + ); + justify-content: center; + margin-bottom: var(--sky-action-button-margin-bottom); + } +} + +@include mixins.sky-component('default', '.sky-action-button', false, 'sm') { + --sky-action-button-padding: #{$sky-padding-triple $sky-padding-double}; + --sky-action-button-max-width: 236px; + --sky-action-button-margin-bottom: 0; + --sky-action-button-icon-header-container-flex-direction: column; +} diff --git a/libs/components/layout/src/lib/modules/action-button/action-button.modern.component.scss b/libs/components/layout/src/lib/modules/action-button/action-button.modern.component.scss new file mode 100644 index 0000000000..0183740228 --- /dev/null +++ b/libs/components/layout/src/lib/modules/action-button/action-button.modern.component.scss @@ -0,0 +1,41 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('modern', '.sky-action-button', false, 'xs') { + --sky-action-button-margin-bottom: #{$sky-margin-double}; + --sky-action-button-icon-header-container-flex-direction: unset; + + cursor: pointer; + text-decoration: none !important; + max-width: 446px; + display: flex; + flex-flow: row nowrap; + padding: $sky-theme-modern-padding-even-xl; + text-align: left; + border: none; + margin: 0; + + &:hover { + border-color: darken($sky-border-color-neutral-light, 12%); + } + + .sky-action-button-content { + flex: 1 1 auto; + margin: 0 $sky-space-md 0 0; + white-space: initial; + } + + .sky-action-button-icon-header-container { + display: flex; + flex-direction: var( + --sky-action-button-icon-header-container-flex-direction + ); + justify-content: center; + margin-bottom: var(--sky-action-button-margin-bottom); + } +} + +@include mixins.sky-component('modern', '.sky-action-button', false, 'sm') { + --sky-action-button-margin-bottom: 0; + --sky-action-button-icon-header-container-flex-direction: column; +} 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/_mixins.scss b/libs/components/theme/src/lib/styles/_public-api/_mixins.scss index 1d6a13803e..1835acffb1 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,114 @@ } } +@mixin sky-component($theme, $selector, $encapsulate: true, $breakpoint: '') { + @if $breakpoint == '' { + @include sky-component-theme($theme, $selector, $encapsulate) { + @content; + } + } @else if $breakpoint == 'xs' { + @include sky-host-responsive-container-xs-min($encapsulate) { + @include sky-component-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'sm' { + @include sky-host-responsive-container-sm-min($encapsulate) { + @include sky-component-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'md' { + @include sky-host-responsive-container-md-min($encapsulate) { + @include sky-component-theme($theme, $selector, $encapsulate) { + @content; + } + } + } @else if $breakpoint == 'lg' { + @include sky-host-responsive-container-lg-min($encapsulate) { + @include sky-component-theme($theme, $selector, $encapsulate) { + @content; + } + } + } +} +@mixin sky-component-theme($theme, $selector, $encapsulate: true) { + @if $theme == 'default' { + #{$selector}:not(.sky-theme-modern *) { + @content; + } + } @else { + @if $encapsulate { + :host-context(.sky-theme-modern) #{$selector} { + @content; + } + } @else { + .sky-theme-modern #{$selector} { + @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/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(); + }); + } +} From 26c36f5d745315fcd79c8a1392455fdf5735e1a4 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 2 Jul 2024 14:18:23 -0400 Subject: [PATCH 03/95] fix(code-examples): satisfy ESLint rules for action button and AG Grid code examples (#2423) --- apps/code-examples/src/app/app.component.ts | 2 +- apps/code-examples/src/app/app.module.ts | 2 +- .../basic/context-menu.component.ts | 18 +++++----- .../ag-grid/data-entry-grid/basic/data.ts | 2 +- .../data-entry-grid/basic/demo.component.ts | 19 +++++++---- .../basic/edit-modal.component.ts | 23 ++++++------- .../context-menu.component.ts | 8 +++-- .../data-manager-added/demo.component.ts | 4 +-- .../edit-modal.component.ts | 23 ++++++------- .../filter-modal.component.ts | 10 +++--- .../data-manager-added/filters.ts | 4 +++ .../data-manager-added/view-grid.component.ts | 27 +++++++++------ .../inline-help/context-menu.component.ts | 8 +++-- .../data-entry-grid/inline-help/data.ts | 2 +- .../inline-help/demo.component.ts | 18 ++++++---- .../inline-help/edit-modal.component.ts | 23 ++++++------- .../context-menu.component.ts | 8 +++-- .../basic-multiselect/demo.component.ts | 16 ++++++--- .../data-grid/basic/context-menu.component.ts | 8 +++-- .../ag-grid/data-grid/basic/demo.component.ts | 18 ++++++---- .../context-menu.component.ts | 8 +++-- .../demo.component.ts | 3 +- .../filter-modal.component.ts | 10 +++--- .../data-manager-multiselect/filters.ts | 4 +++ .../view-grid.component.ts | 27 +++++++++------ .../data-manager/context-menu.component.ts | 8 +++-- .../data-grid/data-manager/demo.component.ts | 3 +- .../data-manager/filter-modal.component.ts | 10 +++--- .../ag-grid/data-grid/data-manager/filters.ts | 4 +++ .../data-manager/view-grid.component.ts | 30 ++++++++++------ .../inline-help/context-menu.component.ts | 8 +++-- .../data-grid/inline-help/demo.component.ts | 12 ++++--- .../paging/context-menu.component.ts | 8 +++-- .../data-grid/paging/demo.component.ts | 34 +++++++++++-------- .../top-scroll/context-menu.component.ts | 8 +++-- .../data-grid/top-scroll/demo.component.ts | 16 ++++++--- .../data-manager/basic/demo.component.ts | 3 +- .../basic/filter-modal.component.ts | 11 +++--- .../data-manager/basic/filters.ts | 4 +++ .../data-manager/basic/view-grid.component.ts | 19 ++++++----- .../basic/view-repeater.component.ts | 30 +++++++++------- apps/code-examples/src/main.ts | 5 ++- 42 files changed, 312 insertions(+), 196 deletions(-) create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/data-manager-added/filters.ts create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager-multiselect/filters.ts create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-grid/data-manager/filters.ts create mode 100644 apps/code-examples/src/app/code-examples/data-manager/data-manager/basic/filters.ts 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/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/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); + }); From 7d0d79b3ed0cc3b333f24672b7e2cd2a816f3a24 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 2 Jul 2024 16:04:22 -0400 Subject: [PATCH 04/95] fix(code-examples): satisfy ESLint rules for colorpicker code examples (#2424) --- .../colorpicker/basic/demo.component.ts | 42 ++++++++++++------- .../programmatic/demo.component.ts | 39 ++++++++++------- 2 files changed, 51 insertions(+), 30 deletions(-) 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 }); } } From e222a736564509baee593daa877d80bd42d8f966 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Wed, 3 Jul 2024 14:56:38 -0400 Subject: [PATCH 05/95] fix(code-examples): satisfy ESLint rules for datetime code examples (#2427) --- .../custom-calculator/demo.component.ts | 2 +- .../datepicker/basic/demo.component.html | 2 +- .../datepicker/basic/demo.component.ts | 43 ++++++++++-------- .../custom-dates/demo.component.html | 2 +- .../datepicker/custom-dates/demo.component.ts | 2 + .../timepicker/basic/demo.component.ts | 44 ++++++++++++------- 6 files changed, 58 insertions(+), 37 deletions(-) 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; - } } From 3d61fe0c635209111a8a7b2de09492f48475d82f Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Wed, 3 Jul 2024 15:36:34 -0400 Subject: [PATCH 06/95] feat(components/icon): add internal support for SVG-based icons (#2433) --- .cspell.json | 2 +- .../src/e2e/icon.component.cy.ts | 3 + .../src/app/icon/icon.component.html | 42 ++++++ .../indicators/icon/icon-routing.module.ts | 26 ++++ .../indicators/icon/icon.component.html | 40 ++++++ .../indicators/icon/icon.component.ts | 17 +++ .../components/indicators/icon/icon.module.ts | 13 ++ .../indicators/indicators.module.ts | 4 + .../icon/icon-svg-resolver.service.spec.ts | 129 ++++++++++++++++++ .../src/lib/icon/icon-svg-resolver.service.ts | 116 ++++++++++++++++ .../icon/src/lib/icon/icon-svg.component.html | 7 + .../src/lib/icon/icon-svg.component.spec.ts | 99 ++++++++++++++ .../icon/src/lib/icon/icon-svg.component.ts | 61 +++++++++ .../lib/icon/icon-svg.default.component.scss | 40 ++++++ .../lib/icon/icon-svg.modern.component.scss | 40 ++++++ .../icon/src/lib/icon/icon.component.html | 36 ++--- .../icon/src/lib/icon/icon.component.ts | 7 + .../icon/src/lib/icon/icon.module.ts | 6 +- 18 files changed, 670 insertions(+), 18 deletions(-) create mode 100644 apps/playground/src/app/components/indicators/icon/icon-routing.module.ts create mode 100644 apps/playground/src/app/components/indicators/icon/icon.component.html create mode 100644 apps/playground/src/app/components/indicators/icon/icon.component.ts create mode 100644 apps/playground/src/app/components/indicators/icon/icon.module.ts create mode 100644 libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts create mode 100644 libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts create mode 100644 libs/components/icon/src/lib/icon/icon-svg.component.html create mode 100644 libs/components/icon/src/lib/icon/icon-svg.component.spec.ts create mode 100644 libs/components/icon/src/lib/icon/icon-svg.component.ts create mode 100644 libs/components/icon/src/lib/icon/icon-svg.default.component.scss create mode 100644 libs/components/icon/src/lib/icon/icon-svg.modern.component.scss 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/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..fff685dfec 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,46 @@ {{ fa }} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Some text. +
+
+ + + +
+
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..2487c616e9 100644 --- a/apps/playground/src/app/components/indicators/indicators.module.ts +++ b/apps/playground/src/app/components/indicators/indicators.module.ts @@ -7,6 +7,10 @@ const routes: Routes = [ loadChildren: () => import('./alert/alert.module').then((m) => m.AlertModule), }, + { + path: 'icon', + loadChildren: () => import('./icon/icon.module').then((m) => m.IconModule), + }, { path: 'illustration', loadChildren: () => diff --git a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts new file mode 100644 index 0000000000..b5b4f4bb43 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts @@ -0,0 +1,129 @@ +import { provideHttpClient } from '@angular/common/http'; +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { firstValueFrom } from 'rxjs'; + +import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; +import { SkyIconVariantType } from './types/icon-variant-type'; + +describe('Icon SVG resolver service', () => { + let resolverSvc: SkyIconSvgResolverService; + let httpTestingController: HttpTestingController; + let spriteLoaded = false; + + function buildSymbolHtml( + name: string, + size: number, + variant: SkyIconVariantType, + ): string { + return ` + +`; + } + + async function validate( + name: string, + expectedHref?: string, + size?: number, + variant?: SkyIconVariantType, + expectedError?: string, + ): Promise { + const hrefPromise = firstValueFrom( + resolverSvc.resolveHref(name, size, variant), + ); + + if (!spriteLoaded) { + const testRequest = httpTestingController.expectOne( + 'https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg', + ); + + testRequest.flush(` + ${buildSymbolHtml('single-size', 12, 'line')} + ${buildSymbolHtml('single-size', 12, 'solid')} + ${buildSymbolHtml('multi-size', 12, 'line')} + ${buildSymbolHtml('multi-size', 12, 'solid')} + ${buildSymbolHtml('multi-size', 24, 'line')} + ${buildSymbolHtml('multi-size', 24, 'solid')} + ${buildSymbolHtml('multi-size', 48, 'line')} + ${buildSymbolHtml('multi-size', 48, 'solid')} + `); + + spriteLoaded = true; + } + + if (expectedError) { + await expectAsync(hrefPromise).toBeRejectedWithError(expectedError); + } else if (expectedHref) { + await expectAsync(hrefPromise).toBeResolvedTo(expectedHref); + } + } + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + SkyIconSvgResolverService, + ], + }); + + resolverSvc = TestBed.inject(SkyIconSvgResolverService); + httpTestingController = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + document.getElementById('sky-icon-svg-sprite')?.remove(); + spriteLoaded = false; + }); + + it('should resolve the expected variant', async () => { + await validate('single-size', '#sky-i-single-size-12-line', 12, 'line'); + await validate('single-size', '#sky-i-single-size-12-solid', 12, 'solid'); + }); + + it('should throw an error when a matching icon is not found', async () => { + await validate( + 'invalid', + undefined, + undefined, + undefined, + `Icon with name 'invalid' was not found.`, + ); + }); + + describe('with single size icons', () => { + it('should resolve the expected icon regardless of specified size', async () => { + await validate('single-size', '#sky-i-single-size-12-line'); + await validate('single-size', '#sky-i-single-size-12-line', 1); + await validate('single-size', '#sky-i-single-size-12-line', 100); + }); + }); + + describe('with multiple size icons', () => { + it('should resolve to the icon size that is an exact match of the specified size', async () => { + await validate('multi-size', '#sky-i-multi-size-12-line', 12); + }); + + it('should resolve to the icon size closest to the specified size when no exact match exists', async () => { + await validate('multi-size', '#sky-i-multi-size-12-line', -Infinity); + await validate('multi-size', '#sky-i-multi-size-12-line', -1); + await validate('multi-size', '#sky-i-multi-size-12-line', 0); + await validate('multi-size', '#sky-i-multi-size-12-line', 11); + await validate('multi-size', '#sky-i-multi-size-12-line', 13); + await validate('multi-size', '#sky-i-multi-size-12-line', 17); + await validate('multi-size', '#sky-i-multi-size-24-line', 18); + await validate('multi-size', '#sky-i-multi-size-24-line', 20); + await validate('multi-size', '#sky-i-multi-size-48-line', 37); + await validate('multi-size', '#sky-i-multi-size-48-line', 100); + await validate('multi-size', '#sky-i-multi-size-48-line', Infinity); + }); + + it('should resolve to the icon size closest to the default size when size is not specified', async () => { + await validate('multi-size', '#sky-i-multi-size-12-line'); + }); + }); +}); diff --git a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts new file mode 100644 index 0000000000..3623fa5200 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts @@ -0,0 +1,116 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; + +import { Observable, map, shareReplay, tap } from 'rxjs'; + +import { SkyIconVariantType } from './types/icon-variant-type'; + +function insertSprite(markup: string): void { + document.body.insertAdjacentHTML('afterbegin', markup); +} + +function getIconsSizes(): Map { + const iconsSizes = Array.from( + document.querySelectorAll('#sky-icon-svg-sprite symbol'), + ).reduce((map, el) => { + const idParts = el.id.split('-'); + + // Construct the icon name by removing `sky-i-` from the beginning + // and `--` from the end. + const name = idParts.slice(2, idParts.length - 2).join('-'); + + let sizes = map.get(name); + + if (!sizes) { + sizes = []; + map.set(name, sizes); + } + + // The penultimate segment is the size for which the icon has + // been optimized. + sizes.push(+idParts[idParts.length - 2]); + + return map; + }, new Map()); + + // Sort all the sizes for later comparison. + for (const id of iconsSizes.keys()) { + // Dedupe and sort the icon sizes. + iconsSizes.set(id, [...new Set(iconsSizes.get(id))].sort()); + } + + return iconsSizes; +} + +function getNearestSize( + iconsSizes: Map, + name: string, + pixelSize: number, +): number | undefined { + const sizes = iconsSizes.get(name); + + if (sizes) { + let nearestSizeUnder = -Infinity; + let nearestSizeOver = Infinity; + + for (const availableSize of sizes) { + if (availableSize === pixelSize) { + return pixelSize; + } else if (availableSize < pixelSize) { + nearestSizeUnder = availableSize; + } else { + nearestSizeOver = availableSize; + break; + } + } + + const underDiff = Math.abs(pixelSize - nearestSizeUnder); + const overDiff = Math.abs(pixelSize - nearestSizeOver); + + return isNaN(overDiff) || underDiff < overDiff + ? nearestSizeUnder + : nearestSizeOver; + } + + return undefined; +} + +/** + * @internal + */ +@Injectable() +export class SkyIconSvgResolverService { + readonly #http = inject(HttpClient); + + readonly #spriteObs = this.#http + .get( + `https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg`, + { + responseType: 'text', + }, + ) + .pipe(tap(insertSprite), map(getIconsSizes), shareReplay(1)); + + public resolveHref( + name: string, + pixelSize = 16, + variant: SkyIconVariantType = 'line', + ): Observable { + return this.#spriteObs.pipe( + map((iconsSizes) => { + let href = `#sky-i-${name}`; + + // Find the icon with the optimal size nearest to the requested size. + const nearestSize = getNearestSize(iconsSizes, name, pixelSize); + + if (!nearestSize) { + throw new Error(`Icon with name '${name}' was not found.`); + } + + href = `${href}-${nearestSize}-${variant}`; + + return href; + }), + ); + } +} diff --git a/libs/components/icon/src/lib/icon/icon-svg.component.html b/libs/components/icon/src/lib/icon/icon-svg.component.html new file mode 100644 index 0000000000..60813b7854 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg.component.html @@ -0,0 +1,7 @@ + diff --git a/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts b/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts new file mode 100644 index 0000000000..041f946961 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts @@ -0,0 +1,99 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; + +import { of } from 'rxjs'; + +import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; +import { SkyIconSvgComponent } from './icon-svg.component'; +import { SkyIconModule } from './icon.module'; + +describe('Icon SVG component', () => { + let resolverSvc: jasmine.SpyObj; + let fixture: ComponentFixture; + + function detectUrlChanges(): void { + fixture.detectChanges(); + + // Resolve icon ID Observable and apply changes. + tick(); + fixture.detectChanges(); + } + + function getSvgEl(): SVGElement { + return fixture.nativeElement.querySelector('.sky-icon-svg-img'); + } + + function validateIconId(expectedId: string): void { + const useEl = getSvgEl().querySelector('use'); + + expect(useEl?.href.baseVal).toBe(expectedId); + } + + beforeEach(() => { + resolverSvc = jasmine.createSpyObj( + 'SkyIconSvgResolverService', + ['resolveHref'], + ); + + resolverSvc.resolveHref.and.callFake((src, size, variant) => { + return of(`#${src}-${size}-${variant ?? 'line'}`); + }); + + TestBed.configureTestingModule({ + imports: [SkyIconModule], + providers: [ + { + provide: SkyIconSvgResolverService, + useValue: resolverSvc, + }, + ], + }); + + fixture = TestBed.createComponent(SkyIconSvgComponent); + }); + + it('should display the resolved icon by ID', fakeAsync(() => { + fixture.componentRef.setInput('iconName', 'test'); + detectUrlChanges(); + + validateIconId('#test-16-line'); + })); + + it('should display the resolved icon by ID and size', fakeAsync(() => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconSize', '2x'); + detectUrlChanges(); + + validateIconId('#test-32-line'); + })); + + it('should display the resolved icon by ID and variant', fakeAsync(() => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconVariant', 'solid'); + detectUrlChanges(); + + validateIconId('#test-16-solid'); + })); + + it("should use the host element's text color as its fill color", fakeAsync(() => { + fixture.nativeElement.style.color = '#0f0'; + + fixture.componentRef.setInput('iconName', 'test'); + detectUrlChanges(); + + expect(getComputedStyle(getSvgEl()).fill).toBe('rgb(0, 255, 0)'); + })); + + it('should handle errors', fakeAsync(() => { + resolverSvc.resolveHref.and.throwError('Icon could not be resolved'); + + fixture.componentRef.setInput('iconName', 'test'); + detectUrlChanges(); + + validateIconId(''); + })); +}); diff --git a/libs/components/icon/src/lib/icon/icon-svg.component.ts b/libs/components/icon/src/lib/icon/icon-svg.component.ts new file mode 100644 index 0000000000..fe1b1bcee6 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg.component.ts @@ -0,0 +1,61 @@ +import { NgClass, NgStyle } from '@angular/common'; +import { Component, computed, inject, input } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; + +import { catchError, of, switchMap } from 'rxjs'; + +import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; +import { SkyIconVariantType } from './types/icon-variant-type'; + +const SIZE_BASE = 16; + +const SIZES = new Map([ + ['', SIZE_BASE], + ['lg', 21.333 /* SIZE_BASE * (4/3) */], + ['2x', 32 /* SIZE_BASE * 2 */], + ['3x', 48 /* SIZE_BASE * 3 */], + ['4x', 64 /* SIZE_BASE * 4 */], + ['5x', 80 /* SIZE_BASE * 5 */], +]); + +/** + * @internal + */ +@Component({ + selector: 'sky-icon-svg', + standalone: true, + imports: [NgClass, NgStyle], + templateUrl: './icon-svg.component.html', + styleUrls: [ + './icon-svg.default.component.scss', + './icon-svg.modern.component.scss', + ], +}) +export class SkyIconSvgComponent { + readonly #resolverSvc = inject(SkyIconSvgResolverService); + + public readonly iconName = input.required(); + public readonly iconSize = input(); + public readonly iconVariant = input(); + + readonly #iconInfo = computed(() => { + return { + src: this.iconName(), + size: this.iconSize(), + variant: this.iconVariant(), + }; + }); + + protected readonly iconHref = toSignal( + toObservable(this.#iconInfo).pipe( + switchMap((info) => + this.#resolverSvc.resolveHref( + info.src, + SIZES.get(info.size ?? ''), + info.variant, + ), + ), + catchError(() => of('')), + ), + ); +} diff --git a/libs/components/icon/src/lib/icon/icon-svg.default.component.scss b/libs/components/icon/src/lib/icon/icon-svg.default.component.scss new file mode 100644 index 0000000000..6aedbe1f57 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg.default.component.scss @@ -0,0 +1,40 @@ +:host { + display: inline-block; + --sky-icon-svg-img-size: 1em; + --sky-icon-svg-img-top: 0.15em; +} + +.sky-icon-svg-img { + display: inline-block; + fill: var(--sky-icon-svg-color-input, currentColor); + height: var(--sky-icon-svg-img-size); + width: var(--sky-icon-svg-img-size); + text-align: center; + position: relative; + top: var(--sky-icon-svg-img-top); + + &-lg { + --sky-icon-svg-img-size: 1.333em; + --sky-icon-svg-img-top: 0.2em; + } + + &-2x { + --sky-icon-svg-img-size: 2em; + --sky-icon-svg-img-top: 0.3em; + } + + &-3x { + --sky-icon-svg-img-size: 3em; + --sky-icon-svg-img-top: 0.45em; + } + + &-4x { + --sky-icon-svg-img-size: 4em; + --sky-icon-svg-img-top: 0.6em; + } + + &-5x { + --sky-icon-svg-img-size: 5em; + --sky-icon-svg-img-top: 0.75em; + } +} diff --git a/libs/components/icon/src/lib/icon/icon-svg.modern.component.scss b/libs/components/icon/src/lib/icon/icon-svg.modern.component.scss new file mode 100644 index 0000000000..6aedbe1f57 --- /dev/null +++ b/libs/components/icon/src/lib/icon/icon-svg.modern.component.scss @@ -0,0 +1,40 @@ +:host { + display: inline-block; + --sky-icon-svg-img-size: 1em; + --sky-icon-svg-img-top: 0.15em; +} + +.sky-icon-svg-img { + display: inline-block; + fill: var(--sky-icon-svg-color-input, currentColor); + height: var(--sky-icon-svg-img-size); + width: var(--sky-icon-svg-img-size); + text-align: center; + position: relative; + top: var(--sky-icon-svg-img-top); + + &-lg { + --sky-icon-svg-img-size: 1.333em; + --sky-icon-svg-img-top: 0.2em; + } + + &-2x { + --sky-icon-svg-img-size: 2em; + --sky-icon-svg-img-top: 0.3em; + } + + &-3x { + --sky-icon-svg-img-size: 3em; + --sky-icon-svg-img-top: 0.45em; + } + + &-4x { + --sky-icon-svg-img-size: 4em; + --sky-icon-svg-img-top: 0.6em; + } + + &-5x { + --sky-icon-svg-img-size: 5em; + --sky-icon-svg-img-top: 0.75em; + } +} diff --git a/libs/components/icon/src/lib/icon/icon.component.html b/libs/components/icon/src/lib/icon/icon.component.html index 6e13c23139..8b10843504 100644 --- a/libs/components/icon/src/lib/icon/icon.component.html +++ b/libs/components/icon/src/lib/icon/icon.component.html @@ -1,16 +1,20 @@ - +@if (iconName) { + +} @else { + +} diff --git a/libs/components/icon/src/lib/icon/icon.component.ts b/libs/components/icon/src/lib/icon/icon.component.ts index 7fa667cdd6..ec4aaac3cc 100644 --- a/libs/components/icon/src/lib/icon/icon.component.ts +++ b/libs/components/icon/src/lib/icon/icon.component.ts @@ -25,6 +25,13 @@ export class SkyIconComponent { @Input() public icon: string | undefined; + /** + * The name of the Blackbaud SVG icon to display. For internal use only. + * @internal + */ + @Input() + public iconName: string | undefined; + /** * The type of icon to display. Specifying `"fa"` displays a Font Awesome icon, * while specifying `"skyux"` displays an icon from the custom SKY UX icon font. Note that diff --git a/libs/components/icon/src/lib/icon/icon.module.ts b/libs/components/icon/src/lib/icon/icon.module.ts index 27d78ef048..8babdc9107 100644 --- a/libs/components/icon/src/lib/icon/icon.module.ts +++ b/libs/components/icon/src/lib/icon/icon.module.ts @@ -1,13 +1,17 @@ import { CommonModule } from '@angular/common'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { SkyIconClassListPipe } from './icon-class-list.pipe'; import { SkyIconStackComponent } from './icon-stack.component'; +import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; +import { SkyIconSvgComponent } from './icon-svg.component'; import { SkyIconComponent } from './icon.component'; @NgModule({ declarations: [SkyIconClassListPipe, SkyIconComponent, SkyIconStackComponent], - imports: [CommonModule], + imports: [CommonModule, SkyIconSvgComponent], exports: [SkyIconComponent, SkyIconStackComponent], + providers: [provideHttpClient(withFetch()), SkyIconSvgResolverService], }) export class SkyIconModule {} From 8768cf9ecbadaa7845d9093f3c2ed1516487095a Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Wed, 3 Jul 2024 17:07:59 -0400 Subject: [PATCH 07/95] fix(components/forms): file attachment components report file size errors with appropriate orders of magnitude (#2437) --- .../src/assets/locales/resources_en_US.json | 8 +++---- .../file-attachment.component.html | 4 ++-- .../file-attachment.component.spec.ts | 4 ++-- .../file-attachment/file-drop.component.html | 8 +++++-- .../file-drop.component.spec.ts | 6 ++--- .../file-attachment/file-size.pipe.spec.ts | 22 +++++++++++++++++-- .../modules/file-attachment/file-size.pipe.ts | 6 ++++- .../shared/sky-forms-resources.module.ts | 8 +++---- 8 files changed, 46 insertions(+), 20 deletions(-) diff --git a/libs/components/forms/src/assets/locales/resources_en_US.json b/libs/components/forms/src/assets/locales/resources_en_US.json index a7b930c6d7..620010086c 100644 --- a/libs/components/forms/src/assets/locales/resources_en_US.json +++ b/libs/components/forms/src/assets/locales/resources_en_US.json @@ -185,19 +185,19 @@ }, "skyux_file_attachment_max_file_size_error_label_text": { "_description": "The error message for file attachment max file size error", - "message": "Upload a file under {0}KB." + "message": "Upload a file under {0}." }, "skyux_file_attachment_max_file_size_error_label_text_with_name": { "_description": "The error message for file attachment max file size error", - "message": "{0}: Upload a file under {1}KB." + "message": "{0}: Upload a file under {1}." }, "skyux_file_attachment_min_file_size_error_label_text": { "_description": "The error message for file attachment min file size error", - "message": "Upload a file over {0}KB." + "message": "Upload a file over {0}." }, "skyux_file_attachment_min_file_size_error_label_text_with_name": { "_description": "The error message for file attachment min file size error", - "message": "{0}: Upload a file over {1}KB." + "message": "{0}: Upload a file over {1}." }, "skyux_file_attachment_label_no_file_chosen": { "_description": "Label for single file attachment when no file is chosen", diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html index a7ddb455ea..bb9c2e624a 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html @@ -187,7 +187,7 @@ [errorName]="'maxFileSize'" [errorText]=" 'skyux_file_attachment_max_file_size_error_label_text' - | skyLibResources: fileErrorParam + | skyLibResources: (fileErrorParam | skyFileSize) " /> diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts index e0efbac628..1e32ba8549 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts @@ -1453,7 +1453,7 @@ describe('File attachment', () => { expect( fixture.nativeElement.querySelector('sky-form-error')?.textContent.trim(), - ).toBe('Error: Upload a file under 50KB.'); + ).toBe('Error: Upload a file under 50 bytes.'); }); it('should render file errors and NgControl errors when label text is set', () => { @@ -1482,7 +1482,7 @@ describe('File attachment', () => { fixture.nativeElement .querySelectorAll('sky-form-error')[1] ?.textContent.trim(), - ).toBe('Error: Upload a file under 50KB.'); + ).toBe('Error: Upload a file under 50 bytes.'); }); it('should render `labelText` and not label element if `labelText` is set', () => { diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html index 269cbe2bf9..91fe8a77f6 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html @@ -190,7 +190,9 @@ errorName="maxFileSize" [errorText]=" 'skyux_file_attachment_max_file_size_error_label_text_with_name' - | skyLibResources: rejectedFile.file.name : rejectedFile.errorParam + | skyLibResources + : rejectedFile.file.name + : (rejectedFile.errorParam | skyFileSize) " /> { componentInstance.labelText = 'Label'; componentInstance.minFileSize = 1500; - componentInstance.maxFileSize = 3000; + componentInstance.maxFileSize = 3073; componentInstance.acceptedTypes = 'image/png, image/jpeg'; fixture.detectChanges(); @@ -465,11 +465,11 @@ describe('File drop component', () => { expect(minSizeError).toBeVisible(); expect(minSizeError.textContent).toContain( - 'foo.txt: Upload a file over 1500KB.', + 'foo.txt: Upload a file over 1 KB.', ); expect(maxSizeError).toBeVisible(); expect(maxSizeError.textContent).toContain( - 'bar.jpeg: Upload a file under 3000KB.', + 'bar.jpeg: Upload a file under 3 KB.', ); expect(typeError).toBeVisible(); diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.spec.ts index a3d4a49ec5..9dcc2efa6c 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.spec.ts @@ -10,9 +10,9 @@ describe('File size pipe', () => { let decimalPipe: DecimalPipe; function validateFormatted( - value: number | undefined | null, + value: number | string | undefined | null, expected: string, - ) { + ): void { const result = fileSizePipe.transform(value); expect(result).toBe(expected); @@ -34,6 +34,9 @@ describe('File size pipe', () => { validateFormatted(1, '1 byte'); validateFormatted(100, '100 bytes'); validateFormatted(999, '999 bytes'); + validateFormatted('1', '1 byte'); + validateFormatted('100', '100 bytes'); + validateFormatted('999', '999 bytes'); }); it('should format kilobytes', function () { @@ -41,6 +44,10 @@ describe('File size pipe', () => { validateFormatted(1024, '1 KB'); validateFormatted(102400, '100 KB'); validateFormatted(1022976, '999 KB'); + validateFormatted('1000', '1,000 bytes'); + validateFormatted('1024', '1 KB'); + validateFormatted('102400', '100 KB'); + validateFormatted('1022976', '999 KB'); }); it('should format megabytes', function () { @@ -49,6 +56,11 @@ describe('File size pipe', () => { validateFormatted(1992300, '1.9 MB'); validateFormatted(104857600, '100 MB'); validateFormatted(1048471150, '999.9 MB'); + validateFormatted('1048575', '1,023 KB'); + validateFormatted('1048576', '1 MB'); + validateFormatted('1992300', '1.9 MB'); + validateFormatted('104857600', '100 MB'); + validateFormatted('1048471150', '999.9 MB'); }); it('should format gigabytes', function () { @@ -56,11 +68,17 @@ describe('File size pipe', () => { validateFormatted(1073741824, '1 GB'); validateFormatted(107374182400, '100 GB'); validateFormatted(1073634449818, '999.9 GB'); + validateFormatted('1073741823', '1,023.9 MB'); + validateFormatted('1073741824', '1 GB'); + validateFormatted('107374182400', '100 GB'); + validateFormatted('1073634449818', '999.9 GB'); }); it('should format values over 1,000 gigabytes as gigabytes', function () { validateFormatted(1073741824000, '1,000 GB'); validateFormatted(10737310865818, '9,999.9 GB'); + validateFormatted('1073741824000', '1,000 GB'); + validateFormatted('10737310865818', '9,999.9 GB'); }); it('should return an empty string when the input is null or undefined', function () { diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.ts b/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.ts index 959a450819..d2d326acee 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-size.pipe.ts @@ -20,7 +20,7 @@ export class SkyFileSizePipe implements PipeTransform { this.#resourcesService = resourcesService; } - public transform(input?: number | null): string { + public transform(input?: number | string | null): string { let decimalPlaces = 0, dividend = 1, template: string; @@ -29,6 +29,10 @@ export class SkyFileSizePipe implements PipeTransform { return ''; } + if (typeof input === 'string') { + input = Number.parseInt(input); + } + if (Math.abs(input) === 1) { template = 'skyux_file_attachment_file_size_b_singular'; } else if (input < 1024) { diff --git a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts index 31f0552add..44110f2b64 100644 --- a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts +++ b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts @@ -119,16 +119,16 @@ const RESOURCES: Record = { message: '{0}: Upload a file of type {1}.', }, skyux_file_attachment_max_file_size_error_label_text: { - message: 'Upload a file under {0}KB.', + message: 'Upload a file under {0}.', }, skyux_file_attachment_max_file_size_error_label_text_with_name: { - message: '{0}: Upload a file under {1}KB.', + message: '{0}: Upload a file under {1}.', }, skyux_file_attachment_min_file_size_error_label_text: { - message: 'Upload a file over {0}KB.', + message: 'Upload a file over {0}.', }, skyux_file_attachment_min_file_size_error_label_text_with_name: { - message: '{0}: Upload a file over {1}KB.', + message: '{0}: Upload a file over {1}.', }, skyux_file_attachment_label_no_file_chosen: { message: 'No file chosen.' }, skyux_file_attachment_required: { message: 'Required' }, From ffa79466cfa356db4495ec4b5116d9668b0cb54f Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Mon, 8 Jul 2024 12:53:09 -0400 Subject: [PATCH 08/95] fix(components/forms): improve default value handling for heading styles on form group components (#2420) --- .../forms/radio/radio.component.html | 10 ++++++++-- .../components/forms/radio/radio.component.ts | 11 +++++++++++ .../checkbox/checkbox-group.component.spec.ts | 11 ++++++++--- .../checkbox/checkbox-group.component.ts | 15 +++++++++------ .../standard-checkbox-group.component.ts | 2 +- .../field-group/field-group.component.spec.ts | 18 +++++++++++++++--- .../field-group/field-group.component.ts | 8 ++++++-- .../fixtures/field-group.component.fixture.ts | 6 ++++-- .../radio-group-reactive.component.fixture.ts | 2 +- .../radio/radio-group.component.spec.ts | 11 ++++++++--- .../lib/modules/radio/radio-group.component.ts | 16 ++++++++++------ 11 files changed, 81 insertions(+), 29 deletions(-) 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/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.spec.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.spec.ts index 33f39eb094..27c169f786 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.spec.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.spec.ts @@ -88,7 +88,12 @@ describe('Checkbox group component', function () { 4, 5, ]; - const headingStyles: SkyCheckboxGroupHeadingStyle[] = [3, 4, 5]; + const headingStyles: (SkyCheckboxGroupHeadingStyle | undefined)[] = [ + undefined, + 3, + 4, + 5, + ]; headingLevels.forEach((headingLevel) => { headingStyles.forEach((headingStyle) => { componentInstance.headingLevel = headingLevel; @@ -96,8 +101,8 @@ describe('Checkbox group component', function () { fixture.detectChanges(); const selector = headingLevel - ? `h${headingLevel}.sky-font-heading-${headingStyle}` - : `span.sky-font-heading-${headingStyle}`; + ? `h${headingLevel}.sky-font-heading-${headingStyle ?? 4}` + : `span.sky-font-heading-${headingStyle ?? 4}`; const heading = fixture.nativeElement.querySelector(selector); expect(heading).toExist(); diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts index db2ff21657..5382f0c5d2 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts @@ -24,6 +24,10 @@ import { SkyFormsResourcesModule } from '../shared/sky-forms-resources.module'; import { SkyCheckboxGroupHeadingLevel } from './checkbox-group-heading-level'; import { SkyCheckboxGroupHeadingStyle } from './checkbox-group-heading-style'; +function numberAttribute4(value: unknown): number { + return numberAttribute(value, 4); +} + /** * Organizes checkboxes into a group. */ @@ -93,8 +97,10 @@ export class SkyCheckboxGroupComponent implements Validator { * The heading [font style](https://developer.blackbaud.com/skyux/design/styles/typography#headings). * @default 4 */ - @Input({ transform: numberAttribute }) - public headingStyle: SkyCheckboxGroupHeadingStyle = 4; + @Input({ transform: numberAttribute4 }) + public set headingStyle(value: SkyCheckboxGroupHeadingStyle) { + this.headingClass = `sky-font-heading-${value}`; + } /** * [Persistent inline help text](https://developer.blackbaud.com/skyux/design/guidelines/user-assistance#inline-help) that provides @@ -137,12 +143,9 @@ export class SkyCheckboxGroupComponent implements Validator { @HostBinding('class.sky-margin-stacked-xl') public stackedXL = false; - public get headingClass(): string { - return `sky-font-heading-${this.headingStyle}`; - } - readonly #idSvc = inject(SkyIdService); protected errorId = this.#idSvc.generateId(); + protected headingClass = 'sky-font-heading-4'; protected formErrorsDataId = 'checkbox-group-form-errors'; protected formGroup: FormGroup | null | undefined; diff --git a/libs/components/forms/src/lib/modules/checkbox/fixtures/standard-checkbox-group.component.ts b/libs/components/forms/src/lib/modules/checkbox/fixtures/standard-checkbox-group.component.ts index ad965bf8c2..da858ba2a3 100644 --- a/libs/components/forms/src/lib/modules/checkbox/fixtures/standard-checkbox-group.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/fixtures/standard-checkbox-group.component.ts @@ -32,7 +32,7 @@ export class SkyStandardCheckboxGroupComponent { public hintText: string | undefined; public headingHidden = false; public headingLevel: SkyCheckboxGroupHeadingLevel | undefined = 3; - public headingStyle: SkyCheckboxGroupHeadingStyle = 3; + public headingStyle: SkyCheckboxGroupHeadingStyle | undefined = 3; public required: boolean | undefined = false; public stacked: boolean | undefined = true; diff --git a/libs/components/forms/src/lib/modules/field-group/field-group.component.spec.ts b/libs/components/forms/src/lib/modules/field-group/field-group.component.spec.ts index fbd967e834..1973fc40b6 100644 --- a/libs/components/forms/src/lib/modules/field-group/field-group.component.spec.ts +++ b/libs/components/forms/src/lib/modules/field-group/field-group.component.spec.ts @@ -5,6 +5,8 @@ import { SkyHelpTestingModule, } from '@skyux/core/testing'; +import { SkyFieldGroupHeadingLevel } from './field-group-heading-level'; +import { SkyFieldGroupHeadingStyle } from './field-group-heading-style'; import { FieldGroupComponent } from './fixtures/field-group.component.fixture'; describe('Field group component', function () { @@ -65,14 +67,24 @@ describe('Field group component', function () { }); it('should render the correct heading level and styles', () => { - [3, 4].forEach((headingLevel) => { - [3, 4].forEach((headingStyle) => { + const headingLevels: (SkyFieldGroupHeadingLevel | undefined)[] = [ + undefined, + 3, + 4, + ]; + const headingStyles: (SkyFieldGroupHeadingStyle | undefined)[] = [ + undefined, + 3, + 4, + ]; + headingLevels.forEach((headingLevel) => { + headingStyles.forEach((headingStyle) => { componentInstance.headingLevel = headingLevel; componentInstance.headingStyle = headingStyle; fixture.detectChanges(); const heading = fixture.nativeElement.querySelector( - `h${headingLevel}.sky-font-heading-${headingStyle}`, + `h${headingLevel ?? 3}.sky-font-heading-${headingStyle ?? 3}`, ); expect(heading).toExist(); diff --git a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts index 82a3d7ed0e..724f3fc7cb 100644 --- a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts +++ b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts @@ -15,6 +15,10 @@ import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label import { SkyFieldGroupHeadingLevel } from './field-group-heading-level'; import { SkyFieldGroupHeadingStyle } from './field-group-heading-style'; +function numberAttribute3(value: unknown): number { + return numberAttribute(value, 3); +} + /** * Organizes form fields into a group. */ @@ -58,14 +62,14 @@ export class SkyFieldGroupComponent { * The semantic heading level in the document structure. * @default 3 */ - @Input({ transform: numberAttribute }) + @Input({ transform: numberAttribute3 }) public headingLevel: SkyFieldGroupHeadingLevel = 3; /** * The heading [font style](https://developer.blackbaud.com/skyux/design/styles/typography#headings). * @default 3 */ - @Input({ transform: numberAttribute }) + @Input({ transform: numberAttribute3 }) public set headingStyle(value: SkyFieldGroupHeadingStyle) { this.headingClass = `sky-font-heading-${value}`; } diff --git a/libs/components/forms/src/lib/modules/field-group/fixtures/field-group.component.fixture.ts b/libs/components/forms/src/lib/modules/field-group/fixtures/field-group.component.fixture.ts index d034d5dcb5..a426cb0e14 100644 --- a/libs/components/forms/src/lib/modules/field-group/fixtures/field-group.component.fixture.ts +++ b/libs/components/forms/src/lib/modules/field-group/fixtures/field-group.component.fixture.ts @@ -8,6 +8,8 @@ import { } from '@angular/forms'; import { SkyInputBoxModule } from '../../input-box/input-box.module'; +import { SkyFieldGroupHeadingLevel } from '../field-group-heading-level'; +import { SkyFieldGroupHeadingStyle } from '../field-group-heading-style'; import { SkyFieldGroupModule } from '../field-group.module'; @Component({ @@ -27,8 +29,8 @@ export class FieldGroupComponent { public headingText = 'Heading text'; public hintText: string | undefined; public headingHidden = false; - public headingStyle = 3; - public headingLevel = 3; + public headingStyle: SkyFieldGroupHeadingStyle | undefined = 3; + public headingLevel: SkyFieldGroupHeadingLevel | undefined = 3; public helpKey: string | undefined; public helpPopoverContent: string | undefined; public helpPopoverTitle: string | undefined; diff --git a/libs/components/forms/src/lib/modules/radio/fixtures/radio-group-reactive.component.fixture.ts b/libs/components/forms/src/lib/modules/radio/fixtures/radio-group-reactive.component.fixture.ts index a08683745d..3e7f911755 100644 --- a/libs/components/forms/src/lib/modules/radio/fixtures/radio-group-reactive.component.fixture.ts +++ b/libs/components/forms/src/lib/modules/radio/fixtures/radio-group-reactive.component.fixture.ts @@ -55,7 +55,7 @@ export class SkyRadioGroupReactiveFixtureComponent implements OnInit { public headingLevel: SkyRadioGroupHeadingLevel | undefined = 3; - public headingStyle: SkyRadioGroupHeadingStyle = 3; + public headingStyle: SkyRadioGroupHeadingStyle | undefined = 3; @ViewChild(SkyRadioGroupComponent) public radioGroupComponent: SkyRadioGroupComponent | undefined; diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts b/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts index c674df67bb..13f9a34afe 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts @@ -646,7 +646,12 @@ describe('Radio group component (reactive)', function () { 4, 5, ]; - const headingStyles: SkyRadioGroupHeadingStyle[] = [3, 4, 5]; + const headingStyles: (SkyRadioGroupHeadingStyle | undefined)[] = [ + undefined, + 3, + 4, + 5, + ]; headingLevels.forEach((headingLevel) => { headingStyles.forEach((headingStyle) => { componentInstance.headingText = 'Label text'; @@ -655,8 +660,8 @@ describe('Radio group component (reactive)', function () { fixture.detectChanges(); const selector = headingLevel - ? `h${headingLevel}.sky-font-heading-${headingStyle}` - : `span.sky-font-heading-${headingStyle}`; + ? `h${headingLevel}.sky-font-heading-${headingStyle ?? 4}` + : `span.sky-font-heading-${headingStyle ?? 4}`; const heading = fixture.nativeElement.querySelector(selector); expect(heading).toExist(); diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts index a61fb8ddb6..a1ea5178eb 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts @@ -33,6 +33,10 @@ import { SkyRadioGroupHeadingStyle } from './types/radio-group-heading-style'; let nextUniqueId = 0; +function numberAttribute4(value: unknown): number { + return numberAttribute(value, 4); +} + /** * Organizes radio buttons into a group. It is required for radio * buttons on Angular reactive forms, and we recommend using it with all radio buttons. @@ -132,8 +136,10 @@ export class SkyRadioGroupComponent * The heading [font style](https://developer.blackbaud.com/skyux/design/styles/typography#headings). * @default 4 */ - @Input({ transform: numberAttribute }) - public headingStyle: SkyRadioGroupHeadingStyle = 4; + @Input({ transform: numberAttribute4 }) + public set headingStyle(value: SkyRadioGroupHeadingStyle) { + this.headingClass = `sky-font-heading-${value}`; + } /** * The name for the collection of radio buttons that the component groups together. @@ -257,10 +263,6 @@ export class SkyRadioGroupComponent @Input() public helpKey: string | undefined; - public get headingClass(): string { - return `sky-font-heading-${this.headingStyle}`; - } - /** * Our radio components are usually implemented using an unordered list. This is an * accessibility violation because the unordered list has an implicit role which @@ -282,6 +284,8 @@ export class SkyRadioGroupComponent @HostBinding('class.sky-margin-stacked-xl') public stackedXL = false; + protected headingClass = 'sky-font-heading-4'; + #controlValue: any; #defaultName = `sky-radio-group-${nextUniqueId++}`; From 89276711d0b4a19a54e9cd1f00e72df3c0ee1596 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Mon, 8 Jul 2024 14:02:54 -0400 Subject: [PATCH 09/95] feat(components/help-inline): update documentation and deprecate `indicators/help-inline` (#2428) --- apps/playground/src/app/app.module.ts | 3 +- .../help-inline/help-inline.component.html | 13 +----- .../help-inline/help-inline.component.html | 20 ++++++++++ .../help-inline/help-inline.component.ts | 16 ++++++++ .../help-inline/help-inline.module.ts | 40 +++++++++++++++++++ .../indicators/indicators.module.ts | 7 ++++ .../help-inline/help-inline.component.ts | 8 ++-- .../src/help-inline/help-inline-harness.ts | 6 +-- .../help-inline/help-inline.component.spec.ts | 18 ++++++++- .../help-inline/help-inline.component.ts | 14 ++++++- 10 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 apps/playground/src/app/components/indicators/help-inline/help-inline.component.html create mode 100644 apps/playground/src/app/components/indicators/help-inline/help-inline.component.ts create mode 100644 apps/playground/src/app/components/indicators/help-inline/help-inline.module.ts diff --git a/apps/playground/src/app/app.module.ts b/apps/playground/src/app/app.module.ts index 16de4563b2..5ca3ab0f93 100644 --- a/apps/playground/src/app/app.module.ts +++ b/apps/playground/src/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { SkyHelpService } from '@skyux/core'; +import { SKY_LOG_LEVEL, SkyHelpService, SkyLogLevel } from '@skyux/core'; import { SkyFluidGridModule } from '@skyux/layout'; import { SkyThemeService } from '@skyux/theme'; @@ -25,6 +25,7 @@ import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selecto providers: [ SkyThemeService, { provide: SkyHelpService, useClass: PlaygroundHelpService }, + { provide: SKY_LOG_LEVEL, useValue: SkyLogLevel.Info }, ], bootstrap: [AppComponent], }) 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..19172e39d8 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/indicators.module.ts b/apps/playground/src/app/components/indicators/indicators.module.ts index 2487c616e9..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,13 @@ 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), diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts index 55c5d41172..9a71b5b7ce 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts @@ -20,7 +20,6 @@ import { SkyHelpInlineAriaHaspopupPipe } from './help-inline-aria-haspopup.pipe' /** * Inserts a help button beside an element, such as a field, to display contextual information about the element. - * @internal */ @Component({ selector: 'sky-help-inline', @@ -44,7 +43,6 @@ export class SkyHelpInlineComponent { * The ID of the element that the help inline button controls. * This property [supports accessibility rules for disclosures](https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure). * For more information about the `aria-controls` attribute, see the [WAI-ARIA definition](https://www.w3.org/TR/wai-aria/#aria-controls). - * @internal */ @Input() public ariaControls: string | undefined; @@ -65,7 +63,7 @@ export class SkyHelpInlineComponent { public ariaLabel: string | undefined; /** - * A unique key that identifies the global help content to display when the button is clicked. + * A unique key that identifies the [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) content to display when the button is clicked. */ @Input() public helpKey: string | undefined; @@ -79,13 +77,13 @@ export class SkyHelpInlineComponent { public labelledBy: string | undefined; /** - * The label of the component help inline is attached to. + * The label of the component the help inline button is attached to. */ @Input() public labelText: string | undefined; /** - * The content of the popover. When specified, clicking the help inline button opens a popover with this content and optional title. + * The content of the help popover. When specified, clicking the help inline button opens a popover with this content and optional title. */ @Input() public set popoverContent(value: string | TemplateRef | undefined) { diff --git a/libs/components/help-inline/testing/src/help-inline/help-inline-harness.ts b/libs/components/help-inline/testing/src/help-inline/help-inline-harness.ts index 3445e75463..4360f34544 100644 --- a/libs/components/help-inline/testing/src/help-inline/help-inline-harness.ts +++ b/libs/components/help-inline/testing/src/help-inline/help-inline-harness.ts @@ -9,7 +9,7 @@ import { import { SkyHelpInlineHarnessFilters } from './help-inline-harness.filters'; /** - * Harness for interacting with a help inline component in tests. + * Harness for interacting with a help inline button component in tests. */ export class SkyHelpInlineHarness extends SkyComponentHarness { /** @@ -103,7 +103,7 @@ export class SkyHelpInlineHarness extends SkyComponentHarness { } /** - * Gets the help inline popover content. + * Gets the help popover content. */ public async getPopoverContent(): Promise< TemplateRef | string | undefined @@ -112,7 +112,7 @@ export class SkyHelpInlineHarness extends SkyComponentHarness { } /** - * Gets the help inline popover title. + * Gets the help popover title. */ public async getPopoverTitle(): Promise { return (await this.#getPopoverHarnessContent())?.getTitleText(); diff --git a/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.spec.ts b/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.spec.ts index 84d90efd36..1a60c0b0f4 100644 --- a/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.spec.ts +++ b/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.spec.ts @@ -2,6 +2,7 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { expect, expectAsync } from '@skyux-sdk/testing'; +import { SkyLogService } from '@skyux/core'; import { SkyHelpInlineModule } from '../help-inline/help-inline.module'; @@ -24,13 +25,16 @@ describe('Help inline component', () => { let fixture: ComponentFixture; let cmp: HelpInlineTestComponent; let debugElement: DebugElement; + let logService: SkyLogService; + let deprecatedLogSpy: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ declarations: [HelpInlineTestComponent], imports: [BrowserModule, SkyHelpInlineModule], }); - + logService = TestBed.inject(SkyLogService); + deprecatedLogSpy = spyOn(logService, 'deprecated'); fixture = TestBed.createComponent(HelpInlineTestComponent); cmp = fixture.componentInstance as HelpInlineTestComponent; debugElement = fixture.debugElement; @@ -38,6 +42,18 @@ describe('Help inline component', () => { fixture.detectChanges(); }); + it('should log that the component is deprecated', () => { + fixture.detectChanges(); + expect(deprecatedLogSpy).toHaveBeenCalledWith( + 'SkyHelpInlineComponent', + Object({ + deprecationMajorVersion: 10, + replacementRecommendation: + 'Use the help inline button component in the `@skyux/help-inline` library instead.', + }), + ); + }); + it('should emit a click event on button click', () => { debugElement .query(By.css('.sky-help-inline')) diff --git a/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.ts b/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.ts index a8f7988a8f..bc75ebf039 100644 --- a/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.ts +++ b/libs/components/indicators/src/lib/modules/help-inline/help-inline.component.ts @@ -1,5 +1,9 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { SkyLogService } from '@skyux/core'; +/** + * @deprecated Use the help inline button component in the `@skyux/help-inline` library instead. + */ @Component({ selector: 'sky-help-inline', templateUrl: './help-inline.component.html', @@ -38,6 +42,14 @@ export class SkyHelpInlineComponent { @Output() public actionClick = new EventEmitter(); + constructor() { + inject(SkyLogService).deprecated('SkyHelpInlineComponent', { + deprecationMajorVersion: 10, + replacementRecommendation: + 'Use the help inline button component in the `@skyux/help-inline` library instead.', + }); + } + public onClick(): void { this.actionClick.emit(); } From febfde3967f5833d602df199efd351334fe51a20 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Mon, 8 Jul 2024 16:27:44 -0400 Subject: [PATCH 10/95] chore: release 10.34.0 (#2426) --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4195a3cacc..d3613d0b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [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/package-lock.json b/package-lock.json index b7ce3d247d..9817f6dc16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.33.0", + "version": "10.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.33.0", + "version": "10.34.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 49af080df0..2a0833d69e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.33.0", + "version": "10.34.0", "license": "MIT", "scripts": { "ng": "nx", From 6bd118283fbccbab4c67764d061a51da2b0c80cd Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 9 Jul 2024 08:20:55 -0400 Subject: [PATCH 11/95] fix(code-examples): satisfy ESLint rules for core, forms, indicators, and inline form code examples (#2432) --- .../core/numeric/basic/demo.component.spec.ts | 26 ++++++++++--------- .../character-count/demo.component.spec.ts | 11 ++++---- .../file-attachment/basic/demo.component.ts | 2 +- .../input-box/basic/demo.component.spec.ts | 24 ++++++++++------- .../forms/radio/standard/demo.component.ts | 26 +++++++++++-------- .../selection-box/checkbox/demo.component.ts | 19 ++------------ .../selection-box/radio/demo.component.ts | 19 ++------------ .../basic/demo.component.ts | 4 +-- .../illustration-demo-resolver.service.ts | 11 ++++---- .../tokens/custom/demo.component.spec.ts | 14 ++++++---- .../wait/element/demo.component.spec.ts | 5 +++- .../wait/page/demo.component.spec.ts | 14 ++++++---- .../indicators/wait/page/demo.component.ts | 8 +++--- .../inline-form/basic/demo.component.html | 10 +++---- .../inline-form/basic/demo.component.ts | 14 ++++++---- .../custom-buttons/demo.component.html | 10 +++---- .../custom-buttons/demo.component.ts | 16 +++++++----- .../inline-form/repeaters/demo.component.ts | 20 +++++++++----- 18 files changed, 128 insertions(+), 125 deletions(-) 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/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/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; } } From 84e1382a2233b381c8c8c859b38bd8f743ef672f Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 9 Jul 2024 13:14:12 -0400 Subject: [PATCH 12/95] fix(components/phone-field): required phone field controls are not "touched" on initialization (#2443) --- .../app/phone-field/phone-field.component.ts | 1 + .../phone-field-adapter.service.ts | 11 + .../phone-field-input.directive.ts | 385 +++++++----------- .../phone-field/phone-field.component.html | 121 +++--- .../phone-field/phone-field.component.spec.ts | 40 +- .../phone-field/phone-field.component.ts | 39 +- 6 files changed, 275 insertions(+), 322 deletions(-) diff --git a/apps/e2e/phone-field-storybook/src/app/phone-field/phone-field.component.ts b/apps/e2e/phone-field-storybook/src/app/phone-field/phone-field.component.ts index ae7e7de956..1d9a1619bc 100644 --- a/apps/e2e/phone-field-storybook/src/app/phone-field/phone-field.component.ts +++ b/apps/e2e/phone-field-storybook/src/app/phone-field/phone-field.component.ts @@ -30,6 +30,7 @@ export class PhoneFieldComponent implements AfterViewInit { phoneControlError: this.phoneControlError, }); this.phoneControlError.setValue('bbb'); + this.phoneControlError.markAsTouched(); this.phoneControlInput = new UntypedFormControl(); this.phoneFormInput = new UntypedFormGroup({ 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..ffe43bc89c 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, 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,17 @@ 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.#notifyChange?.(this.#getValue()); + this.#notifyTouched?.(); }); - - // 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(); - } } public ngOnDestroy(): void { @@ -170,198 +129,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..3c66617af6 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 @@ -210,8 +210,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 +222,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 +233,7 @@ describe('Phone Field Component', () => { } else { expect(model?.errors).toEqual({ skyPhoneField: { - invalid: formattedValue, + invalid: modelValue, }, }); } @@ -539,7 +539,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 +558,7 @@ describe('Phone Field Component', () => { '667-555-530', '667-555-530', false, - true, + false, ngModel, fixture, ); @@ -615,7 +615,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 +702,7 @@ describe('Phone Field Component', () => { '667-555-5309ext3', '(667) 555-5309 ext. 3', false, - true, + false, ngModel, fixture, ); @@ -867,7 +867,7 @@ describe('Phone Field Component', () => { '+3556675555309', '+3556675555309', false, - true, + false, ngModel, fixture, ); @@ -1238,7 +1238,7 @@ describe('Phone Field Component', () => { '+12045555555', '+1 204-555-5555', true, - true, + false, ngModel, fixture, ); @@ -1297,7 +1297,7 @@ describe('Phone Field Component', () => { fixture.detectChanges(); tick(); - validateInputAndModel('', '', true, false, ngModel, fixture); + validateInputAndModel('', undefined, true, false, ngModel, fixture); setCountry('Albania', fixture); @@ -1308,7 +1308,7 @@ describe('Phone Field Component', () => { '024569874', '+355 24 569 874', true, - false, + true, ngModel, fixture, ); @@ -1606,7 +1606,7 @@ describe('Phone Field Component', () => { '1234', '1234', false, - true, + false, component.phoneControl, fixture, ); @@ -1632,7 +1632,7 @@ describe('Phone Field Component', () => { '667-555-530', '667-555-530', false, - true, + false, component.phoneControl, fixture, ); @@ -1686,7 +1686,7 @@ describe('Phone Field Component', () => { '1234', '1234', false, - true, + false, component.phoneControl, fixture, ); @@ -1778,7 +1778,7 @@ describe('Phone Field Component', () => { '667-555-5309ext3', '(667) 555-5309 ext. 3', false, - true, + false, component.phoneControl, fixture, ); @@ -1793,7 +1793,7 @@ describe('Phone Field Component', () => { '8675558309', '(867) 555-8309', false, - true, + false, component.phoneControl, fixture, ); @@ -1974,7 +1974,7 @@ describe('Phone Field Component', () => { '+3556675555309', '+3556675555309', false, - true, + false, component.phoneControl, fixture, ); @@ -2178,7 +2178,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..53e4d48fcb 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,8 +241,6 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { } this.#populateInputBoxHelpText(); - - this.selectedCountryChange.emit(this.#_selectedCountry); } } @@ -278,7 +276,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { #longestDialCodeLength = 0; - #_defaultCountry: string | undefined; + #_defaultCountry = DEFAULT_COUNTRY_CODE; #_selectedCountry: SkyPhoneFieldCountry | undefined; @@ -314,6 +312,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 +321,8 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { } } + this.#defaultCountryData = this.#getDefaultCountryData(); + this.countrySearchForm = this.#formBuilder.group({ countrySearch: this.#countrySearchFormControl, }); @@ -347,13 +348,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); @@ -361,6 +357,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { (newValue: SkyCountryFieldCountry | undefined | null) => { if (newValue?.iso2 !== this.selectedCountry?.iso2) { this.selectedCountry = newValue || undefined; + this.selectedCountryChange.emit(this.selectedCountry); } }, ); @@ -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( From 082130e05c500792ce900f33f9a3c69725f4bd2e Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:20:37 -0400 Subject: [PATCH 13/95] build: update implicit dependencies for testing (#2449) --- libs/components/indicators/testing/project.json | 8 +------- libs/components/modals/testing/project.json | 8 +++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/components/indicators/testing/project.json b/libs/components/indicators/testing/project.json index f9c6ad9b50..8fa6635b45 100644 --- a/libs/components/indicators/testing/project.json +++ b/libs/components/indicators/testing/project.json @@ -5,13 +5,7 @@ "sourceRoot": "libs/components/indicators/testing/src", "prefix": "sky", "tags": ["testing"], - "implicitDependencies": [ - "core-testing", - "icon", - "indicators", - "testing", - "theme" - ], + "implicitDependencies": ["core-testing", "indicators", "testing"], "targets": { "build": { "command": "echo ' 🏗️ build indicators-testing'", 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'", From a3d1a968d8a37442146220a7691acad38b13683b Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:00:50 -0400 Subject: [PATCH 14/95] ci: update changelog cherry-pick message (#2451) --- .github/workflows/cherry-pick.yml | 4 +++- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 091020596a..8737e15e0c 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -72,6 +72,8 @@ jobs: echo "CHERRY_PICK_RESULT=failed" >> $GITHUB_ENV exit 0 fi + + echo "COMMIT_MESSAGE=$(git log -1 --pretty=%B)" >> $GITHUB_ENV env: GH_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} @@ -91,7 +93,7 @@ jobs: repo: context.repo.repo, head: process.env.CHERRY_PICK_BRANCH, base: process.env.TARGET_BRANCH, - title: `${pr.title} (#${pr.number})`, + title: `${process.env.COMMIT_MESSAGE} (#${pr.number})`, body }).then(result => { console.log(`Created PR #${result.data.number}: ${result.data.html_url}`); diff --git a/package-lock.json b/package-lock.json index 9817f6dc16..99cc66a7bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "@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", + "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.7", "@storybook/addon-a11y": "8.1.10", "@storybook/addon-actions": "8.1.10", "@storybook/addon-controls": "8.1.10", @@ -9146,8 +9146,8 @@ } }, "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.7", + "resolved": "git+ssh://git@github.com/blackbaud/skyux-dev-infra-private-builds.git#8f26d24cfaee3d0c6eddb1f5d5eecb31f430bb50", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2a0833d69e..fc4f7b5415 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "@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", + "@skyux/dev-infra-private": "github:blackbaud/skyux-dev-infra-private-builds#10.0.0-alpha.7", "@storybook/addon-a11y": "8.1.10", "@storybook/addon-actions": "8.1.10", "@storybook/addon-controls": "8.1.10", From b228868a59b14715b09dc278c1fcf00829ea674e Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Wed, 10 Jul 2024 10:03:19 -0400 Subject: [PATCH 15/95] chore(components/indicators): split expansion indicator styles (#2453) --- .../expansion-indicator.component.ts | 5 +- ...xpansion-indicator.default.component.scss} | 47 ++----------- .../expansion-indicator.modern.component.scss | 70 +++++++++++++++++++ 3 files changed, 78 insertions(+), 44 deletions(-) rename libs/components/indicators/src/lib/modules/expansion-indicator/{expansion-indicator.component.scss => expansion-indicator.default.component.scss} (55%) create mode 100644 libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.modern.component.scss diff --git a/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.ts b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.ts index 290f311878..de6205e565 100644 --- a/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.ts +++ b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.ts @@ -6,7 +6,10 @@ import { Component, Input } from '@angular/core'; */ @Component({ selector: 'sky-expansion-indicator', - styleUrls: ['./expansion-indicator.component.scss'], + styleUrls: [ + './expansion-indicator.default.component.scss', + './expansion-indicator.modern.component.scss', + ], templateUrl: './expansion-indicator.component.html', }) export class SkyExpansionIndicatorComponent { diff --git a/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.scss b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.default.component.scss similarity index 55% rename from libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.scss rename to libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.default.component.scss index b794266009..5ef833b1bd 100644 --- a/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.component.scss +++ b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.default.component.scss @@ -1,7 +1,7 @@ @use 'libs/components/theme/src/lib/styles/mixins' as mixins; @use 'libs/components/theme/src/lib/styles/variables' as *; -.sky-expansion-indicator { +@include mixins.sky-component('default', '.sky-expansion-indicator') { display: inline-block; border: none; background-color: transparent; @@ -14,7 +14,7 @@ vertical-align: top; } -.sky-expansion-indicator-part { +@include mixins.sky-component('default', '.sky-expansion-indicator-part') { border-color: $sky-text-color-icon-borderless; border-style: solid; border-width: 3px 0 0 0; @@ -29,7 +29,7 @@ width: 10px; } -.sky-expansion-indicator-up { +@include mixins.sky-component('default', '.sky-expansion-indicator-up') { .sky-expansion-indicator-left { left: 7px; transform: rotate(-45deg); @@ -41,7 +41,7 @@ } } -.sky-expansion-indicator-down { +@include mixins.sky-component('default', '.sky-expansion-indicator-down') { .sky-expansion-indicator-left { left: 2px; transform: rotate(45deg); @@ -52,42 +52,3 @@ transform: rotate(-45deg); } } - -@include mixins.sky-theme-modern { - .sky-expansion-indicator { - height: 26px; - width: 26px; - } - - .sky-expansion-indicator-part { - background: $sky-theme-modern-font-deemphasized-color; - border: none; - height: 2px; - width: 11px; - top: 13px; - } - - .sky-expansion-indicator-glyph-container { - transform: scale(0.68); - display: inline-block; - position: absolute; - top: 3.5px; - left: 4px; - } - - .sky-expansion-indicator-left { - border-radius: 1px 0 0 1px; - } - - .sky-expansion-indicator-right { - border-radius: 0 1px 1px 0; - } - - .sky-expansion-indicator-left { - left: 4px; - } - - .sky-expansion-indicator-right { - left: 10.5px; - } -} diff --git a/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.modern.component.scss b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.modern.component.scss new file mode 100644 index 0000000000..03422a184f --- /dev/null +++ b/libs/components/indicators/src/lib/modules/expansion-indicator/expansion-indicator.modern.component.scss @@ -0,0 +1,70 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('modern', '.sky-expansion-indicator') { + display: inline-block; + border: none; + background-color: transparent; + flex-shrink: 0; + height: 26px; + margin: 0; + overflow: hidden; + position: relative; + vertical-align: top; + width: 26px; +} + +@include mixins.sky-component('modern', '.sky-expansion-indicator-part') { + background: $sky-theme-modern-font-deemphasized-color; + border: none; + display: inline-block; + height: 2px; + position: absolute; + top: 13px; + transition: + transform $sky-transition-time-medium, + left $sky-transition-time-medium; + vertical-align: top; + width: 11px; +} + +@include mixins.sky-component( + 'modern', + '.sky-expansion-indicator-glyph-container' +) { + left: 4px; + display: inline-block; + position: absolute; + top: 3.5px; + transform: scale(0.68); +} + +@include mixins.sky-component('modern', '.sky-expansion-indicator-left') { + border-radius: 1px 0 0 1px; + left: 4px; +} + +@include mixins.sky-component('modern', '.sky-expansion-indicator-right') { + border-radius: 0 1px 1px 0; + left: 10.5px; +} + +@include mixins.sky-component('modern', '.sky-expansion-indicator-up') { + .sky-expansion-indicator-left { + transform: rotate(-45deg); + } + + .sky-expansion-indicator-right { + transform: rotate(45deg); + } +} + +@include mixins.sky-component('modern', '.sky-expansion-indicator-down') { + .sky-expansion-indicator-left { + transform: rotate(45deg); + } + + .sky-expansion-indicator-right { + transform: rotate(-45deg); + } +} From 73a47639b6349818b42de5c8d4dcbbf402c13884 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Wed, 10 Jul 2024 10:18:38 -0400 Subject: [PATCH 16/95] fix(code-examples): satisfy ESLint rules for layout, lists, lookup, modals, pages, and others (#2435) --- apps/code-examples/.eslintrc.json | 7 ++ .../infinite-scroll/demo.component.ts | 15 +-- .../layout/box/basic/demo.component.spec.ts | 2 +- .../lists/filter/modal/demo.component.ts | 2 +- .../repeater/demo.component.ts | 15 +-- .../paging/with-content/demo.component.ts | 8 +- .../add-remove/demo.component.spec.ts | 105 +++++++++--------- .../repeater/inline-form/demo.component.ts | 19 ++-- .../lists/sort/basic/demo.component.ts | 2 +- .../autocomplete/advanced/demo.component.ts | 2 +- .../custom-search/demo.component.ts | 2 +- .../country-field/basic/demo.component.html | 2 +- .../country-field/basic/demo.component.ts | 44 ++++---- .../lookup/add-item/demo.component.spec.ts | 2 +- .../lookup/lookup/add-item/demo.component.ts | 14 ++- .../lookup/async/demo.component.spec.ts | 2 +- .../lookup/lookup/async/demo.component.ts | 3 +- .../custom-picker/demo.component.spec.ts | 2 +- .../lookup/custom-picker/demo.component.ts | 4 +- .../custom-picker/picker-modal.component.ts | 9 +- .../lookup/multi-select/demo.component.ts | 2 +- .../result-templates/demo.component.spec.ts | 2 +- .../lookup/result-templates/demo.component.ts | 2 +- .../lookup/single-select/demo.component.ts | 2 +- .../lookup/search/basic/demo.component.ts | 4 +- .../add-item/demo.component.spec.ts | 19 ++-- .../add-item/demo.component.ts | 9 +- .../basic/demo.component.spec.ts | 18 +-- .../demo.component.spec.ts | 12 +- .../basic-with-harness/demo.component.spec.ts | 9 +- .../basic-with-controller/demo.component.ts | 4 +- .../basic-with-harness/demo.component.ts | 3 +- .../modals/modal/with-error/demo.component.ts | 3 +- .../modal/with-error/modal.component.ts | 2 +- .../dashboards-grid-context-menu.component.ts | 6 +- .../demo.component.spec.ts | 2 +- .../page/list-page-list-layout-demo/item.ts | 5 + .../list-page-content.component.ts | 3 +- .../contact-context-menu.component.ts | 6 +- .../list-page-tabs-layout-demo/contact.ts | 5 + .../demo.component.spec.ts | 2 +- .../list-page-content.component.ts | 3 +- .../demo.component.spec.ts | 2 +- .../attachment.ts | 6 + ...attachments-grid-context-menu.component.ts | 6 +- .../demo.component.spec.ts | 2 +- .../record-page-tabs-layout-demo/detail.ts | 4 + .../record-page-attachments-tab.component.ts | 3 +- .../record-page-overview-tab.component.ts | 4 +- .../demo.component.spec.ts | 2 +- .../custom-sky-href-resolver.service.spec.ts | 58 +++++----- .../custom-sky-href-resolver.service.ts | 41 ++++--- .../split-view/basic/demo.component.ts | 26 +++-- .../split-view/page-bound/demo.component.ts | 26 +++-- .../modal/information-form.component.ts | 23 ++-- .../text-editor/text-editor/demo.component.ts | 17 ++- 56 files changed, 350 insertions(+), 254 deletions(-) create mode 100644 apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/item.ts create mode 100644 apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/contact.ts create mode 100644 apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/attachment.ts create mode 100644 apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/detail.ts 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/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.spec.ts b/apps/code-examples/src/app/code-examples/layout/box/basic/demo.component.spec.ts index 44bac8fd95..04debb4409 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; 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.spec.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.spec.ts index 7bc096e035..110644bef1 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/add-item/demo.component.spec.ts @@ -31,7 +31,7 @@ describe('Lookup asynchronous search 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']); + 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.spec.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.spec.ts index c7e3d3865d..fc8c35b894 100644 --- 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 @@ -31,7 +31,7 @@ describe('Lookup asynchronous search 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']); + mockSvc = jasmine.createSpyObj('DemoService', ['search']); TestBed.configureTestingModule({ imports: [DemoComponent, NoopAnimationsModule], 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 index f3b6edf0b7..1fd8d8d5b5 100644 --- 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 @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { + AbstractControl, FormBuilder, FormControl, FormGroup, @@ -52,7 +53,7 @@ export class DemoComponent implements OnInit { constructor() { const names = new FormControl([{ name: 'Shirley' }], { validators: [ - (control): ValidationErrors => { + (control: AbstractControl): ValidationErrors => { if ( control.value?.some((person: Person) => !person.name.match(/e/i)) ) { diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.spec.ts index 9ccbd06f5f..71003017f5 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.component.spec.ts @@ -44,7 +44,7 @@ describe('Lookup custom picker demo', () => { await lookupHarness.enterText('Be'); const allResultHarnesses = await lookupHarness.getSearchResults(); - const firstResultHarness = allResultHarnesses && allResultHarnesses[0]; + const firstResultHarness = allResultHarnesses[0]; if (firstResultHarness) { await firstResultHarness.select(); 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..62129758b7 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 @@ -130,7 +130,7 @@ export class DemoComponent implements OnInit { }); this.searchFilters = [ - (_, item): boolean => { + (_, item: Person): boolean => { const names = this.favoritesForm.value.favoriteNames; // Only show people in the search results that have not been chosen already. @@ -154,7 +154,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[], ); } }); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.ts index 062aa15812..a6a6bdbebc 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.ts @@ -42,13 +42,16 @@ export class PickerModalComponent { constructor() { // This list of people will be rendered as selection boxes. - this.people = this.context.items; + this.people = this.context.items as Person[]; // Create a control for each selection box. this.peopleForm = this.#formBuilder.group({ people: this.#formBuilder.array( - this.context.items.map((item) => - this.#formBuilder.control(this.context.initialValue?.includes(item)), + this.context.items.map((item: Person) => + this.#formBuilder.control( + (this.context.initialValue as Person[]).includes(item), + { nonNullable: true }, + ), ), ), }); diff --git a/apps/code-examples/src/app/code-examples/lookup/lookup/multi-select/demo.component.ts b/apps/code-examples/src/app/code-examples/lookup/lookup/multi-select/demo.component.ts index c44f508c12..ca6511965e 100644 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/multi-select/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/lookup/lookup/multi-select/demo.component.ts @@ -78,7 +78,7 @@ export class DemoComponent implements OnInit { }); this.searchFilters = [ - (_, item, args): boolean => { + (_, item: Person, args): boolean => { // When in the modal view, show all people in the search results, regardless if they have been chosen already. if (args?.context === 'modal') { return true; 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..299d95a56e 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 @@ -77,7 +77,7 @@ describe('Lookup result templates demo', () => { await lookupHarness.enterText('be'); const allResultHarnesses = await lookupHarness.getSearchResults(); - const firstResultHarness = allResultHarnesses && allResultHarnesses[0]; + const firstResultHarness = allResultHarnesses[0]; await firstResultHarness.select(); expect( 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..e7c0066051 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 @@ -145,7 +145,7 @@ export class DemoComponent implements OnInit { }); this.searchFilters = [ - (_, item): boolean => { + (_, item: Person): boolean => { const names = this.favoritesForm.value.favoriteNames; // Only show people in the search results that have not been chosen already. 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..f7796240cd 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 @@ -72,7 +72,7 @@ export class DemoComponent implements OnInit { }); this.searchFilters = [ - (_, item): boolean => { + (_, item: Person): boolean => { const names = this.favoritesForm.value.favoriteName; // Only show people in the search results that have not been chosen already. 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; - } - } } From 3b3823c61c29ca29863b0a9dcdb0468836fc95f8 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:32:05 -0400 Subject: [PATCH 17/95] ci: fix cherry-pick title (#2455) --- .github/workflows/cherry-pick.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 8737e15e0c..7f45f66797 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -93,7 +93,7 @@ jobs: repo: context.repo.repo, head: process.env.CHERRY_PICK_BRANCH, base: process.env.TARGET_BRANCH, - title: `${process.env.COMMIT_MESSAGE} (#${pr.number})`, + title: process.env.COMMIT_MESSAGE, body }).then(result => { console.log(`Created PR #${result.data.number}: ${result.data.html_url}`); From 4d2d70a378d69af7b30370ed44641b036703ce47 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Wed, 10 Jul 2024 12:16:10 -0400 Subject: [PATCH 18/95] fix(components/datetime): ignore extraneous properties when setting calculator value (#2459) --- .../date-range-picker.component.spec.ts | 9 +++++++++ .../date-range-picker/date-range-picker.component.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts index 4ed575401d..a7cd9b70a9 100644 --- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts +++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts @@ -714,6 +714,15 @@ describe('Date range picker', function () { ); })); + it('should ignore extraneous properties when setting the value', () => { + component.dateRange?.setValue({ + foo: 'bar', + calculatorId: SkyDateRangeCalculatorId.LastWeek, + }); + + expect(() => fixture.detectChanges()).not.toThrow(); + }); + describe('accessibility', () => { function verifyFormFieldsRequired(expectation: boolean): void { const inputBoxes = diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts index 7a2ab2490a..575f67c8ae 100644 --- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts +++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts @@ -604,7 +604,7 @@ export class SkyDateRangePickerComponent } if (options?.emitEvent) { - this.formGroup.setValue(valueOrDefault); + this.formGroup.patchValue(valueOrDefault); } } } From cea996db6216e223c9998916e50ba7a4b78aa08f Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:08:21 -0400 Subject: [PATCH 19/95] feat(components/ag-grid): add support for AG Grid 31.3.2 (#2450) * feat(components/ag-grid): add support for AG Grid 31.3.2 * Update peer range --- libs/components/ag-grid/package.json | 4 ++-- ...grid-data-manager-adapter.directive.spec.ts | 1 + .../ag-grid/ag-grid-wrapper.component.spec.ts | 4 ++++ libs/components/packages/package.json | 6 +++--- .../ag-grid-migrate.schematic.spec.ts | 2 +- .../ag-grid-migrate.schematic.ts | 6 ++++-- .../ag-grid/ag-grid.schematic.spec.ts | 4 ++-- .../update-10/ag-grid/ag-grid.schematic.ts | 2 +- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 10 files changed, 29 insertions(+), 22 deletions(-) diff --git a/libs/components/ag-grid/package.json b/libs/components/ag-grid/package.json index 68dbc14d1d..12ba59c127 100644 --- a/libs/components/ag-grid/package.json +++ b/libs/components/ag-grid/package.json @@ -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.2", + "ag-grid-community": "^31.3.2" }, "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/packages/package.json b/libs/components/packages/package.json index 71a2fef96b..674ffcb645 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -85,9 +85,9 @@ "@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.2", + "ag-grid-community": "^31.3.2", + "ag-grid-enterprise": "^31.3.2", "autonumeric": "^4.10.5" } }, 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..60c24c502d 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,7 +19,7 @@ interface TestSetup { schematic: (options: Schema) => Rule; } -const UPDATE_TO_VERSION = '31.2.0'; +const UPDATE_TO_VERSION = '31.3.2'; describe('ag-grid-migrate.schematic', () => { async function setupTest(): Promise { 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..4b7a435091 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,7 +5,7 @@ import { platform } from 'os'; import { Schema } from './schema'; -const AG_GRID_MIGRATIONS = ['31.0.0', '31.1.0', '31.2.0']; +const AG_GRID_MIGRATIONS = ['31.0.0', '31.1.0', '31.2.1', '31.3.2']; const AG_GRID_VERSION = AG_GRID_MIGRATIONS.slice().pop(); export default function (options: Schema): Rule { @@ -42,10 +42,12 @@ export default function (options: Schema): Rule { }, ); for (const migration of AG_GRID_MIGRATIONS) { + const patchVersionZero = + migration.split('.').slice(0, 2).join('.') + '.0'; const cmdArgs = [ 'node_modules/@ag-grid-community/cli/index.cjs', 'migrate', - `--to=${migration}`, + `--to=${patchVersionZero}`, '--allow-dirty', ...agGridFiles, ]; 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..19c5cf4a1c 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.2'; 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..42c11db28a 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.2'; /** * Check package.json for AG Grid dependencies. diff --git a/package-lock.json b/package-lock.json index 99cc66a7bb..55575a07bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@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", + "ag-grid-angular": "31.3.2", + "ag-grid-community": "31.3.2", "autonumeric": "4.10.5", "axe-core": "4.9.0", "comment-json": "4.2.3", @@ -12642,22 +12642,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.2", + "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.3.2.tgz", + "integrity": "sha512-k8nMrOGcoZeTNY7lXoPIWgn2ANE+v169v9X6XqddlJgYWoRrommG1Av2zSxPYUqayLzV0z/mKTHv7UjdLtnCGw==", "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.2" } }, "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.2", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.2.tgz", + "integrity": "sha512-GxqFRD0OcjaVRE1gwLgoP0oERNPH8Lk8wKJ1txulsxysEQ5dZWHhiIoXXSiHjvOCVMkK/F5qzY6HNrn6VeDMTQ==" }, "node_modules/agent-base": { "version": "7.1.1", diff --git a/package.json b/package.json index fc4f7b5415..0cf7e8da44 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,8 @@ "@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", + "ag-grid-angular": "31.3.2", + "ag-grid-community": "31.3.2", "autonumeric": "4.10.5", "axe-core": "4.9.0", "comment-json": "4.2.3", From e08d88366c391455df53922ddc1719a6c66d0021 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Wed, 10 Jul 2024 14:27:27 -0400 Subject: [PATCH 20/95] revert(components/help-inline): stop propagation of click events (#2458) --- .../help-inline/help-inline.component.html | 2 +- .../help-inline/help-inline.component.spec.ts | 19 ------------------- .../help-inline/help-inline.component.ts | 4 +--- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html index 804d675182..023941d65c 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html @@ -24,7 +24,7 @@ 'sky-help-inline-hidden': helpKey && !helpSvc && !popoverContent }" [skyPopover]="popoverContent ? popoverRef : undefined" - (click)="onClick($event)" + (click)="onClick()" > { ariaHaspopup: null, }); }); - - it('should suppress click events', () => { - const clickSpy = spyOn(component, 'onClick'); - - const actionClickSpy = spyOn( - component, - 'onActionClick', - ).and.callThrough(); - - fixture.detectChanges(); - - const helpButton = getHelpButton(fixture); - helpButton.click(); - - fixture.detectChanges(); - - expect(clickSpy).not.toHaveBeenCalled(); - expect(actionClickSpy).toHaveBeenCalled(); - }); }); describe('with global options', () => { diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts index 9a71b5b7ce..41b1efe6e7 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts @@ -124,9 +124,7 @@ export class SkyHelpInlineComponent { readonly #idSvc = inject(SkyIdService); - protected onClick(evt: Event): void { - evt.stopPropagation(); - + protected onClick(): void { this.actionClick.emit(); if (this.helpKey) { From 1c383b1ed27e93695dd957621ac094ee68244c10 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:39:06 -0400 Subject: [PATCH 21/95] ci: fix cherry-pick title, one line (#2461) --- .github/workflows/cherry-pick.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 7f45f66797..fb1540a105 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -73,7 +73,7 @@ jobs: exit 0 fi - echo "COMMIT_MESSAGE=$(git log -1 --pretty=%B)" >> $GITHUB_ENV + echo "COMMIT_MESSAGE='$(git log -1 --pretty=%s | sed -e "s/'/\\\\'/g")'" >> $GITHUB_ENV env: GH_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} From ec07802786d6ecff7519cf5c2046d7ab375997b6 Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Thu, 11 Jul 2024 09:43:47 -0400 Subject: [PATCH 22/95] chore(components/help-inline): split help-inline styles (#2464) --- .../help-inline/help-inline.component.html | 2 +- .../help-inline/help-inline.component.scss | 56 ------------------- .../help-inline/help-inline.component.ts | 5 +- .../help-inline.default.component.scss | 25 +++++++++ .../help-inline.modern.component.scss | 49 ++++++++++++++++ .../src/lib/styles/_public-api/_mixins.scss | 27 ++++----- 6 files changed, 90 insertions(+), 74 deletions(-) delete mode 100644 libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.scss create mode 100644 libs/components/help-inline/src/lib/modules/help-inline/help-inline.default.component.scss create mode 100644 libs/components/help-inline/src/lib/modules/help-inline/help-inline.modern.component.scss 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 19172e39d8..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,7 +3,7 @@

    Giving diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.scss b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.scss deleted file mode 100644 index f88ac2be3a..0000000000 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.scss +++ /dev/null @@ -1,56 +0,0 @@ -@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-help-inline { - color: $sky-background-color-primary-dark; - font-size: $sky-font-size-base; - background-color: transparent; - border: none; - display: inline-block; - - &:hover { - color: darken($sky-background-color-primary-dark, 10%); - transition: color $sky-transition-time-short; - cursor: pointer; - } - - &-hidden { - display: none; - } -} - -::ng-deep .sky-help-inline-popover-text { - overflow-wrap: break-word; - margin: 0; -} - -@include mixins.sky-theme-modern { - sky-icon-stack { - display: inline-flex; - } - - .sky-help-inline { - // The button mixin sets the border radius to 6px but we only want 3px on help inline buttons. - border-radius: 3px; - - ::ng-deep .fa-stack-2x { - font-size: 16px; - } - - ::ng-deep .fa-stack-1x { - font-size: 10px; - } - - // The 0px and 5px padding is because we want 1px top/bottom and 6px left/right but the mixin - // adds a pixel to account for standard button drop shadows that do not exist on this button. - @include modernMixins.sky-theme-modern-button-variant( - $sky-theme-modern-background-color-primary-dark, - transparent, - transparent, - $sky-theme-modern-background-color-primary-dark, - 0px 5px - ); - } -} diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts index 41b1efe6e7..16b0c1e662 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.ts @@ -25,7 +25,10 @@ import { SkyHelpInlineAriaHaspopupPipe } from './help-inline-aria-haspopup.pipe' selector: 'sky-help-inline', standalone: true, templateUrl: './help-inline.component.html', - styleUrls: ['./help-inline.component.scss'], + styleUrls: [ + './help-inline.default.component.scss', + './help-inline.modern.component.scss', + ], imports: [ CommonModule, SkyHelpInlineAriaControlsPipe, diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.default.component.scss b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.default.component.scss new file mode 100644 index 0000000000..53c95314f4 --- /dev/null +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.default.component.scss @@ -0,0 +1,25 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('default', '.sky-help-inline') { + color: $sky-background-color-primary-dark; + font-size: $sky-font-size-base; + background-color: transparent; + border: none; + display: inline-block; + + &:hover { + color: darken($sky-background-color-primary-dark, 10%); + transition: color $sky-transition-time-short; + cursor: pointer; + } +} + +@include mixins.sky-component('default', '.sky-help-inline-hidden') { + display: none; +} + +@include mixins.sky-component('default', '.sky-help-inline-popover-text') { + overflow-wrap: break-word; + margin: 0; +} diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.modern.component.scss b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.modern.component.scss new file mode 100644 index 0000000000..613f4b3804 --- /dev/null +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.modern.component.scss @@ -0,0 +1,49 @@ +@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; + +@include mixins.sky-component('modern', '.sky-help-inline') { + font-size: $sky-font-size-base; + display: inline-block; + border-radius: 3px; + + // The 0px and 5px padding is because we want 1px top/bottom and 6px left/right but the mixin + // adds a pixel to account for standard button drop shadows that do not exist on this button. + @include modernMixins.sky-theme-modern-button-variant( + $sky-theme-modern-background-color-primary-dark, + transparent, + transparent, + $sky-theme-modern-background-color-primary-dark, + 0px 5px + ); + + &:hover { + color: darken($sky-background-color-primary-dark, 10%); + transition: color $sky-transition-time-short; + cursor: pointer; + } + + ::ng-deep { + .fa-stack-2x { + font-size: 16px; + } + + .fa-stack-1x { + font-size: 10px; + } + } +} + +@include mixins.sky-component('modern', '.sky-help-inline-hidden') { + display: none; +} + +@include mixins.sky-component('modern', '.sky-help-inline-popover-text') { + overflow-wrap: break-word; + margin: 0; +} + +@include mixins.sky-component('modern', 'sky-icon-stack') { + display: inline-flex; +} 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 1835acffb1..958ce5edca 100644 --- a/libs/components/theme/src/lib/styles/_public-api/_mixins.scss +++ b/libs/components/theme/src/lib/styles/_public-api/_mixins.scss @@ -102,49 +102,44 @@ @mixin sky-component($theme, $selector, $encapsulate: true, $breakpoint: '') { @if $breakpoint == '' { - @include sky-component-theme($theme, $selector, $encapsulate) { + @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, $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, $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, $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, $encapsulate) { + @include sky-component-theme($theme, $selector) { @content; } } } } -@mixin sky-component-theme($theme, $selector, $encapsulate: true) { - @if $theme == 'default' { - #{$selector}:not(.sky-theme-modern *) { + +@mixin sky-component-theme($theme, $selector) { + @if $theme == 'modern' { + :is(.sky-theme-modern #{$selector}) { @content; } } @else { - @if $encapsulate { - :host-context(.sky-theme-modern) #{$selector} { - @content; - } - } @else { - .sky-theme-modern #{$selector} { - @content; - } + #{$selector}:not(.sky-theme-modern *) { + @content; } } } From bbfb330a8bed125063098ffde6c99c0f1a3deea6 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Thu, 11 Jul 2024 10:47:33 -0400 Subject: [PATCH 23/95] feat(components/datetime): add support for additional date formats to `SkyDatePipe` (#2447) --- .../modules/date-pipe/date.service.spec.ts | 30 +++++++++++++++---- .../src/lib/modules/date-pipe/date.service.ts | 28 +++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/libs/components/datetime/src/lib/modules/date-pipe/date.service.spec.ts b/libs/components/datetime/src/lib/modules/date-pipe/date.service.spec.ts index 50a79181ee..0d43cfcf3f 100644 --- a/libs/components/datetime/src/lib/modules/date-pipe/date.service.spec.ts +++ b/libs/components/datetime/src/lib/modules/date-pipe/date.service.spec.ts @@ -114,12 +114,30 @@ describe('Date service', () => { }); it('should support Angular DatePipe formats', () => { - const value = service.format(new Date(2000, 0, 1), undefined, 'fullDate'); - const expectedValues = [ - 'Saturday, January 1, 2000', - 'Saturday, January 01, 2000', // IE 11 - ]; - expect(expectedValues).toContain(value); + const date = new Date(); + const formatSpy = spyOn(SkyIntlDateFormatter, 'format'); + + /* spell-checker:disable */ + const formats = new Map([ + ['short', 'yMdjm'], + ['medium', 'yMMMdjms'], + ['long', 'MMMM d, y, h:mm:ss a Z'], + ['full', 'EEEE, MMMM d, y, h:mm:ss a z'], + ['shortDate', 'yMd'], + ['mediumDate', 'yMMMd'], + ['longDate', 'yMMMMd'], + ['fullDate', 'yMMMMEEEEd'], + ['shortTime', 'jm'], + ['mediumTime', 'jms'], + ['longTime', 'h:mm:ss a Z'], + ['fullTime', 'h:mm:ss a z'], + ]); + /* spell-checker:enable */ + + for (const [formatName, formatExpression] of formats) { + service.format(date, undefined, formatName); + expect(formatSpy).toHaveBeenCalledWith(date, 'en-US', formatExpression); + } }); it('should default to mediumDate format', () => { diff --git a/libs/components/datetime/src/lib/modules/date-pipe/date.service.ts b/libs/components/datetime/src/lib/modules/date-pipe/date.service.ts index 4fe20a80c1..1f8fc913b6 100644 --- a/libs/components/datetime/src/lib/modules/date-pipe/date.service.ts +++ b/libs/components/datetime/src/lib/modules/date-pipe/date.service.ts @@ -21,15 +21,33 @@ import { takeUntil } from 'rxjs/operators'; }) export class SkyDateService implements OnDestroy { /* spell-checker:disable */ + // See: https://github.com/angular/angular/blob/17.3.x/packages/common/src/pipes/date_pipe.ts + // See: https://www.ibm.com/docs/en/app-connect/11.0.0?topic=function-formatting-parsing-datetimes-as-strings #ALIASES: Record = { - medium: 'yMMMdjms', + // TODO: replace with 'M/d/yy, h:mm a' in a breaking change. short: 'yMdjm', - fullDate: 'yMMMMEEEEd', - longDate: 'yMMMMd', - mediumDate: 'yMMMd', + // TODO: replace with 'MMM d, y, h:mm:ss a' in a breaking change. + medium: 'yMMMdjms', + // TODO: This format was modified from 'MMMM d, y, h:mm:ss a z' due to the limitations of the Intl API. + long: 'MMMM d, y, h:mm:ss a Z', + // TODO: This format was modified from 'EEEE, MMMM d, y, h:mm:ss a zzzz' due to the limitations of the Intl API. + full: 'EEEE, MMMM d, y, h:mm:ss a z', + // TODO: Replace with 'M/d/yy' in a breaking change. shortDate: 'yMd', - mediumTime: 'jms', + // TODO: Replace with 'MMM d, y' in a breaking change. + mediumDate: 'yMMMd', + // TODO: Replace with 'MMMM d, y' in a breaking change. + longDate: 'yMMMMd', + // TODO: Replace with 'EEEE, MMMM d, y' in a breaking change. + fullDate: 'yMMMMEEEEd', + // TODO: Replace with 'h:mm a' in a breaking change. shortTime: 'jm', + // TODO: Replace with 'h:mm:ss a' in a breaking change. + mediumTime: 'jms', + // TODO: This format was modified from 'h:mm:ss a z' due to the limitations of the Intl API. + longTime: 'h:mm:ss a Z', + // TODO: This format was modified from 'h:mm:ss a zzzz' due to the limitations of the Intl API. + fullTime: 'h:mm:ss a z', }; /* spell-checker:enable */ From a715c8a4981267ffc5b8909b225916c6a1cb6977 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:04:20 -0400 Subject: [PATCH 24/95] ci: fix cherry-pick title (#2470) --- .github/workflows/cherry-pick.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index fb1540a105..ebe0c638c7 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -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 }})" @@ -73,7 +74,7 @@ jobs: exit 0 fi - echo "COMMIT_MESSAGE='$(git log -1 --pretty=%s | sed -e "s/'/\\\\'/g")'" >> $GITHUB_ENV + echo "commit_message=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} @@ -83,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) { @@ -93,7 +95,7 @@ jobs: repo: context.repo.repo, head: process.env.CHERRY_PICK_BRANCH, base: process.env.TARGET_BRANCH, - title: process.env.COMMIT_MESSAGE, + title, body }).then(result => { console.log(`Created PR #${result.data.number}: ${result.data.html_url}`); From 8a173ce0fbe5660806f17ede06323f590f944c1c Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Thu, 11 Jul 2024 13:33:00 -0400 Subject: [PATCH 25/95] fix(components/tiles): do not expand/collapse content when help inline clicked (#2468) --- .../modules/tiles/tile/tile.component.html | 2 +- .../modules/tiles/tile/tile.component.spec.ts | 19 ++++++++++++++++++- .../lib/modules/tiles/tile/tile.component.ts | 9 +++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) 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..61279e52b8 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)) { 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 { From 67da79b8702309e4e5b6dbc2c0cf1223f41727f2 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Thu, 11 Jul 2024 13:57:29 -0400 Subject: [PATCH 26/95] chore: release 10.35.0 (#2445) --- CHANGELOG.md | 22 ++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3613d0b0f..88979fdc1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 55575a07bd..e3626a10a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.34.0", + "version": "10.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.34.0", + "version": "10.35.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0cf7e8da44..edc0569706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.34.0", + "version": "10.35.0", "license": "MIT", "scripts": { "ng": "nx", From 61c234e43e1bab37521950d62d004a493020d2b4 Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Thu, 11 Jul 2024 17:00:09 -0400 Subject: [PATCH 27/95] chore(components/indicators): split key info and illustration styles (#2466) --- .../illustration/illustration.component.scss | 4 ---- .../illustration/illustration.component.ts | 7 ++++++- .../illustration.default.component.scss | 9 +++++++++ .../illustration.modern.component.scss | 9 +++++++++ .../lib/modules/key-info/key-info.component.ts | 5 ++++- .../key-info/key-info.default.component.scss | 16 ++++++++++++++++ ...onent.scss => key-info.modern.component.scss} | 4 +++- 7 files changed, 47 insertions(+), 7 deletions(-) delete mode 100644 libs/components/indicators/src/lib/modules/illustration/illustration.component.scss create mode 100644 libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss create mode 100644 libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss create mode 100644 libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss rename libs/components/indicators/src/lib/modules/key-info/{key-info.component.scss => key-info.modern.component.scss} (65%) diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.component.scss b/libs/components/indicators/src/lib/modules/illustration/illustration.component.scss deleted file mode 100644 index 0c0b1abb3c..0000000000 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -:host, -.sky-illustration-img { - display: block; -} diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.component.ts b/libs/components/indicators/src/lib/modules/illustration/illustration.component.ts index 0b326e43eb..1b304f2e8a 100644 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.component.ts +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.component.ts @@ -7,6 +7,7 @@ import { input, } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { SkyThemeComponentClassDirective } from '@skyux/theme'; import { catchError, from, of, switchMap } from 'rxjs'; @@ -28,7 +29,11 @@ const pixelSizes: Record = { standalone: true, imports: [CommonModule, NgOptimizedImage], templateUrl: './illustration.component.html', - styleUrls: ['./illustration.component.scss'], + styleUrls: [ + './illustration.default.component.scss', + './illustration.modern.component.scss', + ], + hostDirectives: [SkyThemeComponentClassDirective], changeDetection: ChangeDetectionStrategy.OnPush, }) export class SkyIllustrationComponent { diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss b/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss new file mode 100644 index 0000000000..10b86f6eab --- /dev/null +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss @@ -0,0 +1,9 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; + +@include mixins.sky-component-host('default') { + display: block; +} + +@include mixins.sky-component('default', '.sky-illustration-img') { + display: block; +} diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss b/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss new file mode 100644 index 0000000000..e6ba64893b --- /dev/null +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss @@ -0,0 +1,9 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; + +@include mixins.sky-component-host('modern') { + display: block; +} + +@include mixins.sky-component('modern', '.sky-illustration-img') { + display: block; +} diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts index 0ab33ccbad..3403a200da 100644 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts @@ -5,7 +5,10 @@ import { SkyKeyInfoLayoutType } from './key-info-layout-type'; @Component({ selector: 'sky-key-info', templateUrl: './key-info.component.html', - styleUrls: ['./key-info.component.scss'], + styleUrls: [ + './key-info.default.component.scss', + './key-info.modern.component.scss', + ], }) export class SkyKeyInfoComponent { /** diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss b/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss new file mode 100644 index 0000000000..9a8c8318dc --- /dev/null +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss @@ -0,0 +1,16 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; + +@include mixins.sky-component('default', '.sky-key-info') { + display: inline-block; + + &.sky-key-info-horizontal { + .sky-key-info-value, + .sky-key-info-label { + display: inline-block; + } + + .sky-key-info-label { + padding: 0 0 0 var(--sky-margin-inline-xs); + } + } +} diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.component.scss b/libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss similarity index 65% rename from libs/components/indicators/src/lib/modules/key-info/key-info.component.scss rename to libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss index c3fa636d71..272a3737cd 100644 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.component.scss +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss @@ -1,4 +1,6 @@ -.sky-key-info { +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; + +@include mixins.sky-component('modern', '.sky-key-info') { display: inline-block; &.sky-key-info-horizontal { From 86072086e4133750817b9862ba9868e2690b33ea Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Fri, 12 Jul 2024 18:22:57 -0400 Subject: [PATCH 28/95] fix(components/icon): provide HTTP with interceptors (#2474) * fix(components/icon): provide HTTP with interceptors * Update icon.module.ts * make HTTP optional * Provide HTTP for apps with icons --------- Co-authored-by: John White <750350+johnhwhite@users.noreply.github.com> --- libs/components/icon/src/lib/icon/icon.module.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/components/icon/src/lib/icon/icon.module.ts b/libs/components/icon/src/lib/icon/icon.module.ts index 8babdc9107..b375ec1c9f 100644 --- a/libs/components/icon/src/lib/icon/icon.module.ts +++ b/libs/components/icon/src/lib/icon/icon.module.ts @@ -1,5 +1,9 @@ import { CommonModule } from '@angular/common'; -import { provideHttpClient, withFetch } from '@angular/common/http'; +import { + provideHttpClient, + withFetch, + withInterceptorsFromDi, +} from '@angular/common/http'; import { NgModule } from '@angular/core'; import { SkyIconClassListPipe } from './icon-class-list.pipe'; @@ -12,6 +16,9 @@ import { SkyIconComponent } from './icon.component'; declarations: [SkyIconClassListPipe, SkyIconComponent, SkyIconStackComponent], imports: [CommonModule, SkyIconSvgComponent], exports: [SkyIconComponent, SkyIconStackComponent], - providers: [provideHttpClient(withFetch()), SkyIconSvgResolverService], + providers: [ + provideHttpClient(withFetch(), withInterceptorsFromDi()), + SkyIconSvgResolverService, + ], }) export class SkyIconModule {} From 6e8fff68cd8961a62eab95525c534f9cdcf0e945 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 12 Jul 2024 18:53:04 -0400 Subject: [PATCH 29/95] chore: release 10.35.1 (#2476) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88979fdc1f..f0a3cc2b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index e3626a10a2..aab505f5f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.35.0", + "version": "10.35.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.35.0", + "version": "10.35.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index edc0569706..2e8994d0a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.35.0", + "version": "10.35.1", "license": "MIT", "scripts": { "ng": "nx", From dc01eea582def695ed18963be5c55e2731173fe3 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Mon, 15 Jul 2024 09:58:41 -0400 Subject: [PATCH 30/95] feat(components/icon): add icon stack visual tests (#2479) --- .../src/app/icon/icon.component.html | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 fff685dfec..cd28d19d97 100644 --- a/apps/e2e/indicators-storybook/src/app/icon/icon.component.html +++ b/apps/e2e/indicators-storybook/src/app/icon/icon.component.html @@ -79,4 +79,40 @@
    +
    + + + + + + + +
    From c50280b250ca49bcfe2635c4dbc1bfe3207e5704 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Mon, 15 Jul 2024 11:25:10 -0400 Subject: [PATCH 31/95] feat(components/layout): add heading and inline help inputs to box component (#2439) --- .../layout/box/basic/demo.component.html | 9 +- .../layout/box/basic/demo.component.spec.ts | 2 +- .../box/inline-help/demo.component.html | 25 --- .../layout/box/inline-help/demo.component.ts | 16 -- .../src/app/features/layout.module.ts | 7 - .../src/app/home/home.component.html | 1 - .../src/app/box/box.component.html | 7 +- .../src/app/box/box.component.ts | 21 ++ .../components/layout/box/box.component.html | 42 ++++ .../components/layout/box/box.component.ts | 12 +- libs/components/layout/src/index.ts | 2 + .../lib/modules/box/box-header.component.ts | 10 + .../src/lib/modules/box/box-heading-level.ts | 1 + .../src/lib/modules/box/box-heading-style.ts | 1 + .../src/lib/modules/box/box.component.html | 45 +++- .../src/lib/modules/box/box.component.scss | 7 + .../src/lib/modules/box/box.component.spec.ts | 158 +++++++++++++- .../src/lib/modules/box/box.component.ts | 87 +++++++- .../layout/src/lib/modules/box/box.module.ts | 3 +- .../box/fixtures/box.component.fixture.html | 7 + .../box/fixtures/box.component.fixture.ts | 10 + .../testing/src/box/box-harness.spec.ts | 201 ++++++++++++++---- .../layout/testing/src/box/box-harness.ts | 91 ++++++++ .../fixtures/box-harness-test.component.html | 14 ++ .../fixtures/box-harness-test.component.ts | 21 ++ .../box/fixtures/box-harness-test.module.ts | 11 + 26 files changed, 709 insertions(+), 102 deletions(-) delete mode 100644 apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.html delete mode 100644 apps/code-examples/src/app/code-examples/layout/box/inline-help/demo.component.ts create mode 100644 libs/components/layout/src/lib/modules/box/box-heading-level.ts create mode 100644 libs/components/layout/src/lib/modules/box/box-heading-style.ts create mode 100644 libs/components/layout/testing/src/box/fixtures/box-harness-test.component.html create mode 100644 libs/components/layout/testing/src/box/fixtures/box-harness-test.component.ts create mode 100644 libs/components/layout/testing/src/box/fixtures/box-harness-test.module.ts 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 04debb4409..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 @@ -31,6 +31,6 @@ describe('Basic box', () => { 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/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/home/home.component.html b/apps/code-examples/src/app/home/home.component.html index b6534dbcee..fd79174a4f 100644 --- a/apps/code-examples/src/app/home/home.component.html +++ b/apps/code-examples/src/app/home/home.component.html @@ -332,7 +332,6 @@ Box

  • 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 }}

    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/libs/components/layout/src/index.ts b/libs/components/layout/src/index.ts index bb49332341..3af544b03c 100644 --- a/libs/components/layout/src/index.ts +++ b/libs/components/layout/src/index.ts @@ -8,6 +8,8 @@ export { SkyBackToTopMessage } from './lib/modules/back-to-top/models/back-to-to export { SkyBackToTopMessageType } from './lib/modules/back-to-top/models/back-to-top-message-type'; export { SkyBackToTopOptions } from './lib/modules/back-to-top/models/back-to-top-options'; +export { SkyBoxHeadingLevel } from './lib/modules/box/box-heading-level'; +export { SkyBoxHeadingStyle } from './lib/modules/box/box-heading-style'; export { SkyBoxModule } from './lib/modules/box/box.module'; export { SkyCardModule } from './lib/modules/card/card.module'; diff --git a/libs/components/layout/src/lib/modules/box/box-header.component.ts b/libs/components/layout/src/lib/modules/box/box-header.component.ts index 2c207e12a2..bdc1503076 100644 --- a/libs/components/layout/src/lib/modules/box/box-header.component.ts +++ b/libs/components/layout/src/lib/modules/box/box-header.component.ts @@ -1,9 +1,11 @@ import { Component, ViewEncapsulation, inject } from '@angular/core'; +import { SkyLogService } from '@skyux/core'; import { SKY_BOX_HEADER_ID } from './box-header-id-token'; /** * Specifies a header for the box. + * @deprecated Use `headingText` input on the `sky-box` component instead. */ @Component({ selector: 'sky-box-header', @@ -13,4 +15,12 @@ import { SKY_BOX_HEADER_ID } from './box-header-id-token'; }) export class SkyBoxHeaderComponent { protected readonly boxHeaderId = inject(SKY_BOX_HEADER_ID); + + constructor() { + inject(SkyLogService).deprecated('SkyToggleSwitchLabelComponent', { + deprecationMajorVersion: 10, + replacementRecommendation: + 'To add a header to box, use the `headerText` input on the box component instead.', + }); + } } diff --git a/libs/components/layout/src/lib/modules/box/box-heading-level.ts b/libs/components/layout/src/lib/modules/box/box-heading-level.ts new file mode 100644 index 0000000000..ceecf4cb61 --- /dev/null +++ b/libs/components/layout/src/lib/modules/box/box-heading-level.ts @@ -0,0 +1 @@ +export type SkyBoxHeadingLevel = 2 | 3 | 4 | 5; diff --git a/libs/components/layout/src/lib/modules/box/box-heading-style.ts b/libs/components/layout/src/lib/modules/box/box-heading-style.ts new file mode 100644 index 0000000000..9ea43c2697 --- /dev/null +++ b/libs/components/layout/src/lib/modules/box/box-heading-style.ts @@ -0,0 +1 @@ +export type SkyBoxHeadingStyle = 2 | 3 | 4 | 5; diff --git a/libs/components/layout/src/lib/modules/box/box.component.html b/libs/components/layout/src/lib/modules/box/box.component.html index 65ce062b4b..411a48d646 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.html +++ b/libs/components/layout/src/lib/modules/box/box.component.html @@ -1,15 +1,52 @@
    -
    - +
    + @if (headingText) { + @switch (headingLevel) { + @case (3) { +

    + {{ headingText }} +

    + } + @case (4) { +

    + {{ headingText }} +

    + } + @case (5) { +
    + {{ headingText }} +
    + } + @default { +

    + {{ headingText }} +

    + } + } + + } @else { + + }
    diff --git a/libs/components/layout/src/lib/modules/box/box.component.scss b/libs/components/layout/src/lib/modules/box/box.component.scss index edc69ce047..292c795779 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.scss +++ b/libs/components/layout/src/lib/modules/box/box.component.scss @@ -42,6 +42,13 @@ sky-box { top: -2px; } } + + h2, + h3, + h4, + h5 { + margin-block: 0; + } } sky-box-content { diff --git a/libs/components/layout/src/lib/modules/box/box.component.spec.ts b/libs/components/layout/src/lib/modules/box/box.component.spec.ts index 653b25a591..c3d02d5b0d 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.spec.ts +++ b/libs/components/layout/src/lib/modules/box/box.component.spec.ts @@ -1,7 +1,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@skyux-sdk/testing'; import { SkyContentInfoProvider } from '@skyux/core'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; import { SkyBoxControlsComponent } from './box-controls.component'; +import { SkyBoxHeadingLevel } from './box-heading-level'; +import { SkyBoxHeadingStyle } from './box-heading-style'; import { BoxTestComponent } from './fixtures/box.component.fixture'; import { SkyBoxFixturesModule } from './fixtures/box.module.fixture'; @@ -33,7 +40,7 @@ describe('BoxComponent', () => { TestBed.configureTestingModule({ declarations: [BoxTestComponent], - imports: [SkyBoxFixturesModule], + imports: [SkyBoxFixturesModule, SkyHelpTestingModule], }); fixture = TestBed.overrideComponent(SkyBoxControlsComponent, { @@ -58,13 +65,15 @@ describe('BoxComponent', () => { }); it('should assign label attribute when ariaLabel is set', () => { + component.headingText = undefined; component.ariaLabel = 'my box'; fixture.detectChanges(); expect(getBoxEl(fixture).getAttribute('aria-label')).toEqual('my box'); }); - it('should assign role attribute when ariaRole is set', () => { + it('should assign labelledby attribute when ariaLabelledby is set', () => { + component.headingText = undefined; component.ariaLabelledBy = 'my-header'; fixture.detectChanges(); @@ -73,7 +82,63 @@ describe('BoxComponent', () => { ); }); + it('should set an id on the headingText element and provide it via contentInfoProvider', async () => { + const contentInfoSpy = spyOn( + contentInfoProvider, + 'patchInfo', + ).and.callThrough(); + fixture.detectChanges(); + let header = getBoxEl(fixture).querySelector('.sky-box-header-content h2'); + expect(header).not.toBeNull(); + + if (header) { + expect(contentInfoProvider.patchInfo).toHaveBeenCalledWith({ + descriptor: { type: 'elementId', value: header.id }, + }); + expect( + getControlsDropdownButton(fixture).getAttribute('aria-label'), + ).toBeNull(); + expect(getContentDropdownButton(fixture).getAttribute('aria-label')).toBe( + 'Context menu', + ); + expect( + getControlsDropdownButton(fixture).getAttribute('aria-labelledby'), + ).toEqual( + jasmine.stringMatching( + /sky-id-gen__[0-9]+__[0-9]+ sky-id-gen__[0-9]+__[0-9]+/, + ), + ); + expect( + getContentDropdownButton(fixture).getAttribute('aria-labelledby'), + ).toBeNull(); + } + + contentInfoSpy.calls.reset(); + component.showHeader = false; + component.headingText = undefined; + fixture.detectChanges(); + header = getBoxEl(fixture).querySelector('.sky-box-header-content h2'); + expect(header).toBeNull(); + + expect(contentInfoProvider.patchInfo).toHaveBeenCalledWith({ + descriptor: undefined, + }); + expect(getContentDropdownButton(fixture).getAttribute('aria-label')).toBe( + 'Context menu', + ); + expect(getControlsDropdownButton(fixture).getAttribute('aria-label')).toBe( + 'Context menu', + ); + expect( + getControlsDropdownButton(fixture).getAttribute('aria-labelledby'), + ).toBeNull(); + expect( + getControlsDropdownButton(fixture).getAttribute('aria-labelledby'), + ).toBeNull(); + }); + it('should set an id on the header and provide it via contentInfoProvider', async () => { + component.headingText = undefined; const contentInfoSpy = spyOn( contentInfoProvider, 'patchInfo', @@ -126,4 +191,93 @@ describe('BoxComponent', () => { getControlsDropdownButton(fixture).getAttribute('aria-labelledby'), ).toBeNull(); }); + + it('should render the correct heading level and styles using the headingText input', () => { + const headerCmp = getBoxEl(fixture).querySelector('sky-box-header span'); + expect(headerCmp).toBeNull(); + + const headingLevels: (SkyBoxHeadingLevel | undefined)[] = [ + undefined, + 2, + 3, + 4, + 5, + ]; + const headingStyles: (SkyBoxHeadingStyle | undefined)[] = [ + undefined, + 2, + 3, + 4, + 5, + ]; + headingLevels.forEach((headingLevel) => { + headingStyles.forEach((headingStyle) => { + component.headingLevel = headingLevel; + component.headingStyle = headingStyle; + fixture.detectChanges(); + + const heading = fixture.nativeElement.querySelector( + `h${headingLevel ?? 2}.sky-font-heading-${headingStyle ?? 2}`, + ); + + expect(heading).toExist(); + }); + }); + }); + + it('should render help inline popover', () => { + component.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelectorAll('sky-help-inline').length, + ).toBe(1); + }); + + it('should not render help inline popover if title is provided without content', () => { + component.helpPopoverTitle = 'popover title'; + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelectorAll('sky-help-inline').length, + ).toBe(0); + + component.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelectorAll('sky-help-inline').length, + ).toBe(1); + }); + + it('should render help inline if help key is provided', () => { + component.helpPopoverContent = undefined; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('.sky-help-inline')).toBeFalsy(); + + component.helpKey = 'helpKey.html'; + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector('.sky-help-inline'), + ).toBeTruthy(); + }); + + it('should set global help config with help key', async () => { + const helpController = TestBed.inject(SkyHelpTestingController); + component.helpKey = 'helpKey.html'; + + fixture.detectChanges(); + + const helpInlineButton = fixture.nativeElement.querySelector( + '.sky-help-inline', + ) as HTMLElement | undefined; + helpInlineButton?.click(); + + await fixture.whenStable(); + fixture.detectChanges(); + + helpController.expectCurrentHelpKey('helpKey.html'); + }); }); diff --git a/libs/components/layout/src/lib/modules/box/box.component.ts b/libs/components/layout/src/lib/modules/box/box.component.ts index 6216859465..02d7b632a3 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.ts +++ b/libs/components/layout/src/lib/modules/box/box.component.ts @@ -3,14 +3,23 @@ import { ContentChild, ElementRef, Input, + TemplateRef, ViewEncapsulation, + booleanAttribute, inject, + numberAttribute, } from '@angular/core'; import { SkyIdService } from '@skyux/core'; import { SkyBoxControlsComponent } from './box-controls.component'; import { SKY_BOX_HEADER_ID } from './box-header-id-token'; import { SkyBoxHeaderComponent } from './box-header.component'; +import { SkyBoxHeadingLevel } from './box-heading-level'; +import { SkyBoxHeadingStyle } from './box-heading-style'; + +function numberAttribute2(value: unknown): number { + return numberAttribute(value, 2); +} /** * Provides a common look-and-feel for box content with options to display a common box header, specify body content, and display common box controls. @@ -31,11 +40,80 @@ import { SkyBoxHeaderComponent } from './box-header.component'; ], }) export class SkyBoxComponent { + /** + * The text to display as the box's heading. + * @preview + */ + @Input() + public set headingText(value: string | undefined) { + this.#_headingText = value; + + if (this.#boxControls) { + this.#boxControls.boxHasHeader(!!value); + } + } + + public get headingText(): string | undefined { + return this.#_headingText; + } + + /** + * Indicates whether to hide the `headingText`. + * @preview + */ + @Input({ transform: booleanAttribute }) + public headingHidden = false; + + /** + * The semantic heading level in the document structure. The default is 2. + * @preview + * @default 2 + */ + @Input({ transform: numberAttribute2 }) + public headingLevel: SkyBoxHeadingLevel = 2; + + /** + * The heading [font style](https://developer.blackbaud.com/skyux/design/styles/typography#headings). + * @preview + * @default 2 + */ + @Input({ transform: numberAttribute2 }) + public set headingStyle(value: SkyBoxHeadingStyle) { + this.headingClass = `sky-font-heading-${value}`; + } + + /** + * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * button is added to the box heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) + * when clicked using the specified content and optional title. + * @preview + */ + @Input() + public helpPopoverContent: string | TemplateRef | undefined; + + /** + * The title of the help popover. This property only applies when `helpPopoverContent` is + * also specified. + * @preview + */ + @Input() + public helpPopoverTitle: string | undefined; + + /** + * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * button is placed beside the box heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) + * as configured by the application. + * @preview + */ + @Input() + public helpKey: string | undefined; + /** * The ARIA label for the box. This sets the box's `aria-label` attribute to provide a text equivalent for screen readers * [to support accessibility](https://developer.blackbaud.com/skyux/learn/accessibility). * If the box 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 `headingText` instead. */ @Input() public ariaLabel: string | undefined; @@ -46,6 +124,7 @@ export class SkyBoxComponent { * [to support accessibility](https://developer.blackbaud.com/skyux/learn/accessibility). * If the box 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 `headingText` instead. */ @Input() public ariaLabelledBy: string | undefined; @@ -73,10 +152,16 @@ export class SkyBoxComponent { this.#boxControls = value; if (value) { - value.boxHasHeader(!!this.#boxHeaderRef); + value.boxHasHeader(!!this.headingText || !!this.#boxHeaderRef); } } + public headerId = inject(SKY_BOX_HEADER_ID); + + protected headingClass = 'sky-font-heading-2'; + + #_headingText: string | undefined; + #boxControls: SkyBoxControlsComponent | undefined; #boxHeaderRef: ElementRef | undefined; } diff --git a/libs/components/layout/src/lib/modules/box/box.module.ts b/libs/components/layout/src/lib/modules/box/box.module.ts index 2194545d7e..eff85c990c 100644 --- a/libs/components/layout/src/lib/modules/box/box.module.ts +++ b/libs/components/layout/src/lib/modules/box/box.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { SkyTrimModule } from '@skyux/core'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; import { SkyThemeModule } from '@skyux/theme'; import { SkyBoxContentComponent } from './box-content.component'; @@ -15,7 +16,7 @@ import { SkyBoxComponent } from './box.component'; SkyBoxContentComponent, SkyBoxControlsComponent, ], - imports: [CommonModule, SkyThemeModule, SkyTrimModule], + imports: [CommonModule, SkyHelpInlineModule, SkyThemeModule, SkyTrimModule], exports: [ SkyBoxComponent, SkyBoxHeaderComponent, diff --git a/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.html b/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.html index df867f121a..9bd17badd9 100644 --- a/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.html +++ b/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.html @@ -2,6 +2,13 @@ [ariaLabel]="ariaLabel" [ariaLabelledBy]="ariaLabelledBy" [ariaRole]="ariaRole" + [headingHidden]="headingHidden" + [headingLevel]="headingLevel" + [headingStyle]="headingStyle" + [headingText]="headingText" + [helpKey]="helpKey" + [helpPopoverContent]="helpPopoverContent" + [helpPopoverTitle]="helpPopoverTitle" >

    Header

    diff --git a/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.ts b/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.ts index e9e4295754..e83a3db5d3 100644 --- a/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.ts +++ b/libs/components/layout/src/lib/modules/box/fixtures/box.component.fixture.ts @@ -1,5 +1,8 @@ import { Component } from '@angular/core'; +import { SkyBoxHeadingLevel } from '../box-heading-level'; +import { SkyBoxHeadingStyle } from '../box-heading-style'; + @Component({ selector: 'sky-box-test', templateUrl: 'box.component.fixture.html', @@ -8,5 +11,12 @@ export class BoxTestComponent { public ariaLabel: string | undefined; public ariaLabelledBy: string | undefined; public ariaRole: string | undefined; + public headingHidden = false; + public headingLevel: SkyBoxHeadingLevel | undefined = 2; + public headingStyle: SkyBoxHeadingStyle | undefined = 2; + public headingText: string | undefined = 'Heading text'; + public helpKey: string | undefined; + public helpPopoverContent: string | undefined; + public helpPopoverTitle: string | undefined; public showHeader = true; } diff --git a/libs/components/layout/testing/src/box/box-harness.spec.ts b/libs/components/layout/testing/src/box/box-harness.spec.ts index 40e76cf6ad..7e87758d76 100644 --- a/libs/components/layout/testing/src/box/box-harness.spec.ts +++ b/libs/components/layout/testing/src/box/box-harness.spec.ts @@ -1,31 +1,12 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SkyBoxModule } from '@skyux/layout'; +import { SkyHelpService } from '@skyux/core'; +import { SkyHelpTestingModule } from '@skyux/core/testing'; import { SkyBoxHarness } from './box-harness'; - -// #region Test component -@Component({ - selector: 'sky-box-test', - template: ` - - - `, -}) -class TestBoxComponent { - public ariaRole: string | undefined; - public ariaLabel: string | undefined; - public ariaLabelledBy: string | undefined; - public otherBox = 'otherBox'; -} -// #endregion Test component +import { BoxHarnessTestComponent } from './fixtures/box-harness-test.component'; +import { BoxHarnessTestModule } from './fixtures/box-harness-test.module'; describe('Box test harness', () => { async function setupTest( @@ -34,28 +15,23 @@ describe('Box test harness', () => { } = {}, ): Promise<{ boxHarness: SkyBoxHarness; - fixture: ComponentFixture; + fixture: ComponentFixture; loader: HarnessLoader; }> { await TestBed.configureTestingModule({ - declarations: [TestBoxComponent], - imports: [SkyBoxModule], + imports: [BoxHarnessTestModule, SkyHelpTestingModule], }).compileComponents(); - const fixture = TestBed.createComponent(TestBoxComponent); + const fixture = TestBed.createComponent(BoxHarnessTestComponent); const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); - let boxHarness: SkyBoxHarness; - - if (options.dataSkyId) { - boxHarness = await loader.getHarness( - SkyBoxHarness.with({ - dataSkyId: options.dataSkyId, - }), - ); - } else { - boxHarness = await loader.getHarness(SkyBoxHarness); - } + const boxHarness: SkyBoxHarness = options.dataSkyId + ? await loader.getHarness( + SkyBoxHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyBoxHarness); return { boxHarness, fixture, loader }; } @@ -68,6 +44,155 @@ describe('Box test harness', () => { await expectAsync(boxHarness.getAriaLabel()).toBeResolvedTo('otherBox'); }); + it('should get the heading text', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'Box header'; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingText()).toBeResolvedTo('Box header'); + }); + + it('should get the heading text when heading text is hidden', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'Box header'; + fixture.componentInstance.headingHidden = true; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingText()).toBeResolvedTo('Box header'); + }); + + it('should indicate the heading is not hidden', async () => { + const { boxHarness } = await setupTest(); + + await expectAsync(boxHarness.getHeadingHidden()).toBeResolvedTo(false); + }); + + it('should indicate the heading is hidden', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingHidden = true; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingHidden()).toBeResolvedTo(true); + }); + + it('should return the heading level and heading style', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'Box header'; + fixture.componentInstance.headingLevel = undefined; + fixture.componentInstance.headingStyle = 2; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingLevel()).toBeResolvedTo(2); + await expectAsync(boxHarness.getHeadingStyle()).toBeResolvedTo(2); + + fixture.componentInstance.headingLevel = 2; + fixture.componentInstance.headingStyle = 3; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingLevel()).toBeResolvedTo(2); + await expectAsync(boxHarness.getHeadingStyle()).toBeResolvedTo(3); + + fixture.componentInstance.headingLevel = 3; + fixture.componentInstance.headingStyle = 4; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingLevel()).toBeResolvedTo(3); + await expectAsync(boxHarness.getHeadingStyle()).toBeResolvedTo(4); + + fixture.componentInstance.headingLevel = 4; + fixture.componentInstance.headingStyle = 5; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingLevel()).toBeResolvedTo(4); + await expectAsync(boxHarness.getHeadingStyle()).toBeResolvedTo(5); + + fixture.componentInstance.headingLevel = 5; + fixture.componentInstance.headingStyle = undefined; + fixture.detectChanges(); + + await expectAsync(boxHarness.getHeadingLevel()).toBeResolvedTo(5); + await expectAsync(boxHarness.getHeadingStyle()).toBeResolvedTo(2); + }); + + it('should throw an error if no help inline is found', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'heading'; + fixture.detectChanges(); + + await expectAsync(boxHarness.clickHelpInline()).toBeRejectedWithError( + 'No help inline found.', + ); + }); + + it('should open help inline popover when clicked', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'heading'; + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + await boxHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(boxHarness.getHelpPopoverContent()).toBeResolved(); + }); + + it('should open global help widget when clicked', async () => { + const { boxHarness, fixture } = await setupTest(); + const helpSvc = TestBed.inject(SkyHelpService); + const helpSpy = spyOn(helpSvc, 'openHelp'); + + fixture.componentInstance.headingText = 'heading'; + fixture.componentInstance.helpPopoverContent = undefined; + fixture.componentInstance.helpKey = 'helpKey.html'; + fixture.detectChanges(); + + await boxHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(helpSpy).toHaveBeenCalledWith({ helpKey: 'helpKey.html' }); + }); + + it('should get help popover content', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'heading'; + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + await boxHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(boxHarness.getHelpPopoverContent()).toBeResolvedTo( + 'popover content', + ); + }); + + it('should get help popover title', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingText = 'heading'; + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.componentInstance.helpPopoverTitle = 'popover title'; + fixture.detectChanges(); + + await boxHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(boxHarness.getHelpPopoverTitle()).toBeResolvedTo( + 'popover title', + ); + }); + it('should get the aria-label', async () => { const { boxHarness, fixture } = await setupTest(); diff --git a/libs/components/layout/testing/src/box/box-harness.ts b/libs/components/layout/testing/src/box/box-harness.ts index 168dd42f16..5bd9a977bb 100644 --- a/libs/components/layout/testing/src/box/box-harness.ts +++ b/libs/components/layout/testing/src/box/box-harness.ts @@ -1,5 +1,9 @@ import { HarnessPredicate } from '@angular/cdk/testing'; +import { TemplateRef } from '@angular/core'; import { SkyComponentHarness } from '@skyux/core/testing'; +import { SkyHelpInlineHarness } from '@skyux/help-inline/testing'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { SkyBoxHeadingLevel, SkyBoxHeadingStyle } from '@skyux/layout'; import { SkyBoxHarnessFilters } from './box-harness.filters'; @@ -10,6 +14,11 @@ export class SkyBoxHarness extends SkyComponentHarness { public static hostSelector = 'sky-box'; #getBox = this.locatorFor('.sky-box'); + #getHeading = this.locatorFor('.sky-box-header-content'); + #getH2 = this.locatorForOptional('.sky-box-header-content h2'); + #getH3 = this.locatorForOptional('.sky-box-header-content h3'); + #getH4 = this.locatorForOptional('.sky-box-header-content h4'); + #getH5 = this.locatorForOptional('.sky-box-header-content h5'); /** * Gets a `HarnessPredicate` that can be used to search for a @@ -21,6 +30,74 @@ export class SkyBoxHarness extends SkyComponentHarness { return SkyBoxHarness.getDataSkyIdPredicate(filters); } + /** + * Clicks the help inline button. + */ + public async clickHelpInline(): Promise { + return (await this.#getHelpInline()).click(); + } + + /** + * Gets the help popover content. + */ + public async getHelpPopoverContent(): Promise< + TemplateRef | string | undefined + > { + return await (await this.#getHelpInline()).getPopoverContent(); + } + + /** + * Gets the help popover title. + */ + public async getHelpPopoverTitle(): Promise { + return await (await this.#getHelpInline()).getPopoverTitle(); + } + + /** + * Gets the box's heading text. If `headingHidden` is true, + * the text will still be returned. + */ + public async getHeadingText(): Promise { + return (await this.#getHeading()).text(); + } + + /** + * Whether the heading is hidden. + */ + public async getHeadingHidden(): Promise { + return (await this.#getHeading()).hasClass('sky-screen-reader-only'); + } + + /** + * The semantic heading level used for the checkbox group. Returns undefined if heading level is not set. + */ + public async getHeadingLevel(): Promise { + return (await this.#getH2()) + ? 2 + : (await this.#getH3()) + ? 3 + : (await this.#getH4()) + ? 4 + : 5; + } + + /** + * The heading style used for the checkbox group. + */ + public async getHeadingStyle(): Promise { + const heading = + (await this.#getH2()) || + (await this.#getH3()) || + (await this.#getH4()) || + (await this.#getH5()); + + const isHeadingStyle2 = await heading?.hasClass('sky-font-heading-2'); + const isHeadingStyle3 = await heading?.hasClass('sky-font-heading-3'); + const isHeadingStyle4 = await heading?.hasClass('sky-font-heading-4'); + + return isHeadingStyle2 ? 2 : isHeadingStyle3 ? 3 : isHeadingStyle4 ? 4 : 5; + } + /** * Gets the aria-label property of the box */ @@ -41,4 +118,18 @@ export class SkyBoxHarness extends SkyComponentHarness { public async getAriaRole(): Promise { return (await this.#getBox()).getAttribute('role'); } + + async #getHelpInline(): Promise { + const harness = await this.locatorForOptional( + SkyHelpInlineHarness.with({ + ancestor: '.sky-box-header-content', + }), + )(); + + if (harness) { + return harness; + } + + throw Error('No help inline found.'); + } } diff --git a/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.html b/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.html new file mode 100644 index 0000000000..1945aa4a30 --- /dev/null +++ b/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.html @@ -0,0 +1,14 @@ + + diff --git a/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.ts b/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.ts new file mode 100644 index 0000000000..1ac6a86b9f --- /dev/null +++ b/libs/components/layout/testing/src/box/fixtures/box-harness-test.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +// #region Test component +@Component({ + selector: 'sky-box-fixture', + templateUrl: './box-harness-test.component.html', +}) +export class BoxHarnessTestComponent { + public ariaRole: string | undefined; + public ariaLabel: string | undefined; + public ariaLabelledBy: string | undefined; + public headingText: string | undefined; + public headingHidden = false; + public headingLevel: number | undefined; + public headingStyle: number | undefined; + public helpKey: string | undefined; + public helpPopoverContent: string | undefined; + public helpPopoverTitle: string | undefined; + public otherBox = 'otherBox'; +} +// #endregion Test component diff --git a/libs/components/layout/testing/src/box/fixtures/box-harness-test.module.ts b/libs/components/layout/testing/src/box/fixtures/box-harness-test.module.ts new file mode 100644 index 0000000000..52941d8641 --- /dev/null +++ b/libs/components/layout/testing/src/box/fixtures/box-harness-test.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyBoxModule } from '@skyux/layout'; + +import { BoxHarnessTestComponent } from './box-harness-test.component'; + +@NgModule({ + imports: [SkyBoxModule, NoopAnimationsModule], + declarations: [BoxHarnessTestComponent], +}) +export class BoxHarnessTestModule {} From 2fbfbefc15bf9f68ddefd2dbc8b9c5eebd9d2aca Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:02:49 -0400 Subject: [PATCH 32/95] fix(components/packages): switch to `@ag-grid-devtools/cli` for AG Grid codemods (#2483) * fix(components/packages): switch to `@ag-grid-devtools/cli` for AG Grid codemods * Fix comment --- .../ag-grid-migrate.schematic.spec.ts | 116 +++++++++++++++--- .../ag-grid-migrate.schematic.ts | 64 ++++++---- .../src/schematics/ag-grid-migrate/schema.ts | 8 +- 3 files changed, 148 insertions(+), 40 deletions(-) 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 60c24c502d..fbd4cc7e13 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 @@ -22,17 +22,56 @@ interface TestSetup { const UPDATE_TO_VERSION = '31.3.2'; describe('ag-grid-migrate.schematic', () => { - async function setupTest(): Promise { + const defaultSetup = { + fileList: '', + sourceRoot: '.', + startingVersion: '31.3.2', + 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(' ') === + `cat-file HEAD:${setup.sourceRoot}/package-lock.json` + ) { + return { + stdout: JSON.stringify({ + '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 +97,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 +122,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,7 +157,7 @@ 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_VERSION}`], { stdio: 'ignore', windowsVerbatimArguments: true, @@ -114,9 +166,10 @@ describe('ag-grid-migrate.schematic', () => { }); 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,7 +190,41 @@ 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_VERSION}`], + { + stdio: 'ignore', + windowsVerbatimArguments: true, + }, + ); + }); + + 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_VERSION}`], { stdio: 'ignore', windowsVerbatimArguments: true, @@ -146,9 +233,10 @@ describe('ag-grid-migrate.schematic', () => { }); 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()); 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 4b7a435091..a7a6baa1a6 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,35 @@ import { platform } from 'os'; import { Schema } from './schema'; -const AG_GRID_MIGRATIONS = ['31.0.0', '31.1.0', '31.2.1', '31.3.2']; -const AG_GRID_VERSION = AG_GRID_MIGRATIONS.slice().pop(); +const AG_GRID_VERSION = '31.3.2'; +const AG_GRID_MIGRATION = '31.3.0'; + +function getStartingVersion(sourceRoot: string): string { + const content = spawnSync( + 'git', + ['cat-file', `HEAD:${sourceRoot}/package-lock.json`], + { + encoding: 'utf-8', + stdio: 'pipe', + }, + ); + const packageJson = JSON.parse(content.stdout); + return packageJson['node_modules/ag-grid-community'].version; +} 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 === 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,32 +55,28 @@ 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_VERSION}`], { stdio: 'ignore', windowsVerbatimArguments: true, }, ); - for (const migration of AG_GRID_MIGRATIONS) { - const patchVersionZero = - migration.split('.').slice(0, 2).join('.') + '.0'; - const cmdArgs = [ - 'node_modules/@ag-grid-community/cli/index.cjs', - 'migrate', - `--to=${patchVersionZero}`, - '--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', - }); - } + 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-community/cli`], { stdio: 'ignore', }); 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; } From 198d89588ab7d758587aa12e3be1a9df634c7cbb Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Mon, 15 Jul 2024 14:13:34 -0400 Subject: [PATCH 33/95] feat(components/icon): use native fetch() for retrieving icon sprite (#2478) --- .../icon/icon-svg-resolver.service.spec.ts | 78 ++++++++++--------- .../src/lib/icon/icon-svg-resolver.service.ts | 78 +++++++++---------- .../src/lib/icon/icon-svg.component.spec.ts | 4 +- .../icon/src/lib/icon/icon.module.ts | 10 --- 4 files changed, 80 insertions(+), 90 deletions(-) diff --git a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts index b5b4f4bb43..c4a317c951 100644 --- a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts +++ b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.spec.ts @@ -1,19 +1,11 @@ -import { provideHttpClient } from '@angular/common/http'; -import { - HttpTestingController, - provideHttpClientTesting, -} from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { firstValueFrom } from 'rxjs'; - import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; import { SkyIconVariantType } from './types/icon-variant-type'; describe('Icon SVG resolver service', () => { + let fetchMock: jasmine.Spy; let resolverSvc: SkyIconSvgResolverService; - let httpTestingController: HttpTestingController; - let spriteLoaded = false; function buildSymbolHtml( name: string, @@ -32,52 +24,46 @@ describe('Icon SVG resolver service', () => { variant?: SkyIconVariantType, expectedError?: string, ): Promise { - const hrefPromise = firstValueFrom( - resolverSvc.resolveHref(name, size, variant), - ); - - if (!spriteLoaded) { - const testRequest = httpTestingController.expectOne( - 'https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg', - ); - - testRequest.flush(` - ${buildSymbolHtml('single-size', 12, 'line')} - ${buildSymbolHtml('single-size', 12, 'solid')} - ${buildSymbolHtml('multi-size', 12, 'line')} - ${buildSymbolHtml('multi-size', 12, 'solid')} - ${buildSymbolHtml('multi-size', 24, 'line')} - ${buildSymbolHtml('multi-size', 24, 'solid')} - ${buildSymbolHtml('multi-size', 48, 'line')} - ${buildSymbolHtml('multi-size', 48, 'solid')} - `); - - spriteLoaded = true; - } + const hrefPromise = resolverSvc.resolveHref(name, size, variant); if (expectedError) { await expectAsync(hrefPromise).toBeRejectedWithError(expectedError); } else if (expectedHref) { await expectAsync(hrefPromise).toBeResolvedTo(expectedHref); } + + // Fetch should only be called once per instance of the resolver service + // and the result shared across subsequent calls to resolveHref(). + expect(fetchMock).toHaveBeenCalledOnceWith( + 'https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg', + ); } beforeEach(() => { + fetchMock = spyOn(window, 'fetch').and.resolveTo( + new Response( + ` + ${buildSymbolHtml('single-size', 12, 'line')} + ${buildSymbolHtml('single-size', 12, 'solid')} + ${buildSymbolHtml('multi-size', 12, 'line')} + ${buildSymbolHtml('multi-size', 12, 'solid')} + ${buildSymbolHtml('multi-size', 24, 'line')} + ${buildSymbolHtml('multi-size', 24, 'solid')} + ${buildSymbolHtml('multi-size', 48, 'line')} + ${buildSymbolHtml('multi-size', 48, 'solid')} + `, + ), + ); + TestBed.configureTestingModule({ - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - SkyIconSvgResolverService, - ], + providers: [SkyIconSvgResolverService], }); resolverSvc = TestBed.inject(SkyIconSvgResolverService); - httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => { document.getElementById('sky-icon-svg-sprite')?.remove(); - spriteLoaded = false; }); it('should resolve the expected variant', async () => { @@ -95,6 +81,22 @@ describe('Icon SVG resolver service', () => { ); }); + it('should throw an error when the request fails', async () => { + fetchMock.and.resolveTo( + new Response('Internal Server Error', { + status: 500, + }), + ); + + await validate( + 'single-size', + undefined, + undefined, + undefined, + `Icon sprite could not be loaded.`, + ); + }); + describe('with single size icons', () => { it('should resolve the expected icon regardless of specified size', async () => { await validate('single-size', '#sky-i-single-size-12-line'); diff --git a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts index 3623fa5200..ad5296d8d7 100644 --- a/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts +++ b/libs/components/icon/src/lib/icon/icon-svg-resolver.service.ts @@ -1,16 +1,21 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; - -import { Observable, map, shareReplay, tap } from 'rxjs'; +import { Injectable } from '@angular/core'; import { SkyIconVariantType } from './types/icon-variant-type'; -function insertSprite(markup: string): void { +async function getIconMap(): Promise> { + const response = await fetch( + `https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg`, + ); + + if (!response.ok) { + throw new Error('Icon sprite could not be loaded.'); + } + + const markup = await response.text(); + document.body.insertAdjacentHTML('afterbegin', markup); -} -function getIconsSizes(): Map { - const iconsSizes = Array.from( + const iconMap = Array.from( document.querySelectorAll('#sky-icon-svg-sprite symbol'), ).reduce((map, el) => { const idParts = el.id.split('-'); @@ -34,20 +39,20 @@ function getIconsSizes(): Map { }, new Map()); // Sort all the sizes for later comparison. - for (const id of iconsSizes.keys()) { + for (const id of iconMap.keys()) { // Dedupe and sort the icon sizes. - iconsSizes.set(id, [...new Set(iconsSizes.get(id))].sort()); + iconMap.set(id, [...new Set(iconMap.get(id))].sort()); } - return iconsSizes; + return iconMap; } function getNearestSize( - iconsSizes: Map, + iconMap: Map, name: string, pixelSize: number, ): number | undefined { - const sizes = iconsSizes.get(name); + const sizes = iconMap.get(name); if (sizes) { let nearestSizeUnder = -Infinity; @@ -78,39 +83,34 @@ function getNearestSize( /** * @internal */ -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class SkyIconSvgResolverService { - readonly #http = inject(HttpClient); - - readonly #spriteObs = this.#http - .get( - `https://sky.blackbaudcdn.net/static/skyux-icons/7/assets/svg/skyux-icons.svg`, - { - responseType: 'text', - }, - ) - .pipe(tap(insertSprite), map(getIconsSizes), shareReplay(1)); - - public resolveHref( + #iconMapPromise: Promise> | undefined; + + public async resolveHref( name: string, pixelSize = 16, variant: SkyIconVariantType = 'line', - ): Observable { - return this.#spriteObs.pipe( - map((iconsSizes) => { - let href = `#sky-i-${name}`; + ): Promise { + if (!this.#iconMapPromise) { + this.#iconMapPromise = getIconMap(); + } + + const iconMap = await this.#iconMapPromise; - // Find the icon with the optimal size nearest to the requested size. - const nearestSize = getNearestSize(iconsSizes, name, pixelSize); + let href = `#sky-i-${name}`; - if (!nearestSize) { - throw new Error(`Icon with name '${name}' was not found.`); - } + // Find the icon with the optimal size nearest to the requested size. + const nearestSize = getNearestSize(iconMap, name, pixelSize); + + if (!nearestSize) { + throw new Error(`Icon with name '${name}' was not found.`); + } - href = `${href}-${nearestSize}-${variant}`; + href = `${href}-${nearestSize}-${variant}`; - return href; - }), - ); + return href; } } diff --git a/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts b/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts index 041f946961..6b55545ec9 100644 --- a/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts +++ b/libs/components/icon/src/lib/icon/icon-svg.component.spec.ts @@ -5,8 +5,6 @@ import { tick, } from '@angular/core/testing'; -import { of } from 'rxjs'; - import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; import { SkyIconSvgComponent } from './icon-svg.component'; import { SkyIconModule } from './icon.module'; @@ -40,7 +38,7 @@ describe('Icon SVG component', () => { ); resolverSvc.resolveHref.and.callFake((src, size, variant) => { - return of(`#${src}-${size}-${variant ?? 'line'}`); + return Promise.resolve(`#${src}-${size}-${variant ?? 'line'}`); }); TestBed.configureTestingModule({ diff --git a/libs/components/icon/src/lib/icon/icon.module.ts b/libs/components/icon/src/lib/icon/icon.module.ts index b375ec1c9f..b9fe683646 100644 --- a/libs/components/icon/src/lib/icon/icon.module.ts +++ b/libs/components/icon/src/lib/icon/icon.module.ts @@ -1,14 +1,8 @@ import { CommonModule } from '@angular/common'; -import { - provideHttpClient, - withFetch, - withInterceptorsFromDi, -} from '@angular/common/http'; import { NgModule } from '@angular/core'; import { SkyIconClassListPipe } from './icon-class-list.pipe'; import { SkyIconStackComponent } from './icon-stack.component'; -import { SkyIconSvgResolverService } from './icon-svg-resolver.service'; import { SkyIconSvgComponent } from './icon-svg.component'; import { SkyIconComponent } from './icon.component'; @@ -16,9 +10,5 @@ import { SkyIconComponent } from './icon.component'; declarations: [SkyIconClassListPipe, SkyIconComponent, SkyIconStackComponent], imports: [CommonModule, SkyIconSvgComponent], exports: [SkyIconComponent, SkyIconStackComponent], - providers: [ - provideHttpClient(withFetch(), withInterceptorsFromDi()), - SkyIconSvgResolverService, - ], }) export class SkyIconModule {} From b0496dcf78953868c6dfceb19d1a49890a2f73b3 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Mon, 15 Jul 2024 14:22:52 -0400 Subject: [PATCH 34/95] chore: release 10.36.0 (#2481) --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a3cc2b53..060feb079d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index aab505f5f3..ed484f1e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.35.1", + "version": "10.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.35.1", + "version": "10.36.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2e8994d0a2..5170c93b09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.35.1", + "version": "10.36.0", "license": "MIT", "scripts": { "ng": "nx", From 8674e08b0fedcbbf7a32a156a344913800848ae1 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Tue, 16 Jul 2024 11:32:25 -0400 Subject: [PATCH 35/95] chore(components/indicators): move help inline and icon code examples out (#2490) --- .../src/app/app-routing.module.ts | 12 +++++++++ .../help-inline/basic/demo.component.html | 0 .../help-inline/basic/demo.component.spec.ts | 0 .../help-inline/basic/demo.component.ts | 0 .../icon/basic/demo.component.html | 0 .../icon/basic/demo.component.spec.ts | 0 .../icon/basic/demo.component.ts | 0 .../icon/icon-button/demo.component.html | 0 .../icon/icon-button/demo.component.spec.ts | 0 .../icon/icon-button/demo.component.ts | 0 .../src/app/features/help-inline.module.ts | 17 ++++++++++++ .../src/app/features/icon.module.ts | 24 +++++++++++++++++ .../src/app/features/indicators.module.ts | 21 --------------- .../src/app/home/home.component.html | 26 +++++++++---------- 14 files changed, 66 insertions(+), 34 deletions(-) rename apps/code-examples/src/app/code-examples/{indicators => }/help-inline/basic/demo.component.html (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/help-inline/basic/demo.component.spec.ts (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/help-inline/basic/demo.component.ts (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/basic/demo.component.html (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/basic/demo.component.spec.ts (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/basic/demo.component.ts (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/icon-button/demo.component.html (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/icon-button/demo.component.spec.ts (100%) rename apps/code-examples/src/app/code-examples/{indicators => }/icon/icon-button/demo.component.ts (100%) create mode 100644 apps/code-examples/src/app/features/help-inline.module.ts create mode 100644 apps/code-examples/src/app/features/icon.module.ts 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/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/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/home/home.component.html b/apps/code-examples/src/app/home/home.component.html index fd79174a4f..59a9f93572 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
      From 9a720f2e7306a59134ee5b0fdb59eea4db0a222b Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:02:13 -0400 Subject: [PATCH 36/95] feat(components/ag-grid): add support for AG Grid 31.3.4 (#2491) * feat(components/ag-grid): add support for AG Grid 31.3.4 * Include enterprise package. * Update test * Update test --------- Co-authored-by: Trevor Burch --- libs/components/ag-grid/package.json | 4 +-- libs/components/layout/testing/project.json | 8 +++++- libs/components/packages/package.json | 6 ++--- .../ag-grid-migrate.schematic.spec.ts | 26 +++++++++++++++---- .../ag-grid-migrate.schematic.ts | 6 ++--- .../ag-grid/ag-grid.schematic.spec.ts | 2 +- .../update-10/ag-grid/ag-grid.schematic.ts | 2 +- package-lock.json | 18 ++++++------- package.json | 4 +-- 9 files changed, 49 insertions(+), 27 deletions(-) diff --git a/libs/components/ag-grid/package.json b/libs/components/ag-grid/package.json index 12ba59c127..e20b47e88d 100644 --- a/libs/components/ag-grid/package.json +++ b/libs/components/ag-grid/package.json @@ -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.3.2", - "ag-grid-community": "^31.3.2" + "ag-grid-angular": "^31.3.4", + "ag-grid-community": "^31.3.4" }, "dependencies": { "@skyux/icon": "0.0.0-PLACEHOLDER", diff --git a/libs/components/layout/testing/project.json b/libs/components/layout/testing/project.json index cb44cf671a..92ce1b2d4e 100644 --- a/libs/components/layout/testing/project.json +++ b/libs/components/layout/testing/project.json @@ -5,7 +5,13 @@ "sourceRoot": "libs/components/layout/testing/src", "prefix": "sky", "tags": ["testing"], - "implicitDependencies": ["core-testing", "layout", "testing"], + "implicitDependencies": [ + "core", + "core-testing", + "help-inline-testing", + "layout", + "testing" + ], "targets": { "build": { "command": "echo ' 🏗️ build layout-testing'", diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index 674ffcb645..2c4ad02d59 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -85,9 +85,9 @@ "@skyux/tiles": "0.0.0-PLACEHOLDER", "@skyux/toast": "0.0.0-PLACEHOLDER", "@skyux/validation": "0.0.0-PLACEHOLDER", - "ag-grid-angular": "^31.3.2", - "ag-grid-community": "^31.3.2", - "ag-grid-enterprise": "^31.3.2", + "ag-grid-angular": "^31.3.4", + "ag-grid-community": "^31.3.4", + "ag-grid-enterprise": "^31.3.4", "autonumeric": "^4.10.5" } }, 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 fbd4cc7e13..b3256eceb1 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,13 +19,14 @@ interface TestSetup { schematic: (options: Schema) => Rule; } -const UPDATE_TO_VERSION = '31.3.2'; +const UPDATE_TO_VERSION = '31.3.4'; +const UPDATE_TO_MIGRATION = '31.3.0'; describe('ag-grid-migrate.schematic', () => { const defaultSetup = { fileList: '', sourceRoot: '.', - startingVersion: '31.3.2', + startingVersion: UPDATE_TO_VERSION, debug: false, }; type Setup = typeof defaultSetup; @@ -157,12 +158,17 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).toHaveBeenCalled(); expect(childProcess.spawnSync).toHaveBeenCalledWith( 'npm.cmd', - ['install', '--no-save', `@ag-grid-devtools/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 () => { @@ -190,12 +196,17 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).toHaveBeenCalled(); expect(childProcess.spawnSync).toHaveBeenCalledWith( 'npm', - ['install', '--no-save', `@ag-grid-devtools/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 () => { @@ -224,12 +235,17 @@ describe('ag-grid-migrate.schematic', () => { expect(os.platform).toHaveBeenCalled(); expect(childProcess.spawnSync).toHaveBeenCalledWith( 'npm', - ['install', '--no-save', `@ag-grid-devtools/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 not run if no files match', async () => { 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 a7a6baa1a6..21c3e071a6 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,8 +5,8 @@ import { platform } from 'os'; import { Schema } from './schema'; -const AG_GRID_VERSION = '31.3.2'; const AG_GRID_MIGRATION = '31.3.0'; +const AG_GRID_VERSION = '31.3.4'; function getStartingVersion(sourceRoot: string): string { const content = spawnSync( @@ -55,7 +55,7 @@ export default function (options: Schema): Rule { const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'; spawnSync( npm, - ['install', '--no-save', `@ag-grid-devtools/cli@${AG_GRID_VERSION}`], + ['install', '--no-save', `@ag-grid-devtools/cli@~${AG_GRID_MIGRATION}`], { stdio: 'ignore', windowsVerbatimArguments: true, @@ -77,7 +77,7 @@ export default function (options: Schema): Rule { windowsVerbatimArguments: true, argv0: 'npx', }); - spawnSync(npm, ['remove', `@ag-grid-community/cli`], { + spawnSync(npm, ['remove', `@ag-grid-devtools/cli`], { stdio: 'ignore', }); context.logger.info( 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 19c5cf4a1c..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.3.2'; +const UPDATE_TO_VERSION = '31.3.4'; describe('ag-grid.schematic', () => { const runner = new SchematicTestRunner( 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 42c11db28a..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.3.2'; +const AG_GRID_VERSION = '^31.3.4'; /** * Check package.json for AG Grid dependencies. diff --git a/package-lock.json b/package-lock.json index ed484f1e32..f09617c1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", "@skyux/icons": "7.3.0", - "ag-grid-angular": "31.3.2", - "ag-grid-community": "31.3.2", + "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", @@ -12642,22 +12642,22 @@ } }, "node_modules/ag-grid-angular": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.3.2.tgz", - "integrity": "sha512-k8nMrOGcoZeTNY7lXoPIWgn2ANE+v169v9X6XqddlJgYWoRrommG1Av2zSxPYUqayLzV0z/mKTHv7UjdLtnCGw==", + "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.3.2" + "ag-grid-community": "31.3.4" } }, "node_modules/ag-grid-community": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.2.tgz", - "integrity": "sha512-GxqFRD0OcjaVRE1gwLgoP0oERNPH8Lk8wKJ1txulsxysEQ5dZWHhiIoXXSiHjvOCVMkK/F5qzY6HNrn6VeDMTQ==" + "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", diff --git a/package.json b/package.json index 5170c93b09..f3c52e7c46 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,8 @@ "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", "@skyux/icons": "7.3.0", - "ag-grid-angular": "31.3.2", - "ag-grid-community": "31.3.2", + "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", From 03e4f547a2b5e6b99c88569781276c481296715d Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Tue, 16 Jul 2024 12:12:39 -0400 Subject: [PATCH 37/95] chore: release 10.37.0 (#2494) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060feb079d..9edae8e325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index f09617c1c3..cccbe31a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.36.0", + "version": "10.37.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.36.0", + "version": "10.37.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f3c52e7c46..60382ab010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.36.0", + "version": "10.37.0", "license": "MIT", "scripts": { "ng": "nx", From 4d96f275facdf55d1da8cd30b46125abbee535a6 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Tue, 16 Jul 2024 12:39:00 -0400 Subject: [PATCH 38/95] chore(components/icon): icon stack overrides visual tests (#2496) --- .../src/app/icon/icon.component.html | 12 ++++++++++++ .../icon/src/lib/icon/icon-stack.component.scss | 1 + 2 files changed, 13 insertions(+) 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 cd28d19d97..2e445bcab6 100644 --- a/apps/e2e/indicators-storybook/src/app/icon/icon.component.html +++ b/apps/e2e/indicators-storybook/src/app/icon/icon.component.html @@ -115,4 +115,16 @@ [topIcon]="{ icon: 'eye', iconType: 'skyux' }" /> +
      + + +
      diff --git a/libs/components/icon/src/lib/icon/icon-stack.component.scss b/libs/components/icon/src/lib/icon/icon-stack.component.scss index faf6b37104..950dbbcb3f 100644 --- a/libs/components/icon/src/lib/icon/icon-stack.component.scss +++ b/libs/components/icon/src/lib/icon/icon-stack.component.scss @@ -23,6 +23,7 @@ } // Temporary overrides until the proportions of stackable SKY UX icons have been corrected. +// TODO: correct the proportions of stackable SKY UX icons .fa-stack-1x { &.sky-i-check:before { position: relative; From a01a108cc8cb95ff245dad6f9e472ba60ef1c350 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Tue, 16 Jul 2024 13:27:18 -0400 Subject: [PATCH 39/95] fix(components/lists): reorderable repeater will not throw a warning when no repeater items exist (#2492) --- .../fixtures/repeater.component.fixture.ts | 4 +- .../repeater/repeater.component.spec.ts | 40 +++++++++++++++---- .../modules/repeater/repeater.component.ts | 7 ++-- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/libs/components/lists/src/lib/modules/repeater/fixtures/repeater.component.fixture.ts b/libs/components/lists/src/lib/modules/repeater/fixtures/repeater.component.fixture.ts index a417f2a22c..7c0d65950d 100644 --- a/libs/components/lists/src/lib/modules/repeater/fixtures/repeater.component.fixture.ts +++ b/libs/components/lists/src/lib/modules/repeater/fixtures/repeater.component.fixture.ts @@ -22,7 +22,7 @@ export class RepeaterTestComponent { public expandMode: SkyRepeaterExpandModeType | undefined = 'single'; - public items: { id?: string; title: string }[] = [ + public items: { id?: string; title: string }[] | undefined = [ { id: 'item1', title: 'Title 1', @@ -82,7 +82,7 @@ export class RepeaterTestComponent { id: `item${nextItemId++}`, title: 'New record ' + nextItemId, }; - this.items.push(newItem); + this.items?.push(newItem); } public onOrderChange(tags: any[]): void { 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.', ); } From b34086f230df8b7e9b2eb9a46ec2181c8bb64b19 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Tue, 16 Jul 2024 15:04:27 -0400 Subject: [PATCH 40/95] fix(sdk/testing): `expectAsync` type includes async matchers from Jasmine (#2503) --- libs/sdk/testing/src/lib/matchers/matchers.spec.ts | 5 +++++ libs/sdk/testing/src/lib/matchers/matchers.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) 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); } From dbdeb0d72d3e119074caf2cf22f8ae555a479009 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Tue, 16 Jul 2024 15:43:06 -0400 Subject: [PATCH 41/95] fix(components/layout): box headingHidden input shouldn't hide controls component (#2505) --- .../src/lib/modules/box/box.component.html | 33 ++++++++++++------- .../testing/src/box/box-harness.spec.ts | 1 + .../layout/testing/src/box/box-harness.ts | 7 +++- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/libs/components/layout/src/lib/modules/box/box.component.html b/libs/components/layout/src/lib/modules/box/box.component.html index 411a48d646..4e330cda72 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.html +++ b/libs/components/layout/src/lib/modules/box/box.component.html @@ -8,37 +8,48 @@ 'sky-elevation-1-bordered': 'modern' }" > -
      +
      @if (headingText) { @switch (headingLevel) { @case (3) { -

      +

      {{ headingText }}

      } @case (4) { -

      +

      {{ headingText }}

      } @case (5) { -
      +
      {{ headingText }}
      } @default { -

      +

      {{ headingText }}

      } } { it('should indicate the heading is hidden', async () => { const { boxHarness, fixture } = await setupTest(); + fixture.componentInstance.headingText = 'Box header'; fixture.componentInstance.headingHidden = true; fixture.detectChanges(); diff --git a/libs/components/layout/testing/src/box/box-harness.ts b/libs/components/layout/testing/src/box/box-harness.ts index 5bd9a977bb..f2ded6abfa 100644 --- a/libs/components/layout/testing/src/box/box-harness.ts +++ b/libs/components/layout/testing/src/box/box-harness.ts @@ -65,7 +65,12 @@ export class SkyBoxHarness extends SkyComponentHarness { * Whether the heading is hidden. */ public async getHeadingHidden(): Promise { - return (await this.#getHeading()).hasClass('sky-screen-reader-only'); + const heading = + (await this.#getH2()) || + (await this.#getH3()) || + (await this.#getH4()) || + (await this.#getH5()); + return (await heading?.hasClass('sky-screen-reader-only')) ?? false; } /** From 6212fa7af5b71e35f05526e5c0cc2145d02e2e4e Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Tue, 16 Jul 2024 15:57:09 -0400 Subject: [PATCH 42/95] fix(components/help-inline): add schematic for missing popovers peer which may be missing due to the package being added by schematic (#2506) --- .../migrations/migration-collection.json | 5 ++ ...popovers-peer-dependency.schematic.spec.ts | 78 +++++++++++++++++++ ...line-popovers-peer-dependency.schematic.ts | 14 ++++ 3 files changed, 97 insertions(+) create mode 100644 libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.spec.ts create mode 100644 libs/components/packages/src/schematics/migrations/update-10/add-help-inline-popovers-peer-dependency/add-help-inline-popovers-peer-dependency.schematic.ts 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, + }, + ]); +} From f2076c950bffbd5210960cdb61a5e4d93a0705fb Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Tue, 16 Jul 2024 16:00:52 -0400 Subject: [PATCH 43/95] chore: release 10.37.1 (#2500) --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9edae8e325..350fca35ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index cccbe31a11..a2ebad8e1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.37.0", + "version": "10.37.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.37.0", + "version": "10.37.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 60382ab010..a0d4670dd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.37.0", + "version": "10.37.1", "license": "MIT", "scripts": { "ng": "nx", From 27498a2ba227cf8ff98026c1f3990a0eb5f756ac Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:05:45 -0400 Subject: [PATCH 44/95] fix(components/packages): additional error handling for AG Grid schematic (#2509) --- .../ag-grid-migrate.schematic.spec.ts | 25 ++++++++++++++-- .../ag-grid-migrate.schematic.ts | 30 ++++++++++++------- .../schematics/ag-grid-migrate/schema.json | 3 +- 3 files changed, 43 insertions(+), 15 deletions(-) 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 b3256eceb1..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 @@ -42,12 +42,15 @@ describe('ag-grid-migrate.schematic', () => { if ( cmd === 'git' && args?.join(' ') === - `cat-file HEAD:${setup.sourceRoot}/package-lock.json` + // eslint-disable-next-line @cspell/spellchecker + `cat-file --textconv HEAD:${setup.sourceRoot}/package-lock.json` ) { return { stdout: JSON.stringify({ - 'node_modules/ag-grid-community': { - version: setup.startingVersion, + packages: { + 'node_modules/ag-grid-community': { + version: setup.startingVersion, + }, }, }), }; @@ -274,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 21c3e071a6..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 @@ -8,17 +8,22 @@ import { Schema } from './schema'; const AG_GRID_MIGRATION = '31.3.0'; const AG_GRID_VERSION = '31.3.4'; -function getStartingVersion(sourceRoot: string): string { - const content = spawnSync( - 'git', - ['cat-file', `HEAD:${sourceRoot}/package-lock.json`], - { - encoding: 'utf-8', - stdio: 'pipe', - }, - ); - const packageJson = JSON.parse(content.stdout); - return packageJson['node_modules/ag-grid-community'].version; +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 { @@ -26,6 +31,9 @@ export default function (options: Schema): Rule { let { sourceRoot } = options; 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.`, 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"] From c7ca3e02ff53729746e4b91e67a64b606d0dd79f Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Wed, 17 Jul 2024 09:12:12 -0400 Subject: [PATCH 45/95] chore: release 10.37.2 (#2511) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 350fca35ab..2e758f7e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index a2ebad8e1f..c3c9f2a750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.37.1", + "version": "10.37.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.37.1", + "version": "10.37.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a0d4670dd4..06bc67bdba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.37.1", + "version": "10.37.2", "license": "MIT", "scripts": { "ng": "nx", From 2a5a1d153eb801fe196d85ea359bb6ba144f8a2d Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Wed, 17 Jul 2024 13:23:06 -0400 Subject: [PATCH 46/95] fix(components/layout): remove onpush change detection from description list term to dynamically detect inline help inputs from description list content (#2515) --- .../description-list.component.html | 14 ++++++++++++-- .../description-list/description-list.component.ts | 6 ++++++ .../description-list-term.component.ts | 9 +-------- 3 files changed, 19 insertions(+), 10 deletions(-) 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/libs/components/layout/src/lib/modules/description-list/description-list-term.component.ts b/libs/components/layout/src/lib/modules/description-list/description-list-term.component.ts index 9be038bf4d..2cc4be02ef 100644 --- a/libs/components/layout/src/lib/modules/description-list/description-list-term.component.ts +++ b/libs/components/layout/src/lib/modules/description-list/description-list-term.component.ts @@ -1,10 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - TemplateRef, - ViewChild, - inject, -} from '@angular/core'; +import { Component, TemplateRef, ViewChild, inject } from '@angular/core'; import { SkyDescriptionListContentComponent } from './description-list-content.component'; @@ -17,7 +11,6 @@ import { SkyDescriptionListContentComponent } from './description-list-content.c selector: 'sky-description-list-term', templateUrl: './description-list-term.component.html', styleUrl: './description-list-term.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, }) export class SkyDescriptionListTermComponent { @ViewChild('termTemplateRef', { From 82d1ba4583e25fdb4e1aaa34668e06277556c390 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Wed, 17 Jul 2024 14:12:11 -0400 Subject: [PATCH 47/95] chore: release 10.37.3 (#2517) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e758f7e71..908035e63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index c3c9f2a750..64c9c522bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.37.2", + "version": "10.37.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.37.2", + "version": "10.37.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 06bc67bdba..b36b779eb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.37.2", + "version": "10.37.3", "license": "MIT", "scripts": { "ng": "nx", From 39a8bca00bda4ae296d1cd12a10c5c2b3dfea5e0 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:56:04 -0400 Subject: [PATCH 48/95] build: remove sandbox override (#2510) --- karma.conf.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 2e9221dffb..834a65091a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -39,12 +39,7 @@ module.exports = () => { customLaunchers: { ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', - flags: [ - '--no-sandbox', - '--headless', - '--disable-gpu', - '--disable-dev-shm-usage', - ], + flags: ['--headless', '--disable-gpu', '--disable-dev-shm-usage'], }, }, restartOnFileChange: true, From 9c78db4b2fe51a29d1f4371fc1191f525b86a2cc Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Thu, 18 Jul 2024 12:14:22 -0400 Subject: [PATCH 49/95] chore(components/indicators): chevron split scss stylesheets (#2520) --- .../lib/modules/chevron/chevron.component.ts | 7 ++++++- .../chevron/chevron.default.component.scss | 20 +++++++++++++++++++ ...ent.scss => chevron.modern.component.scss} | 7 ++----- 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 libs/components/indicators/src/lib/modules/chevron/chevron.default.component.scss rename libs/components/indicators/src/lib/modules/chevron/{chevron.component.scss => chevron.modern.component.scss} (77%) diff --git a/libs/components/indicators/src/lib/modules/chevron/chevron.component.ts b/libs/components/indicators/src/lib/modules/chevron/chevron.component.ts index 82b2508cee..7bfea8f47b 100644 --- a/libs/components/indicators/src/lib/modules/chevron/chevron.component.ts +++ b/libs/components/indicators/src/lib/modules/chevron/chevron.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { SkyThemeComponentClassDirective } from '@skyux/theme'; /** * Creates an accessible button that wraps the chevron icon. @@ -6,8 +7,12 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; */ @Component({ selector: 'sky-chevron', - styleUrls: ['./chevron.component.scss'], + styleUrls: [ + './chevron.default.component.scss', + './chevron.modern.component.scss', + ], templateUrl: './chevron.component.html', + hostDirectives: [SkyThemeComponentClassDirective], }) export class SkyChevronComponent { /** diff --git a/libs/components/indicators/src/lib/modules/chevron/chevron.default.component.scss b/libs/components/indicators/src/lib/modules/chevron/chevron.default.component.scss new file mode 100644 index 0000000000..c7f36365bb --- /dev/null +++ b/libs/components/indicators/src/lib/modules/chevron/chevron.default.component.scss @@ -0,0 +1,20 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; + +@include mixins.sky-component('default', '.sky-chevron') { + border: none; + background-color: transparent; + flex-shrink: 0; + height: $sky-context-menu-size; + margin: 0; + padding: 0; + overflow: hidden; + width: $sky-context-menu-size; + cursor: pointer; + position: relative; + vertical-align: top; + + &:hover .sky-chevron-part { + border-color: darken($sky-text-color-icon-borderless, 20%); + } +} diff --git a/libs/components/indicators/src/lib/modules/chevron/chevron.component.scss b/libs/components/indicators/src/lib/modules/chevron/chevron.modern.component.scss similarity index 77% rename from libs/components/indicators/src/lib/modules/chevron/chevron.component.scss rename to libs/components/indicators/src/lib/modules/chevron/chevron.modern.component.scss index f3b9d576d9..4267ccd7e0 100644 --- a/libs/components/indicators/src/lib/modules/chevron/chevron.component.scss +++ b/libs/components/indicators/src/lib/modules/chevron/chevron.modern.component.scss @@ -1,10 +1,7 @@ +@use 'libs/components/theme/src/lib/styles/mixins' as mixins; @use 'libs/components/theme/src/lib/styles/variables' as *; -button { - background: lightblue; -} - -.sky-chevron { +@include mixins.sky-component('modern', '.sky-chevron') { border: none; background-color: transparent; flex-shrink: 0; From bc029bd098e98d62123295b5c82ab15fe780ff9d Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Mon, 22 Jul 2024 10:23:27 -0400 Subject: [PATCH 50/95] fix(components/indicators): hide illustration while loading (#2526) --- .../illustration/illustration.component.html | 3 ++ .../illustration.component.spec.ts | 43 ++++++++++++++++++- .../illustration.default.component.scss | 4 ++ .../illustration.modern.component.scss | 4 ++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.component.html b/libs/components/indicators/src/lib/modules/illustration/illustration.component.html index 53b292a1b9..0806eb0584 100644 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.component.html +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.component.html @@ -4,6 +4,9 @@ loading="lazy" [attr.data-sky-illustration-name]="name()" [height]="pixelSize()" + [ngClass]="{ + 'sky-illustration-img-loaded': !!url() + }" [src]="url()" [width]="pixelSize()" /> diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.component.spec.ts b/libs/components/indicators/src/lib/modules/illustration/illustration.component.spec.ts index 2ba5c2168d..ba46885b7c 100644 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.component.spec.ts +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.component.spec.ts @@ -20,13 +20,16 @@ describe('Illustration', () => { provideResolver: boolean, name: string, size: SkyIllustrationSize, + resolver?: { + resolveUrl: (url: string) => Promise; + }, ): void { const providers: Provider[] = []; if (provideResolver) { providers.push({ provide: SkyIllustrationResolverService, - useClass: SkyIllustrationTestResolverService, + useFactory: () => resolver ?? new SkyIllustrationTestResolverService(), }); } @@ -50,6 +53,17 @@ describe('Illustration', () => { expect(getImgEl()?.getAttribute(name)).toBe(expectedValue); } + function validateImageVisibility(expectedVisible: boolean): void { + const imgEl = getImgEl(); + let visibility: string | undefined; + + if (imgEl) { + visibility = getComputedStyle(imgEl).visibility; + } + + expect(visibility).toBe(expectedVisible ? 'visible' : 'hidden'); + } + function detectUrlChanges(): void { fixture.detectChanges(); @@ -101,6 +115,33 @@ describe('Illustration', () => { await expectAsync(fixture.nativeElement).toBeAccessible(); }); + + it('should be hidden until the URL is resolved', fakeAsync(async () => { + // TODO: Use the more concise Promise.withResolvers() when it's available in TypeScript + // to avoid this awkward workaround for strict type checking. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers + let resolveUrl = (_: string): void => void _; + + const resolvePromise = new Promise( + (resolve) => (resolveUrl = resolve), + ); + + setupTest(true, 'test', 'sm', { + resolveUrl: () => resolvePromise, + }); + + detectUrlChanges(); + + validateImageVisibility(false); + + resolveUrl('https://example.com/success.svg'); + + detectUrlChanges(); + + validateImageVisibility(true); + + await resolvePromise; + })); }); describe('without resolver provided', () => { diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss b/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss index 10b86f6eab..d3f89941a3 100644 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.default.component.scss @@ -6,4 +6,8 @@ @include mixins.sky-component('default', '.sky-illustration-img') { display: block; + + &[src=''] { + visibility: hidden; + } } diff --git a/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss b/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss index e6ba64893b..1bc468a515 100644 --- a/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss +++ b/libs/components/indicators/src/lib/modules/illustration/illustration.modern.component.scss @@ -6,4 +6,8 @@ @include mixins.sky-component('modern', '.sky-illustration-img') { display: block; + + &[src=''] { + visibility: hidden; + } } From 09092d7b8d7eae39950cbc3209ff8dae5727251e Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Mon, 22 Jul 2024 10:43:09 -0400 Subject: [PATCH 51/95] fix(components/tiles): remove `::ng-deep` from tile styles (#2527) --- .../tile-content-section.component.scss | 24 +++++++++ .../tile-content/tile-content.component.scss | 8 --- .../tile-content/tile-content.component.ts | 1 - .../tile-dashboard.component.scss | 20 -------- .../modules/tiles/tile/tile.component.scss | 50 ++++++++++++------- 5 files changed, 55 insertions(+), 48 deletions(-) delete mode 100644 libs/components/tiles/src/lib/modules/tiles/tile-content/tile-content.component.scss 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.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; + } + } +} From d25e8cbfac933897f252f1be6e567debf4c486ff Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Mon, 22 Jul 2024 11:46:45 -0400 Subject: [PATCH 52/95] chore: release 10.37.4 (#2531) --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 908035e63c..9947f7ff33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 64c9c522bc..d58cf61ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.37.3", + "version": "10.37.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.37.3", + "version": "10.37.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b36b779eb3..c8911f527a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.37.3", + "version": "10.37.4", "license": "MIT", "scripts": { "ng": "nx", From db223adfcb20767ffc48f9392c2a7722ce109c79 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Tue, 23 Jul 2024 16:53:00 -0400 Subject: [PATCH 53/95] feat(components/core): add scroll shadow directive (#2537) --- libs/components/core/src/index.ts | 3 + .../scroll-shadow.component.fixture.html | 26 ++ .../scroll-shadow.component.fixture.scss | 17 ++ .../scroll-shadow.component.fixture.ts | 25 ++ .../scroll-shadow-event-args.ts} | 2 +- .../scroll-shadow.directive.spec.ts | 180 +++++++++++ .../scroll-shadow/scroll-shadow.directive.ts} | 74 +++-- .../lib/modules/modal/modal.component.html | 3 +- .../lib/modules/modal/modal.component.spec.ts | 286 ++++++++++++------ .../src/lib/modules/modal/modal.component.ts | 26 +- 10 files changed, 499 insertions(+), 143 deletions(-) create mode 100644 libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html create mode 100644 libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss create mode 100644 libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts rename libs/components/{modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts => core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts} (58%) create mode 100644 libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts rename libs/components/{modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts => core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts} (64%) diff --git a/libs/components/core/src/index.ts b/libs/components/core/src/index.ts index 6ce3e553e6..3cc071e7a2 100644 --- a/libs/components/core/src/index.ts +++ b/libs/components/core/src/index.ts @@ -94,6 +94,9 @@ export { SkyResizeObserverService } from './lib/modules/resize-observer/resize-o export { SkyScreenReaderLabelDirective } from './lib/modules/screen-reader-label/screen-reader-label.directive'; +export { SkyScrollShadowDirective } from './lib/modules/scroll-shadow/scroll-shadow.directive'; +export { SkyScrollShadowEventArgs } from './lib/modules/scroll-shadow/scroll-shadow-event-args'; + export { SkyScrollableHostService } from './lib/modules/scrollable-host/scrollable-host.service'; export { SkyStackingContext } from './lib/modules/stacking-context/stacking-context'; diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html new file mode 100644 index 0000000000..0d6d7db432 --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html @@ -0,0 +1,26 @@ +
      +
      +
      + +
      + +
      diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss new file mode 100644 index 0000000000..8556008a69 --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss @@ -0,0 +1,17 @@ +:is(.scroll-shadow-test-header, .scroll-shadow-test-footer) { + height: 100px; +} + +.scroll-shadow-test-body { + overflow-y: scroll; + display: block; + max-height: calc(100vh - 200px); +} + +.scroll-shadow-test-content { + display: block; +} + +.scroll-shadow-test-wrapper { + max-height: 100vh; +} diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts new file mode 100644 index 0000000000..aac1149dff --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectorRef, Component, inject } from '@angular/core'; + +import { SkyScrollShadowEventArgs } from '../scroll-shadow-event-args'; +import { SkyScrollShadowDirective } from '../scroll-shadow.directive'; + +@Component({ + selector: 'sky-scroll-shadow-fixture', + styleUrls: ['./scroll-shadow.component.fixture.scss'], + templateUrl: './scroll-shadow.component.fixture.html', + imports: [CommonModule, SkyScrollShadowDirective], + standalone: true, +}) +export class ScrollShadowFixtureComponent { + public enabled = true; + public height = 400; + public scrollShadow: SkyScrollShadowEventArgs | undefined; + + #changeDetector = inject(ChangeDetectorRef); + + public scrollShadowChange(args: SkyScrollShadowEventArgs): void { + this.scrollShadow = args; + this.#changeDetector.markForCheck(); + } +} diff --git a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts similarity index 58% rename from libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts rename to libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts index 17b6a80d79..6a4df4ccb2 100644 --- a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts @@ -1,7 +1,7 @@ /** * @internal */ -export interface SkyModalScrollShadowEventArgs { +export interface SkyScrollShadowEventArgs { bottomShadow: string; topShadow: string; diff --git a/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts new file mode 100644 index 0000000000..bd68d2e24e --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts @@ -0,0 +1,180 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; + +import { ScrollShadowFixtureComponent } from './fixtures/scroll-shadow.component.fixture'; + +// Wait for the next change detection cycle. This avoids having nested setTimeout() calls +// and using the Jasmine done() function. +function waitForMutationObserver(): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve()); + }); +} + +describe('Scroll shadow directive', () => { + function getScrollBody(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-body'); + } + + function getScrollFooter(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-footer'); + } + + function getScrollHeader(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-header'); + } + + function scrollElement(element: HTMLElement | null, yDistance: number): void { + if (element) { + element.scrollTop = yDistance; + SkyAppTestUtility.fireDomEvent(element, 'scroll'); + fixture.detectChanges(); + } + } + + function validateShadow( + el: HTMLElement | null, + expectedAlpha?: number, + ): void { + if (!el) { + fail('Element not provided'); + return; + } + + const boxShadowStyle = getComputedStyle(el).boxShadow; + + if (expectedAlpha) { + const rgbaMatch = boxShadowStyle.match( + /rgba\(0,\s*0,\s*0,\s*([0-9.]*)\)/, + ); + + if (!(rgbaMatch && rgbaMatch[1])) { + fail('No shadow found'); + } else { + const alpha = parseFloat(rgbaMatch[1]); + + expect(expectedAlpha).toBeCloseTo(alpha, 2); + } + } else { + expect(boxShadowStyle).toBe('none'); + } + } + + let fixture: ComponentFixture; + let cmp: ScrollShadowFixtureComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ScrollShadowFixtureComponent], + }); + + fixture = TestBed.createComponent(ScrollShadowFixtureComponent); + cmp = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it('should not show a shadow when the body is not scrollable when disabled', async () => { + cmp.enabled = false; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should not show a shadow when the body is scrollable when disabled', async () => { + cmp.height = 800; + cmp.enabled = false; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should not show a shadow when the body is not scrollable', async () => { + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should progressively show a drop shadow as the modal content scrolls', async () => { + const headerEl = getScrollHeader(); + const contentEl = getScrollBody(); + const footerEl = getScrollFooter(); + + if (!contentEl) { + fail('Content element not found'); + return; + } + + cmp.height = 800; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + scrollElement(contentEl, 0); + validateShadow(headerEl); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 15); + validateShadow(headerEl, 0.15); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 30); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 31); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.3); + + scrollElement( + contentEl, + contentEl.scrollHeight - 15 - contentEl.clientHeight, + ); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.15); + + scrollElement(contentEl, contentEl.scrollHeight - contentEl.clientHeight); + validateShadow(headerEl, 0.3); + validateShadow(footerEl); + }); + + it('should update the shadow on window resize', async () => { + const headerEl = getScrollHeader(); + const contentEl = getScrollBody(); + const footerEl = getScrollFooter(); + + if (!contentEl) { + fail('Content element not found'); + return; + } + + cmp.height = 800; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(headerEl); + validateShadow(footerEl, 0.3); + + spyOnProperty(Element.prototype, 'scrollTop').and.returnValue(15); + SkyAppTestUtility.fireDomEvent(window, 'resize'); + fixture.detectChanges(); + + validateShadow(headerEl, 0.15); + validateShadow(footerEl, 0.3); + }); +}); diff --git a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts similarity index 64% rename from libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts rename to libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts index 62e5404192..51946b830e 100644 --- a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts @@ -3,37 +3,53 @@ import { ElementRef, EventEmitter, HostListener, + Input, NgZone, OnDestroy, - OnInit, Output, inject, } from '@angular/core'; -import { SkyMutationObserverService } from '@skyux/core'; -import { SkyTheme, SkyThemeService } from '@skyux/theme'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { SkyModalScrollShadowEventArgs } from './modal-scroll-shadow-event-args'; +import { SkyMutationObserverService } from '../mutation/mutation-observer-service'; + +import { SkyScrollShadowEventArgs } from './scroll-shadow-event-args'; /** - * Raises an event when the box shadow for the modal header or footer should be adjusted + * Raises an event when the box shadow for a component's header or footer should be adjusted * based on the scroll position of the host element. * @internal */ @Directive({ standalone: true, - selector: '[skyModalScrollShadow]', + selector: '[skyScrollShadow]', }) -export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { - @Output() - public skyModalScrollShadow = - new EventEmitter(); +export class SkyScrollShadowDirective implements OnDestroy { + @Input() + public set skyScrollShadowEnabled(value: boolean) { + this.#_enabled = value; + + if (value) { + this.#initMutationObserver(); + } else { + this.#emitShadow({ + bottomShadow: 'none', + topShadow: 'none', + }); + + this.#destroyMutationObserver(); + } + } + + public get skyScrollShadowEnabled(): boolean { + return this.#_enabled; + } - #currentShadow: SkyModalScrollShadowEventArgs | undefined; + @Output() + public skyScrollShadow = new EventEmitter(); - #currentTheme: SkyTheme | undefined; + #currentShadow: SkyScrollShadowEventArgs | undefined; #mutationObserver: MutationObserver | undefined; @@ -42,7 +58,8 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { readonly #elRef = inject(ElementRef); readonly #mutationObserverSvc = inject(SkyMutationObserverService); readonly #ngZone = inject(NgZone); - readonly #themeSvc = inject(SkyThemeService, { optional: true }); + + #_enabled = false; @HostListener('window:resize') public windowResize(): void { @@ -54,27 +71,6 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { this.#checkForShadow(); } - public ngOnInit(): void { - if (this.#themeSvc) { - this.#themeSvc.settingsChange - .pipe(takeUntil(this.#ngUnsubscribe)) - .subscribe((themeSettings) => { - this.#currentTheme = themeSettings.currentSettings.theme; - - if (this.#currentTheme === SkyTheme.presets.modern) { - this.#initMutationObserver(); - } else { - this.#emitShadow({ - bottomShadow: 'none', - topShadow: 'none', - }); - - this.#destroyMutationObserver(); - } - }); - } - } - public ngOnDestroy(): void { this.#ngUnsubscribe.next(); this.#ngUnsubscribe.complete(); @@ -112,8 +108,8 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { } #checkForShadow(): void { - if (this.#currentTheme === SkyTheme.presets.modern) { - const el = this.#elRef.nativeElement; + if (this.skyScrollShadowEnabled) { + const el: Element = this.#elRef.nativeElement; const topShadow = this.#buildShadowStyle(el.scrollTop); @@ -136,13 +132,13 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { return opacity > 0 ? `0px 1px 8px 0px rgba(0, 0, 0, ${opacity})` : 'none'; } - #emitShadow(shadow: SkyModalScrollShadowEventArgs): void { + #emitShadow(shadow: SkyScrollShadowEventArgs): void { if ( !this.#currentShadow || this.#currentShadow.bottomShadow !== shadow.bottomShadow || this.#currentShadow.topShadow !== shadow.topShadow ) { - this.skyModalScrollShadow.emit(shadow); + this.skyScrollShadow.emit(shadow); this.#currentShadow = shadow; } } 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; } From e9309cf433a21a0c7e23d4faa9b7df4c19eb167c Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Tue, 23 Jul 2024 21:09:57 -0400 Subject: [PATCH 54/95] chore: release 10.38.0 (#2544) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9947f7ff33..aea9692d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index d58cf61ebb..648e4b35ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.37.4", + "version": "10.38.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.37.4", + "version": "10.38.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c8911f527a..8ba8333605 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.37.4", + "version": "10.38.0", "license": "MIT", "scripts": { "ng": "nx", From deab22a1352bdb10e813087c3e2129926acd4695 Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Wed, 24 Jul 2024 13:40:57 -0400 Subject: [PATCH 55/95] fix(components/lists): remove `::ng-deep` from sort styles (#2538) --- .../lib/modules/sort/sort-item.component.scss | 59 +++++++++++++++++++ .../lib/modules/sort/sort-item.component.ts | 3 + .../src/lib/modules/sort/sort.component.scss | 57 ------------------ .../styles/_public-api/_compat/_mixins.scss | 44 ++++++++------ 4 files changed, 89 insertions(+), 74 deletions(-) create mode 100644 libs/components/lists/src/lib/modules/sort/sort-item.component.scss 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/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(); } } } From 73f9e68d5a0804651df76349618c0c0b17e36bfb Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 25 Jul 2024 10:16:57 -0400 Subject: [PATCH 56/95] fix(components/lookup): lookup aria labels are now set appropriately when using the input box `labelText` input (#2548) --- .../input-box/input-box-host.service.spec.ts | 32 +++ .../input-box/input-box-host.service.ts | 4 + .../input-box/input-box.component.html | 1 + .../modules/input-box/input-box.component.ts | 1 + .../lookup-input-box.component.fixture.html | 10 +- .../lookup-input-box.component.fixture.ts | 24 ++ .../lib/modules/lookup/lookup.component.html | 2 +- .../modules/lookup/lookup.component.spec.ts | 216 +++++++++++++----- .../lib/modules/lookup/lookup.component.ts | 2 + 9 files changed, 235 insertions(+), 57 deletions(-) diff --git a/libs/components/forms/src/lib/modules/input-box/input-box-host.service.spec.ts b/libs/components/forms/src/lib/modules/input-box/input-box-host.service.spec.ts index 63d00a1889..dc6f1957c3 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box-host.service.spec.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box-host.service.spec.ts @@ -20,6 +20,9 @@ describe('Input box host service', () => { ], { ariaDescribedBy: new ReplaySubject(1), + controlId: 'controlId', + labelId: 'labelId', + labelText: 'labelText', }, ); @@ -104,10 +107,39 @@ describe('Input box host service', () => { expect(hostService.controlId).toBe(''); }); + it('should return control ID when host is defined', () => { + hostService.init(mockInputBox); + + expect(hostService.controlId).toBe('controlId'); + }); + + it('should return an empty string for label ID when host is undefined', () => { + expect(hostService.labelId).toBe(''); + }); + + it('should return label ID when host is defined', () => { + hostService.init(mockInputBox); + + expect(hostService.labelId).toBe('labelId'); + }); + + it('should return label ID when host is defined but label text is undefined', () => { + mockInputBox.labelText = undefined; + hostService.init(mockInputBox); + + expect(hostService.labelId).toBe('labelId'); + }); + it('should return an empty string for label text when host is undefined', () => { expect(hostService.labelText).toBe(''); }); + it('should return label text when host is defined', () => { + hostService.init(mockInputBox); + + expect(hostService.labelText).toBe('labelText'); + }); + it('should undefined for ariaDescribedBy when host is undefined', () => { expect(hostService.ariaDescribedBy).toBeUndefined(); }); diff --git a/libs/components/forms/src/lib/modules/input-box/input-box-host.service.ts b/libs/components/forms/src/lib/modules/input-box/input-box-host.service.ts index 2eb8f9213f..3bb062545e 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box-host.service.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box-host.service.ts @@ -16,6 +16,10 @@ export class SkyInputBoxHostService { return this.#host?.controlId ?? ''; } + public get labelId(): string { + return this.#host?.labelText ? this.#host.labelId : ''; + } + public get labelText(): string { return this.#host?.labelText ?? ''; } diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.html b/libs/components/forms/src/lib/modules/input-box/input-box.component.html index bc20306f61..1d1f431644 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.html +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.html @@ -88,6 +88,7 @@ : null " [for]="controlId" + [id]="labelId" [ngClass]="{ 'sky-control-label-required': required }" diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts index 517fa6891b..e39c3aba84 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts @@ -196,6 +196,7 @@ export class SkyInputBoxComponent protected hostHintText: string | undefined; public readonly controlId = this.#idSvc.generateId(); + public readonly labelId = this.#idSvc.generateId(); public readonly errorId = this.#idSvc.generateId(); public readonly hintTextId = this.#idSvc.generateId(); public readonly ariaDescribedBy = new ReplaySubject(1); 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.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..24c9ec3661 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; From 3c8b6b7eacd69c95ed8e1d33c28ccdad6a82c266 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 25 Jul 2024 11:46:40 -0400 Subject: [PATCH 57/95] feat(components/icon): upgrade icons library to 7.5.0 (#2549) --- libs/components/packages/package.json | 2 +- libs/components/theme/package.json | 2 +- libs/components/theme/src/lib/styles/sky.scss | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index 2c4ad02d59..523d6166c0 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.5.0", "@skyux/indicators": "0.0.0-PLACEHOLDER", "@skyux/inline-form": "0.0.0-PLACEHOLDER", "@skyux/layout": "0.0.0-PLACEHOLDER", diff --git a/libs/components/theme/package.json b/libs/components/theme/package.json index 1373813002..488f3ac2a4 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@blackbaud/skyux-design-tokens": "0.0.28", - "@skyux/icons": "7.3.0", + "@skyux/icons": "7.5.0", "fontfaceobserver": "2.3.0", "tslib": "^2.6.2" } diff --git a/libs/components/theme/src/lib/styles/sky.scss b/libs/components/theme/src/lib/styles/sky.scss index 73ebca0ae8..5b346a0dcc 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.5.0/assets/css/skyux-icons.min.css'); diff --git a/package-lock.json b/package-lock.json index 648e4b35ea..2ab94d7715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.3.0", + "@skyux/icons": "7.5.0", "ag-grid-angular": "31.3.4", "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", @@ -9301,9 +9301,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.5.0", + "resolved": "https://registry.npmjs.org/@skyux/icons/-/icons-7.5.0.tgz", + "integrity": "sha512-IA1z6wXuWchms2B47aKsLxanx4XjHWjy1mdECVTY0574TCJSDHqjQHUl43/7U3rRR1mI1PA22IEWrxTt8KpdAQ==" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.1", diff --git a/package.json b/package.json index 8ba8333605..d252cd02a4 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.3.0", + "@skyux/icons": "7.5.0", "ag-grid-angular": "31.3.4", "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", From 55e7030cbd5dc7fd19e69917ab9ba7ef6f06de22 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Thu, 25 Jul 2024 12:38:26 -0400 Subject: [PATCH 58/95] fix(components/help-inline): set `aria-controls` on help inline button when widget element exists (#2541) --- .../playground/src/app/shared/help.service.ts | 4 +- .../src/lib/modules/help/help.service.spec.ts | 10 ++++ .../core/src/lib/modules/help/help.service.ts | 9 ++++ .../src/help/help-testing-controller.spec.ts | 8 +++ .../src/help/help-testing-controller.ts | 1 + .../testing/src/help/help-testing.service.ts | 7 +++ .../help-inline-aria-controls.pipe.ts | 11 +++- .../help-inline/help-inline.component.html | 6 ++- .../help-inline/help-inline.component.spec.ts | 54 ++++++++++++++++++- 9 files changed, 103 insertions(+), 7 deletions(-) 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/libs/components/core/src/lib/modules/help/help.service.spec.ts b/libs/components/core/src/lib/modules/help/help.service.spec.ts index 5a4a79cd5f..15bc4bee9d 100644 --- a/libs/components/core/src/lib/modules/help/help.service.spec.ts +++ b/libs/components/core/src/lib/modules/help/help.service.spec.ts @@ -1,5 +1,7 @@ import { TestBed } from '@angular/core/testing'; +import { firstValueFrom } from 'rxjs'; + import { SkyHelpService } from './help.service'; describe('Help service', () => { @@ -18,4 +20,12 @@ describe('Help service', () => { }), ).toThrowError(); }); + + it('should default widget ready state to false', async () => { + const helpService = TestBed.inject(SkyHelpService); + + await expectAsync( + firstValueFrom(helpService.widgetReadyStateChange), + ).toBeResolvedTo(false); + }); }); diff --git a/libs/components/core/src/lib/modules/help/help.service.ts b/libs/components/core/src/lib/modules/help/help.service.ts index 07ef4efae5..9029539f84 100644 --- a/libs/components/core/src/lib/modules/help/help.service.ts +++ b/libs/components/core/src/lib/modules/help/help.service.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + import { SkyHelpOpenArgs } from './help-open-args'; /** @@ -8,6 +10,13 @@ import { SkyHelpOpenArgs } from './help-open-args'; */ @Injectable() export abstract class SkyHelpService { + /** + * Emits when the help widget ready state changes. + */ + public get widgetReadyStateChange(): Observable { + return of(false); + } + /** * Opens a globally accessible help dialog. * @param args The options for opening the help dialog. diff --git a/libs/components/core/testing/src/help/help-testing-controller.spec.ts b/libs/components/core/testing/src/help/help-testing-controller.spec.ts index a91c20f43e..00910d27d1 100644 --- a/libs/components/core/testing/src/help/help-testing-controller.spec.ts +++ b/libs/components/core/testing/src/help/help-testing-controller.spec.ts @@ -1,6 +1,8 @@ import { TestBed } from '@angular/core/testing'; import { SkyHelpService } from '@skyux/core'; +import { firstValueFrom } from 'rxjs'; + import { SkyHelpTestingController } from './help-testing-controller'; import { SkyHelpTestingModule } from './help-testing.module'; @@ -50,4 +52,10 @@ describe('Help testing controller', () => { `Expected current help key to be 'test', but the current help key is undefined.`, ); }); + + it('should set widget ready state to "true" on init', async () => { + await expectAsync( + firstValueFrom(helpSvc.widgetReadyStateChange), + ).toBeResolvedTo(true); + }); }); diff --git a/libs/components/core/testing/src/help/help-testing-controller.ts b/libs/components/core/testing/src/help/help-testing-controller.ts index d08aaebda1..d919b361e4 100644 --- a/libs/components/core/testing/src/help/help-testing-controller.ts +++ b/libs/components/core/testing/src/help/help-testing-controller.ts @@ -1,4 +1,5 @@ import { inject } from '@angular/core'; +// eslint-disable-next-line @nx/enforce-module-boundaries import { SkyHelpService } from '@skyux/core'; import { SkyHelpTestingService } from './help-testing.service'; diff --git a/libs/components/core/testing/src/help/help-testing.service.ts b/libs/components/core/testing/src/help/help-testing.service.ts index 0b9a73f138..709a06c87f 100644 --- a/libs/components/core/testing/src/help/help-testing.service.ts +++ b/libs/components/core/testing/src/help/help-testing.service.ts @@ -1,11 +1,18 @@ import { Injectable } from '@angular/core'; +// eslint-disable-next-line @nx/enforce-module-boundaries import { SkyHelpOpenArgs, SkyHelpService } from '@skyux/core'; +import { Observable, of } from 'rxjs'; + /** * @internal */ @Injectable() export class SkyHelpTestingService extends SkyHelpService { + public override get widgetReadyStateChange(): Observable { + return of(true); + } + #currentHelpKey: string | undefined; public override openHelp(args: SkyHelpOpenArgs): void { diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline-aria-controls.pipe.ts b/libs/components/help-inline/src/lib/modules/help-inline/help-inline-aria-controls.pipe.ts index 64b3c02a93..e191019af6 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline-aria-controls.pipe.ts +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline-aria-controls.pipe.ts @@ -17,9 +17,16 @@ export class SkyHelpInlineAriaControlsPipe implements PipeTransform { ariaControls: string | undefined, popoverId: string | undefined, helpKey: string | undefined, + widgetReadyState: boolean | null, ): string | undefined { - if (helpKey && this.#helpGlobalOptions) { - return this.#helpGlobalOptions.ariaControls; + if (helpKey) { + if (!widgetReadyState) { + return; + } + + if (this.#helpGlobalOptions) { + return this.#helpGlobalOptions.ariaControls; + } } return popoverId || ariaControls; diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html index 023941d65c..d5809a3ad3 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.html @@ -2,7 +2,11 @@ class="sky-help-inline" type="button" [attr.aria-controls]=" - ariaControls | skyHelpInlineAriaControls: popoverId : helpKey + ariaControls + | skyHelpInlineAriaControls + : popoverId + : helpKey + : (helpSvc?.widgetReadyStateChange | async) " [attr.aria-expanded]=" ariaExpanded | skyHelpInlineAriaExpanded: ariaControls : isPopoverOpened diff --git a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.spec.ts b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.spec.ts index 091a1009ae..960e053a59 100644 --- a/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.spec.ts +++ b/libs/components/help-inline/src/lib/modules/help-inline/help-inline.component.spec.ts @@ -60,7 +60,7 @@ describe('Help inline component', () => { options.ariaExpanded, ); - if (options.ariaHaspopup) { + if (options.ariaHaspopup !== undefined) { expect(helpInlineEl?.getAttribute('aria-haspopup')).toBe( options.ariaHaspopup, ); @@ -90,11 +90,14 @@ describe('Help inline component', () => { let mockThemeSvc: { settingsChange: BehaviorSubject }; let uniqueId = 0; let mockHelpSvc: jasmine.SpyObj; + let readyStateChange: BehaviorSubject; function setupTest( provideHelpSvc?: boolean, globalOptions?: SkyHelpGlobalOptions, ): void { + readyStateChange = new BehaviorSubject(false); + mockThemeSvc = { settingsChange: new BehaviorSubject({ currentSettings: new SkyThemeSettings( @@ -105,7 +108,16 @@ describe('Help inline component', () => { }), }; - mockHelpSvc = jasmine.createSpyObj('SkyHelpService', ['openHelp']); + mockHelpSvc = jasmine.createSpyObj( + 'SkyHelpService', + ['openHelp'], + ['widgetReadyStateChange'], + ); + + ( + Object.getOwnPropertyDescriptor(mockHelpSvc, 'widgetReadyStateChange') + ?.get as unknown as jasmine.Spy + ).and.returnValue(readyStateChange); const providers: Provider[] = [ { provide: SkyThemeService, useValue: mockThemeSvc }, @@ -434,6 +446,7 @@ describe('Help inline component', () => { }); component.helpKey = 'test.html'; + readyStateChange.next(true); fixture.detectChanges(); @@ -445,6 +458,43 @@ describe('Help inline component', () => { }); }); + it('should pass accessibility when a non-existent element is provided to the global ariaControls', async () => { + const ariaControls = 'lazy-element'; + + setupTest(true, { + ariaControls, + ariaHaspopup: 'dialog', + }); + + component.helpKey = 'foo.html'; + fixture.detectChanges(); + + await checkAriaPropertiesAndAccessibility({ + ariaLabel: 'Show help content', + ariaControls: null, + ariaExpanded: null, + ariaHaspopup: 'dialog', + }); + + // Create the ariaControls element. + const div = document.createElement('div'); + div.id = ariaControls; + document.body.appendChild(div); + + // Fire the ready state change event. + readyStateChange.next(true); + fixture.detectChanges(); + + await checkAriaPropertiesAndAccessibility({ + ariaLabel: 'Show help content', + ariaControls, + ariaExpanded: null, + ariaHaspopup: 'dialog', + }); + + div.remove(); + }); + describe('and help service is not provided but helpKey is specified', () => { it('should hide the help button', () => { setupTest(false); From f0bf189ed6fe05fbaa22ca3a7195b2ab261cbbce Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Thu, 25 Jul 2024 12:48:34 -0400 Subject: [PATCH 59/95] chore: release 10.39.0 (#2546) --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea9692d81..9ee69ff308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 2ab94d7715..876d8d8cb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.38.0", + "version": "10.39.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.38.0", + "version": "10.39.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d252cd02a4..387897bb2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.38.0", + "version": "10.39.0", "license": "MIT", "scripts": { "ng": "nx", From 4c6e357d3f9d8d426b7f08d93e6d982c59789f31 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Fri, 26 Jul 2024 10:26:33 -0400 Subject: [PATCH 60/95] fix(components/lists): repeater focus styles show on focus-visible in modern theme (#2554) --- .../src/lib/modules/repeater/repeater-item.component.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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; } From 93404bf9a97eddcc9955139d374d7218d1f678ea Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Fri, 26 Jul 2024 12:47:42 -0400 Subject: [PATCH 61/95] feat(components/forms): improve file attachments error messaging for incorrect file types (#2553) --- .../src/assets/locales/resources_en_US.json | 4 ++-- .../file-attachment.component.html | 3 ++- .../file-attachment.component.spec.ts | 8 ++------ .../file-attachment/file-attachment.component.ts | 7 +++++++ .../file-attachment/file-attachment.service.ts | 13 ++++++++++++- .../file-attachment/file-drop.component.html | 3 ++- .../file-attachment/file-drop.component.spec.ts | 16 +++++----------- .../file-attachment/file-drop.component.ts | 7 +++++++ .../modules/shared/sky-forms-resources.module.ts | 4 ++-- 9 files changed, 41 insertions(+), 24 deletions(-) diff --git a/libs/components/forms/src/assets/locales/resources_en_US.json b/libs/components/forms/src/assets/locales/resources_en_US.json index 620010086c..8893f48560 100644 --- a/libs/components/forms/src/assets/locales/resources_en_US.json +++ b/libs/components/forms/src/assets/locales/resources_en_US.json @@ -177,11 +177,11 @@ }, "skyux_file_attachment_file_type_error_label_text": { "_description": "The error message for file attachment incorrect file type error", - "message": "Upload a file of type {0}." + "message": "Upload one of these file types: {0}." }, "skyux_file_attachment_file_type_error_label_text_with_name": { "_description": "The error message for file attachment incorrect file type error", - "message": "{0}: Upload a file of type {1}." + "message": "{0}: Upload one of these file types: {1}." }, "skyux_file_attachment_max_file_size_error_label_text": { "_description": "The error message for file attachment max file size error", diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html index bb9c2e624a..86a56b2707 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.html @@ -178,7 +178,8 @@ *ngIf="fileErrorName === 'fileType'" [errorName]="'fileType'" [errorText]=" - 'skyux_file_attachment_file_type_error_label_text' + acceptedTypesErrorMessage ?? + 'skyux_file_attachment_file_type_error_label_text' | skyLibResources: fileErrorParam " /> diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts index 1e32ba8549..dd6d9e78cf 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts @@ -1243,9 +1243,7 @@ describe('File attachment', () => { expect(fileChangeActual?.file?.file.name).toBe('woo.txt'); expect(fileChangeActual?.file?.file.size).toBe(2000); expect(fileChangeActual?.file?.errorType).toBe('fileType'); - expect(fileChangeActual?.file?.errorParam).toBe( - fileAttachmentInstance.acceptedTypes, - ); + expect(fileChangeActual?.file?.errorParam).toBe('PNG, TIFF'); }); it('should reject a file with no type when accepted types are defined', () => { @@ -1271,9 +1269,7 @@ describe('File attachment', () => { expect(fileChangeActual?.file?.file.name).toBe('foo.txt'); expect(fileChangeActual?.file?.file.size).toBe(1000); expect(fileChangeActual?.file?.errorType).toBe('fileType'); - expect(fileChangeActual?.file?.errorParam).toBe( - fileAttachmentInstance.acceptedTypes, - ); + expect(fileChangeActual?.file?.errorParam).toBe('PNG, TIFF'); }); it('should allow the user to specify accepted type with wildcards', () => { diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts index 784f7bc9a4..eb12bc7282 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts @@ -73,6 +73,13 @@ export class SkyFileAttachmentComponent @Input() public acceptedTypes: string | undefined; + /** + * A custom error message to display when a file doesn't match the accepted types. + * This replaces a default error message that lists all accepted types. + */ + @Input() + public acceptedTypesErrorMessage: string | undefined; + /** * Whether to disable the input 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. diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.service.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.service.ts index cfe2d98b70..9fb5fa1064 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.service.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.service.ts @@ -32,7 +32,7 @@ export class SkyFileAttachmentService { fileResults.push(fileItem); } else if (this.fileTypeRejected(fileItem.file.type, acceptedTypes)) { fileItem.errorType = 'fileType'; - fileItem.errorParam = acceptedTypes; + fileItem.errorParam = this.#getAcceptedTypesList(acceptedTypes); fileResults.push(fileItem); } else if (validateFn) { const errorParam = validateFn(fileItem); @@ -100,6 +100,17 @@ export class SkyFileAttachmentService { return false; } + #getAcceptedTypesList(rawTypes: string | undefined): string | undefined { + return rawTypes + ?.toUpperCase() + .split(',') + .map((type) => { + const subType = this.#getMimeSubtype(type); + return subType.startsWith('X-') ? subType.substr(2) : subType; + }) + .join(', '); + } + #getMimeSubtype(type: string): string { return type.substr(type.indexOf('/') + 1, type.length); } diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html index 91fe8a77f6..b37873b36b 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.html @@ -189,7 +189,8 @@ *ngIf="rejectedFile.errorType === 'maxFileSize'" errorName="maxFileSize" [errorText]=" - 'skyux_file_attachment_max_file_size_error_label_text_with_name' + acceptedTypesErrorMessage ?? + 'skyux_file_attachment_max_file_size_error_label_text_with_name' | skyLibResources : rejectedFile.file.name : (rejectedFile.errorParam | skyFileSize) diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts index ced4028886..b6b2796b1f 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts @@ -474,7 +474,7 @@ describe('File drop component', () => { expect(typeError).toBeVisible(); expect(typeError.textContent).toContain( - 'Upload a file of type image/png, image/jpeg.', + 'Upload one of these file types: PNG, JPEG.', ); }); @@ -859,7 +859,7 @@ describe('File drop component', () => { (filesChanged: SkyFileDropChange) => (filesChangedActual = filesChanged), ); - componentInstance.acceptedTypes = 'image/png,image/tiff'; + componentInstance.acceptedTypes = 'image/png,audio/x-midi'; fixture.detectChanges(); @@ -869,9 +869,7 @@ describe('File drop component', () => { expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('woo.txt'); expect(filesChangedActual?.rejectedFiles[0].file.size).toBe(2000); expect(filesChangedActual?.rejectedFiles[0].errorType).toBe('fileType'); - expect(filesChangedActual?.rejectedFiles[0].errorParam).toBe( - componentInstance.acceptedTypes, - ); + expect(filesChangedActual?.rejectedFiles[0].errorParam).toBe('PNG, MIDI'); expect(filesChangedActual?.files.length).toBe(1); expect(filesChangedActual?.files[0].url).toBe('url'); @@ -911,16 +909,12 @@ describe('File drop component', () => { expect(filesChangedActual?.rejectedFiles[1].file.name).toBe('woo.txt'); expect(filesChangedActual?.rejectedFiles[1].file.size).toBe(2000); expect(filesChangedActual?.rejectedFiles[1].errorType).toBe('fileType'); - expect(filesChangedActual?.rejectedFiles[1].errorParam).toBe( - componentInstance.acceptedTypes, - ); + expect(filesChangedActual?.rejectedFiles[1].errorParam).toBe('PNG, TIFF'); expect(filesChangedActual?.rejectedFiles[0].file.name).toBe('foo.txt'); expect(filesChangedActual?.rejectedFiles[0].file.size).toBe(1000); expect(filesChangedActual?.rejectedFiles[0].errorType).toBe('fileType'); - expect(filesChangedActual?.rejectedFiles[0].errorParam).toBe( - componentInstance.acceptedTypes, - ); + expect(filesChangedActual?.rejectedFiles[0].errorParam).toBe('PNG, TIFF'); expect(liveAnnouncerSpy.calls.count()).toBe(0); }); diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts index 59b82029e8..e2844c0d81 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts @@ -134,6 +134,13 @@ export class SkyFileDropComponent implements OnInit, OnDestroy { @Input() public acceptedTypes: string | undefined; + /** + * A custom error message to display when a file doesn't match the accepted types. + * This replaces a default error message that lists all accepted types. + */ + @Input() + public acceptedTypesErrorMessage: string | undefined; + /** * Whether to disable the option to browse for files to attach. */ diff --git a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts index 44110f2b64..36f7b98283 100644 --- a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts +++ b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts @@ -113,10 +113,10 @@ const RESOURCES: Record = { message: 'Link to {0} removed.', }, skyux_file_attachment_file_type_error_label_text: { - message: 'Upload a file of type {0}.', + message: 'Upload one of these file types: {0}.', }, skyux_file_attachment_file_type_error_label_text_with_name: { - message: '{0}: Upload a file of type {1}.', + message: '{0}: Upload one of these file types: {1}.', }, skyux_file_attachment_max_file_size_error_label_text: { message: 'Upload a file under {0}.', From ae73ccbf05ca878d92be47c28b44cd8760fef17d Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 26 Jul 2024 12:56:10 -0400 Subject: [PATCH 62/95] chore: release 10.40.0 (#2559) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee69ff308..3ebfb43c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 876d8d8cb2..f616a6886b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.39.0", + "version": "10.40.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.39.0", + "version": "10.40.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 387897bb2f..70c238c4f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.39.0", + "version": "10.40.0", "license": "MIT", "scripts": { "ng": "nx", From 0337f7825431699609f668d3408d353b59ba6896 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Mon, 29 Jul 2024 13:31:43 -0400 Subject: [PATCH 63/95] fix(components/modals): `SkyModalTestingController.closeTopModal` passes `reason` and `result` to the modal instance (#2565) --- .../modal-testing.controller.spec.ts | 22 +++++++++++++++++++ .../modal/controller/modal-testing.service.ts | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) 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 { From 1cd7b1e2052f23a0a88b5bfdace41b36c852330a Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 30 Jul 2024 16:35:35 -0400 Subject: [PATCH 64/95] feat: move help inline features out of developer preview for key info, box, and description list (#2574) --- .../src/lib/modules/key-info/key-info.component.ts | 3 --- .../components/layout/src/lib/modules/box/box.component.ts | 7 ------- .../description-list/description-list-content.component.ts | 3 --- 3 files changed, 13 deletions(-) diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts index 3403a200da..f7f9edf886 100644 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts @@ -14,7 +14,6 @@ export class SkyKeyInfoComponent { /** * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is * placed beside the key info. Clicking the button invokes global help as configured by the application. - * @preview */ @Input() public helpKey: string | undefined; @@ -23,7 +22,6 @@ export class SkyKeyInfoComponent { * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the key info. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) * when clicked using the specified content and optional title. - * @preview */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -31,7 +29,6 @@ export class SkyKeyInfoComponent { /** * The title of the help popover. This property only applies when `helpPopoverContent` is * also specified. - * @preview */ @Input() public helpPopoverTitle: string | undefined; diff --git a/libs/components/layout/src/lib/modules/box/box.component.ts b/libs/components/layout/src/lib/modules/box/box.component.ts index 02d7b632a3..535ccc3dc4 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.ts +++ b/libs/components/layout/src/lib/modules/box/box.component.ts @@ -42,7 +42,6 @@ function numberAttribute2(value: unknown): number { export class SkyBoxComponent { /** * The text to display as the box's heading. - * @preview */ @Input() public set headingText(value: string | undefined) { @@ -59,14 +58,12 @@ export class SkyBoxComponent { /** * Indicates whether to hide the `headingText`. - * @preview */ @Input({ transform: booleanAttribute }) public headingHidden = false; /** * The semantic heading level in the document structure. The default is 2. - * @preview * @default 2 */ @Input({ transform: numberAttribute2 }) @@ -74,7 +71,6 @@ export class SkyBoxComponent { /** * The heading [font style](https://developer.blackbaud.com/skyux/design/styles/typography#headings). - * @preview * @default 2 */ @Input({ transform: numberAttribute2 }) @@ -86,7 +82,6 @@ export class SkyBoxComponent { * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the box heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) * when clicked using the specified content and optional title. - * @preview */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -94,7 +89,6 @@ export class SkyBoxComponent { /** * The title of the help popover. This property only applies when `helpPopoverContent` is * also specified. - * @preview */ @Input() public helpPopoverTitle: string | undefined; @@ -103,7 +97,6 @@ export class SkyBoxComponent { * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the box heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) * as configured by the application. - * @preview */ @Input() public helpKey: string | undefined; diff --git a/libs/components/layout/src/lib/modules/description-list/description-list-content.component.ts b/libs/components/layout/src/lib/modules/description-list/description-list-content.component.ts index d3c1c94393..74e482d86f 100644 --- a/libs/components/layout/src/lib/modules/description-list/description-list-content.component.ts +++ b/libs/components/layout/src/lib/modules/description-list/description-list-content.component.ts @@ -28,7 +28,6 @@ export class SkyDescriptionListContentComponent { /** * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is * placed beside the description list content label. Clicking the button invokes global help as configured by the application. - * @preview */ @Input() public helpKey: string | undefined; @@ -37,7 +36,6 @@ export class SkyDescriptionListContentComponent { * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the description list content. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) * when clicked using the specified content and optional title. - * @preview */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -45,7 +43,6 @@ export class SkyDescriptionListContentComponent { /** * The title of the help popover. This property only applies when `helpPopoverContent` is * also specified. - * @preview */ @Input() public helpPopoverTitle: string | undefined; From 944c3299c608e616475c4b1c0e3bd4ce6694311c Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Wed, 31 Jul 2024 12:22:45 -0400 Subject: [PATCH 65/95] fix(components/layout): fix key info display in page summary (#2576) --- ...rn.component.scss => key-info.component.scss} | 6 ++---- .../lib/modules/key-info/key-info.component.ts | 5 +---- .../key-info/key-info.default.component.scss | 16 ---------------- .../page-summary-key-info.component.scss | 4 ++-- 4 files changed, 5 insertions(+), 26 deletions(-) rename libs/components/indicators/src/lib/modules/key-info/{key-info.modern.component.scss => key-info.component.scss} (58%) delete mode 100644 libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss b/libs/components/indicators/src/lib/modules/key-info/key-info.component.scss similarity index 58% rename from libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss rename to libs/components/indicators/src/lib/modules/key-info/key-info.component.scss index 272a3737cd..eae1d027f7 100644 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.modern.component.scss +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.component.scss @@ -1,7 +1,5 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; - -@include mixins.sky-component('modern', '.sky-key-info') { - display: inline-block; +.sky-key-info { + display: var(--sky-host-key-info-display, inline-block); &.sky-key-info-horizontal { .sky-key-info-value, diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts index f7f9edf886..cb64f359cb 100644 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts +++ b/libs/components/indicators/src/lib/modules/key-info/key-info.component.ts @@ -5,10 +5,7 @@ import { SkyKeyInfoLayoutType } from './key-info-layout-type'; @Component({ selector: 'sky-key-info', templateUrl: './key-info.component.html', - styleUrls: [ - './key-info.default.component.scss', - './key-info.modern.component.scss', - ], + styleUrls: ['./key-info.component.scss'], }) export class SkyKeyInfoComponent { /** diff --git a/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss b/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss deleted file mode 100644 index 9a8c8318dc..0000000000 --- a/libs/components/indicators/src/lib/modules/key-info/key-info.default.component.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use 'libs/components/theme/src/lib/styles/mixins' as mixins; - -@include mixins.sky-component('default', '.sky-key-info') { - display: inline-block; - - &.sky-key-info-horizontal { - .sky-key-info-value, - .sky-key-info-label { - display: inline-block; - } - - .sky-key-info-label { - padding: 0 0 0 var(--sky-margin-inline-xs); - } - } -} diff --git a/libs/components/layout/src/lib/modules/page-summary/page-summary-key-info.component.scss b/libs/components/layout/src/lib/modules/page-summary/page-summary-key-info.component.scss index 03d33d64cf..b744cc2f68 100644 --- a/libs/components/layout/src/lib/modules/page-summary/page-summary-key-info.component.scss +++ b/libs/components/layout/src/lib/modules/page-summary/page-summary-key-info.component.scss @@ -13,6 +13,6 @@ } } -.sky-page-summary-key-info ::ng-deep .sky-key-info { - display: block; +.sky-page-summary-key-info { + --sky-host-key-info-display: block; } From 682dc3bff50362f4b147b65c0a17ab55d28059d1 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Thu, 1 Aug 2024 10:47:46 -0400 Subject: [PATCH 66/95] chore: release 10.41.0 (#2568) --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ebfb43c04..4b846e5795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index f616a6886b..0b19ed6fc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.40.0", + "version": "10.41.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.40.0", + "version": "10.41.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 70c238c4f4..e6e9c136e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.40.0", + "version": "10.41.0", "license": "MIT", "scripts": { "ng": "nx", From 79ed2d0ed802a23a2f2089e1e3944240f39a16d9 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Fri, 2 Aug 2024 10:54:42 -0400 Subject: [PATCH 67/95] fix(components/router): fix `SkyHrefHarness` to find elements when `skyHref` is bound to a template variable (#2580) --- .../href/fixtures/href-harness-test.component.html | 3 +++ .../src/href/fixtures/href-harness-test.component.ts | 4 +++- .../router/testing/src/href/href-harness.spec.ts | 11 +++++++++++ .../router/testing/src/href/href-harness.ts | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) 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 From fc631c935c6ba8d02c1a5c53e5e6212406248e4c Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 2 Aug 2024 11:03:00 -0400 Subject: [PATCH 68/95] chore: release 10.41.1 (#2582) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b846e5795..d82c042122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 0b19ed6fc5..b0e23b9d77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.41.0", + "version": "10.41.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.41.0", + "version": "10.41.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e6e9c136e6..c91867a46c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.41.0", + "version": "10.41.1", "license": "MIT", "scripts": { "ng": "nx", From 894a3c476d8012fdc553040671436ae79933546b Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 6 Aug 2024 10:47:04 -0400 Subject: [PATCH 69/95] test: add ESLint rules from `@angular-eslint/template` (#2588) --- .eslintrc-overrides.json | 21 ++++++++++++++++++- .../modules/tiles/tile/tile.component.html | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) 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/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.html index 61279e52b8..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 @@ -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" > From 6abe29cbe423d7a276adfa69093725150b88ef36 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Wed, 7 Aug 2024 14:29:30 -0400 Subject: [PATCH 70/95] fix(components/colorpicker): match label text to easy mode label styling (#2591) --- .../colorpicker/colorpicker.component.html | 18 +++++++++--------- .../colorpicker/colorpicker.component.html | 8 ++++---- .../colorpicker/colorpicker.component.scss | 6 ++++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/playground/src/app/components/colorpicker/colorpicker/colorpicker.component.html b/apps/playground/src/app/components/colorpicker/colorpicker/colorpicker.component.html index 1c0a6da725..df78557d98 100644 --- a/apps/playground/src/app/components/colorpicker/colorpicker/colorpicker.component.html +++ b/apps/playground/src/app/components/colorpicker/colorpicker/colorpicker.component.html @@ -1,14 +1,14 @@ -
      +
      - + @if ("favoriteColor.errors?.['opaque']") { + + }
      - 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..fd2492d348 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 @@

      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/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-input.directive.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field-input.directive.ts index ffe43bc89c..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 @@ -18,7 +18,7 @@ import { } from '@angular/forms'; import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber'; -import { Subject, takeUntil } from 'rxjs'; +import { Subject, take, takeUntil } from 'rxjs'; import { SkyPhoneFieldAdapterService } from './phone-field-adapter.service'; import { SkyPhoneFieldComponent } from './phone-field.component'; @@ -119,8 +119,15 @@ export class SkyPhoneFieldInputDirective .subscribe(() => { const value = this.#adapterSvc?.getInputValue(this.#elRef); this.#setValue(value); - this.#notifyChange?.(this.#getValue()); - this.#notifyTouched?.(); + this.#control?.updateValueAndValidity(); + }); + + this.#phoneFieldComponent.countrySearchForm + .get('countrySearch') + ?.valueChanges.pipe(takeUntil(this.#ngUnsubscribe), take(1)) + .subscribe(() => { + this.#control?.markAsDirty(); + this.#control?.markAsTouched(); }); } 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 3c66617af6..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); } @@ -826,7 +833,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, ngModel, @@ -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')); @@ -1936,7 +1986,7 @@ describe('Phone Field Component', () => { validateInputAndModel( '6675555309', - '6675555309', + '(667) 555-5309', false, true, component.phoneControl, @@ -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'; 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 53e4d48fcb..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 @@ -241,6 +241,7 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { } this.#populateInputBoxHelpText(); + this.selectedCountryChange.emit(this.#_selectedCountry); } } @@ -357,7 +358,6 @@ export class SkyPhoneFieldComponent implements OnDestroy, OnInit { (newValue: SkyCountryFieldCountry | undefined | null) => { if (newValue?.iso2 !== this.selectedCountry?.iso2) { this.selectedCountry = newValue || undefined; - this.selectedCountryChange.emit(this.selectedCountry); } }, ); From 7250a207568ab2cf04b73f949ff84bdb4c57c906 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Fri, 16 Aug 2024 13:42:34 -0400 Subject: [PATCH 76/95] fix(components/lookup): autocomplete and lookup show wait in dropdown when using an async search when no dropdown was previously open (#2610) --- .../lookup/lookup/lookup.component.html | 28 +++---- .../autocomplete.component.spec.ts | 79 +++++++++++++++++++ .../autocomplete/autocomplete.component.ts | 3 + 3 files changed, 96 insertions(+), 14 deletions(-) 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/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(() => { From 970f07b05530c9f02ae440b3bab410a16b3c94aa Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Sat, 17 Aug 2024 10:54:47 -0400 Subject: [PATCH 77/95] chore: release 10.41.3 (#2605) --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 620e2855fc..89dc4f25d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 9b4f99f92d..d9a541e39b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.41.2", + "version": "10.41.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.41.2", + "version": "10.41.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4060c97be1..0e0c50ec44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.41.2", + "version": "10.41.3", "license": "MIT", "scripts": { "ng": "nx", From e28887ffabf7831870501dc61749446e45be4e22 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Tue, 20 Aug 2024 14:34:32 -0400 Subject: [PATCH 78/95] deprecation(components/lookup): lookup component's `data` input has been deprecated and consumers should use the `searchAsync` event instead (#2617) --- .../modules/lookup/lookup-autocomplete-adapter.ts | 15 +++++++++------ .../src/lib/modules/lookup/lookup.component.ts | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) 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.ts b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts index 24c9ec3661..5bbd173c42 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts @@ -95,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) { From a14707f1ffed28ea94851ebc786ac47be8bc65c6 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:15:38 -0400 Subject: [PATCH 79/95] chore: skyux-dev-infra 10.0.0-alpha.10 (#2616) --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9a541e39b..94c6b9cd44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "@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.7", + "@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", @@ -9146,12 +9146,12 @@ } }, "node_modules/@skyux/dev-infra-private": { - "version": "10.0.0-alpha.7", - "resolved": "git+ssh://git@github.com/blackbaud/skyux-dev-infra-private-builds.git#8f26d24cfaee3d0c6eddb1f5d5eecb31f430bb50", + "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", @@ -13126,11 +13126,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" } diff --git a/package.json b/package.json index 0e0c50ec44..2a20e936bd 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "@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.7", + "@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", From 041dc29197b625106a4874c72fb396fa2424c11d Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 22 Aug 2024 17:29:41 -0400 Subject: [PATCH 80/95] fix(code-examples): update lookup code examples to all use the `searchAsync` event (#2621) --- .../lookup/add-item/demo.component.html | 2 +- .../lookup/lookup/async/demo.component.html | 36 ----- .../lookup/async/demo.component.spec.ts | 110 -------------- .../lookup/lookup/async/demo.component.ts | 110 -------------- .../lookup/lookup/async/person.ts | 3 - .../lookup/custom-picker/demo.component.html | 5 +- .../custom-picker/demo.component.spec.ts | 120 +++++++++++++++ .../lookup/custom-picker/demo.component.ts | 138 +++++------------ .../lookup/custom-picker/demo.service.ts | 121 +++++++++++++++ .../custom-picker/picker-modal.component.html | 46 +++--- .../custom-picker/picker-modal.component.scss | 3 + .../custom-picker/picker-modal.component.ts | 47 ++++-- .../search-results.ts | 0 .../lookup/multi-select/demo.component.html | 16 +- .../multi-select/demo.component.spec.ts | 53 ++++++- .../lookup/multi-select/demo.component.ts | 96 ++++++------ .../{async => multi-select}/demo.service.ts | 0 .../lookup/multi-select/search-results.ts | 7 + .../result-templates/demo.component.html | 10 +- .../result-templates/demo.component.spec.ts | 67 +++++++++ .../lookup/result-templates/demo.component.ts | 140 ++++++------------ .../lookup/result-templates/demo.service.ts | 121 +++++++++++++++ .../lookup/result-templates/search-results.ts | 7 + .../lookup/single-select/demo.component.html | 18 ++- .../single-select/demo.component.spec.ts | 57 ++++++- .../lookup/single-select/demo.component.ts | 95 ++++++------ .../lookup/single-select/demo.service.ts | 61 ++++++++ .../lookup/single-select/search-results.ts | 7 + .../src/app/features/lookup.module.ts | 7 - .../src/app/home/home.component.html | 3 +- 30 files changed, 889 insertions(+), 617 deletions(-) delete mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.html delete mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.spec.ts delete mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.ts delete mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/async/person.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/demo.service.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/custom-picker/picker-modal.component.scss rename apps/code-examples/src/app/code-examples/lookup/lookup/{async => custom-picker}/search-results.ts (100%) rename apps/code-examples/src/app/code-examples/lookup/lookup/{async => multi-select}/demo.service.ts (100%) create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/multi-select/search-results.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/demo.service.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/result-templates/search-results.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/single-select/demo.service.ts create mode 100644 apps/code-examples/src/app/code-examples/lookup/lookup/single-select/search-results.ts 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)" />
      - - - - -
      - 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 fc8c35b894..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 1fd8d8d5b5..0000000000 --- a/apps/code-examples/src/app/code-examples/lookup/lookup/async/demo.component.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, OnInit, inject } from '@angular/core'; -import { - AbstractControl, - 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: AbstractControl): 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,6 +140,19 @@ 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(); @@ -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 62129758b7..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: Person): 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 => { @@ -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 299d95a56e..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,6 +115,19 @@ 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(); @@ -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 e7c0066051..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: Person): 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 f7796240cd..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: Person): 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/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 59a9f93572..667a3d967f 100644 --- a/apps/code-examples/src/app/home/home.component.html +++ b/apps/code-examples/src/app/home/home.component.html @@ -490,9 +490,8 @@ Lookup
      • - Async with add button + Add button
      • -
      • Async
      • Custom picker
      • From 0c09550d1727eef7025417497ecf9981be749f3a Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 23 Aug 2024 10:55:15 -0400 Subject: [PATCH 81/95] chore: release 10.41.4 (#2619) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89dc4f25d0..7247b99a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 94c6b9cd44..9c0a08c132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.41.3", + "version": "10.41.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.41.3", + "version": "10.41.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2a20e936bd..3af3a65b25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.41.3", + "version": "10.41.4", "license": "MIT", "scripts": { "ng": "nx", From 3bf4ea21d613d30404ef7a450b7069bf697b1eff Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:55:43 -0400 Subject: [PATCH 82/95] chore: rotate nx token (#2634) --- .github/workflows/ci.yml | 1 + .github/workflows/e2e.yml | 1 + .gitignore | 1 + apps/e2e/angular-tree-component-storybook/project.json | 2 +- apps/e2e/text-editor-storybook/project.json | 2 +- nx.json | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afa70d621c..06a24a3f8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: - 10.x.x env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} NX_CLOUD_DISTRIBUTED_EXECUTION: true jobs: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2f68aab3e9..5240b3d6c8 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,6 +12,7 @@ on: 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' 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/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/text-editor-storybook/project.json b/apps/e2e/text-editor-storybook/project.json index 87e9c193c5..cbcc2fde7a 100644 --- a/apps/e2e/text-editor-storybook/project.json +++ b/apps/e2e/text-editor-storybook/project.json @@ -33,7 +33,7 @@ { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "15kb" + "maximumError": "150kb" } ], "outputHashing": "all" diff --git a/nx.json b/nx.json index dbbb749490..c3c5e88858 100644 --- a/nx.json +++ b/nx.json @@ -122,6 +122,6 @@ "!{projectRoot}/tsconfig.storybook.json" ] }, - "nxCloudAccessToken": "NzE5ZWYwYzUtMGU0OC00OTU3LTk4ZDYtOTc1Zjk3MTExMzY5fHJlYWQtd3JpdGU=", + "nxCloudAccessToken": "Y2EyNjQ2OTctN2ZkMS00NTA2LWIxZDEtZTQyZDc3MjIwNDQyfHJlYWQ=", "defaultBase": "10.x.x" } From 110bfdfeaa2c79417b9970b7fbee2c821febe660 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Mon, 26 Aug 2024 16:07:25 -0400 Subject: [PATCH 83/95] fix(components/lists): selectable repeater items do not log a checkbox `label` deprecation warning (#2641) --- .../repeater/repeater-item.component.html | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/libs/components/lists/src/lib/modules/repeater/repeater-item.component.html b/libs/components/lists/src/lib/modules/repeater/repeater-item.component.html index eac4a30ba1..94f71a1e4d 100644 --- a/libs/components/lists/src/lib/modules/repeater/repeater-item.component.html +++ b/libs/components/lists/src/lib/modules/repeater/repeater-item.component.html @@ -1,4 +1,5 @@
        @@ -24,21 +24,21 @@ [template]="inlineFormTemplate" (close)="onInlineFormClose($event)" > - - + + - - + +
        {{ reorderButtonLabel }} @@ -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)" - > - + />
        From 087aad1bba3dd7e5b3ed2c883d0bffb8e7c98a4b Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 30 Aug 2024 10:50:24 -0400 Subject: [PATCH 84/95] chore: release 10.41.5 (#2642) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7247b99a3a..f66b918689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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) diff --git a/package-lock.json b/package-lock.json index 9c0a08c132..ba54dec141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.41.4", + "version": "10.41.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.41.4", + "version": "10.41.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3af3a65b25..4fa9e40786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.41.4", + "version": "10.41.5", "license": "MIT", "scripts": { "ng": "nx", From a245e9a71a7512ec9503a273c7dce1534c2dcdee Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:37:48 -0400 Subject: [PATCH 85/95] fix(components/forms): stop requiring labelText in field group (#2674) --- karma.conf.js | 7 ++++- .../scroll-shadow.directive.spec.ts | 6 ++-- .../date-range-picker.component.spec.ts | 23 --------------- .../date-range-picker.component.ts | 7 ----- .../checkbox/checkbox.component.spec.ts | 22 -------------- .../modules/checkbox/checkbox.component.ts | 9 +----- .../field-group/field-group.component.ts | 3 -- .../file-attachment.component.spec.ts | 29 ------------------- .../file-attachment.component.ts | 7 ----- .../file-drop.component.spec.ts | 22 -------------- .../file-attachment/file-drop.component.ts | 7 ----- .../input-box/input-box.component.spec.ts | 28 ------------------ .../modules/input-box/input-box.component.ts | 7 ----- .../radio/radio-group.component.spec.ts | 24 --------------- .../modules/radio/radio-group.component.ts | 7 ----- .../lib/modules/radio/radio.component.spec.ts | 21 -------------- .../src/lib/modules/radio/radio.component.ts | 8 ----- .../toggle-switch.component.spec.ts | 21 -------------- .../toggle-switch/toggle-switch.component.ts | 8 ----- .../lib/modules/lookup/lookup.component.ts | 5 ++++ .../text-editor/text-editor.component.spec.ts | 18 ------------ .../text-editor/text-editor.component.ts | 5 ---- 22 files changed, 15 insertions(+), 279 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 834a65091a..cb3846c64e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -39,7 +39,12 @@ module.exports = () => { customLaunchers: { ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', - flags: ['--headless', '--disable-gpu', '--disable-dev-shm-usage'], + flags: [ + '--headless', + '--disable-gpu', + '--disable-dev-shm-usage', + '--window-size=1920,1080', + ], }, }, restartOnFileChange: true, diff --git a/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts index bd68d2e24e..320f862b81 100644 --- a/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts @@ -91,7 +91,7 @@ describe('Scroll shadow directive', () => { }); it('should not show a shadow when the body is scrollable when disabled', async () => { - cmp.height = 800; + cmp.height = 850; cmp.enabled = false; fixture.detectChanges(); await waitForMutationObserver(); @@ -119,7 +119,7 @@ describe('Scroll shadow directive', () => { return; } - cmp.height = 800; + cmp.height = 850; fixture.detectChanges(); await waitForMutationObserver(); fixture.detectChanges(); @@ -162,7 +162,7 @@ describe('Scroll shadow directive', () => { return; } - cmp.height = 800; + cmp.height = 850; fixture.detectChanges(); await waitForMutationObserver(); fixture.detectChanges(); diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts index 5b8e2f18b3..fc838cbee5 100644 --- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts +++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.spec.ts @@ -10,7 +10,6 @@ import { SkyHelpTestingController, SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '@skyux/forms'; import { DateRangePickerTestComponent } from './fixtures/date-range-picker.component.fixture'; import { SkyDateRangeCalculation } from './types/date-range-calculation'; @@ -243,28 +242,6 @@ describe('Date range picker', function () { expect(dateRangePicker).not.toHaveClass('sky-margin-stacked-lg'); })); - it('should not render if a parent component requires label text and label and labelText input is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [DateRangePickerTestComponent], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent(DateRangePickerTestComponent); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - const dateRangePicker = fixture.nativeElement.querySelector( - 'sky-date-range-picker', - ); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(dateRangePicker).not.toBeVisible(); - }); - it('should only show end date picker for Before type', fakeAsync(function () { verifyVisiblePickers( SkyDateRangeCalculatorId.Before, diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts index 92c51460c7..0e2b59a7cb 100644 --- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts +++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts @@ -32,7 +32,6 @@ import { SkyLogService } from '@skyux/core'; import { SKY_FORM_ERRORS_ENABLED, SkyFormErrorsModule, - SkyFormFieldLabelTextRequiredDirective, SkyInputBoxModule, } from '@skyux/forms'; @@ -109,12 +108,6 @@ function isPartialValue( SkyInputBoxModule, SkyFormErrorsModule, ], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText: label || labelText'], - }, - ], providers: [ { provide: NG_VALIDATORS, diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts index b60b544df9..28f473c739 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts @@ -27,8 +27,6 @@ import { import { sampleTime } from 'rxjs/operators'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyCheckboxChange } from './checkbox-change'; import { SkyCheckboxComponent } from './checkbox.component'; import { SkyCheckboxModule } from './checkbox.module'; @@ -495,26 +493,6 @@ describe('Checkbox component', () => { expect(label?.textContent?.trim()).toBe(labelText); }); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - declarations: [SingleCheckboxComponent], - imports: [FormsModule, ReactiveFormsModule, SkyCheckboxModule], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent(SingleCheckboxComponent); - const checkbox = fixture.nativeElement.querySelector('sky-checkbox'); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(checkbox).not.toBeVisible(); - }); - it('should render help inline popover only if label text is provided', () => { testComponent.helpPopoverContent = 'popover content'; fixture.detectChanges(); diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts index 2e8b0356c6..d239d06cf5 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts @@ -26,7 +26,6 @@ import { SkyThemeComponentClassDirective } from '@skyux/theme'; import { BehaviorSubject, Observable } from 'rxjs'; import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; import { SkyCheckboxChange } from './checkbox-change'; @@ -41,13 +40,7 @@ import { SkyCheckboxChange } from './checkbox-change'; './checkbox.default.component.scss', './checkbox.modern.component.scss', ], - hostDirectives: [ - SkyThemeComponentClassDirective, - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], + hostDirectives: [SkyThemeComponentClassDirective], providers: [ { provide: NG_VALIDATORS, useExisting: SkyCheckboxComponent, multi: true }, { diff --git a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts index 724f3fc7cb..e61d7ee3cc 100644 --- a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts +++ b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts @@ -10,8 +10,6 @@ import { import { SkyIdModule } from '@skyux/core'; import { SkyHelpInlineModule } from '@skyux/help-inline'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyFieldGroupHeadingLevel } from './field-group-heading-level'; import { SkyFieldGroupHeadingStyle } from './field-group-heading-style'; @@ -28,7 +26,6 @@ function numberAttribute3(value: unknown): number { styleUrl: './field-group.component.scss', standalone: true, imports: [CommonModule, SkyHelpInlineModule, SkyIdModule], - providers: [SkyFormFieldLabelTextRequiredService], }) export class SkyFieldGroupComponent { /** diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts index dd6d9e78cf..d7759e82a1 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.spec.ts @@ -23,8 +23,6 @@ import { import { BehaviorSubject } from 'rxjs'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyFileAttachmentComponent } from './file-attachment.component'; import { SkyFileItem } from './file-item'; import { FileAttachmentTestComponent } from './fixtures/file-attachment.component.fixture'; @@ -1518,33 +1516,6 @@ describe('File attachment', () => { validateLabelText('label element'); }); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [FileAttachmentTestModule, SkyThemeModule], - providers: [ - { - provide: SkyThemeService, - useValue: mockThemeSvc, - }, - SkyFormFieldLabelTextRequiredService, - ], - }); - - const fixture = TestBed.createComponent(FileAttachmentTestComponent); - const fileAttachment = fixture.nativeElement.querySelector( - 'sky-file-attachment', - ); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(fileAttachment).not.toBeVisible(); - }); - it('should mark as dirty when an invalid file is uploaded first', () => { const files = [ { diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts index 3338a67a27..2ca3c2a536 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment.component.ts @@ -33,7 +33,6 @@ import { Subject } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; import { SkyFileAttachmentLabelComponent } from './file-attachment-label.component'; import { SkyFileAttachmentService } from './file-attachment.service'; @@ -56,12 +55,6 @@ const MIN_FILE_SIZE_DEFAULT = 0; selector: 'sky-file-attachment', templateUrl: './file-attachment.component.html', styleUrls: ['./file-attachment.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], providers: [ SkyFileAttachmentService, { provide: SKY_FORM_ERRORS_ENABLED, useValue: true }, diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts index b6b2796b1f..a70843eba9 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.spec.ts @@ -9,8 +9,6 @@ import { SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyFileAttachmentsModule } from './file-attachments.module'; import { SkyFileDropComponent } from './file-drop.component'; import { SkyFileItem } from './file-item'; @@ -408,26 +406,6 @@ describe('File drop component', () => { expect(labelEl).toHaveCssClass('sky-screen-reader-only'); }); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - - TestBed.configureTestingModule({ - imports: [SkyFileAttachmentsModule], - declarations: [FileDropContentComponent], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent(SkyFileDropComponent); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(fixture.nativeElement).not.toBeVisible(); - }); - it('should render the hintText when provided', () => { const hintText = 'Hint text'; componentInstance.hintText = hintText; diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts index 0b27be5bcd..e779f17934 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop.component.ts @@ -18,7 +18,6 @@ import { SkyLibResourcesService } from '@skyux/i18n'; import { take } from 'rxjs/operators'; import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; import { SkyFileAttachmentService } from './file-attachment.service'; import { SkyFileItem } from './file-item'; @@ -43,12 +42,6 @@ const MIN_FILE_SIZE_DEFAULT = 0; selector: 'sky-file-drop', templateUrl: './file-drop.component.html', styleUrls: ['./file-drop.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], providers: [ SkyFileAttachmentService, { provide: SKY_FORM_ERRORS_ENABLED, useValue: true }, diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts index b3212881f9..9cc38c51ed 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts @@ -23,8 +23,6 @@ import { import { BehaviorSubject } from 'rxjs'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { InputBoxFixtureComponent } from './fixtures/input-box.component.fixture'; import { InputBoxFixturesModule } from './fixtures/input-box.module.fixture'; import { SkyInputBoxAdapterService } from './input-box-adapter.service'; @@ -669,32 +667,6 @@ describe('Input box component', () => { validateLabelAccessibilityLabel(els, 'Easy mode 0 characters out of 10'); }); - it('should not display if a parent component requires label text and it is not provided', () => { - TestBed.configureTestingModule({ - imports: [InputBoxFixturesModule], - providers: [ - { - provide: SkyThemeService, - useValue: mockThemeSvc, - }, - SkyFormFieldLabelTextRequiredService, - ], - }); - - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - const fixture = TestBed.createComponent(InputBoxFixtureComponent); - const els = getDefaultEls(fixture, 'input-basic'); - - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(els.inputBoxEl).toExist(); - expect(els.inputEl).toBeNull(); - }); - it('should add stacked CSS class', () => { const fixture = TestBed.createComponent(InputBoxFixtureComponent); fixture.detectChanges(); diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts index e884b4075e..5bc9aee654 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts @@ -34,7 +34,6 @@ import { SkyContentInfoProvider, SkyIdService } from '@skyux/core'; import { ReplaySubject } from 'rxjs'; import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; import { SkyInputBoxAdapterService } from './input-box-adapter.service'; import { SkyInputBoxControlDirective } from './input-box-control.directive'; @@ -48,12 +47,6 @@ import { SkyInputBoxPopulateArgs } from './input-box-populate-args'; selector: 'sky-input-box', templateUrl: './input-box.component.html', styleUrls: ['./input-box.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], providers: [ SkyContentInfoProvider, SkyInputBoxAdapterService, diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts b/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts index 13f9a34afe..eb0450ec36 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.spec.ts @@ -12,8 +12,6 @@ import { SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyRadioFixturesModule } from './fixtures/radio-fixtures.module'; import { SkyRadioGroupBooleanTestComponent } from './fixtures/radio-group-boolean.component.fixture'; import { SkyRadioGroupReactiveFixtureComponent } from './fixtures/radio-group-reactive.component.fixture'; @@ -698,28 +696,6 @@ describe('Radio group component (reactive)', function () { expect(hintEl?.textContent.trim()).toBe(hintText); }); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [SkyRadioFixturesModule], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent( - SkyRadioGroupReactiveFixtureComponent, - ); - const headingTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const validationSpy = spyOn(headingTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - const radioGroup = fixture.nativeElement.querySelector('sky-radio-group'); - - expect(validationSpy).toHaveBeenCalled(); - expect(radioGroup).not.toBeVisible(); - }); - it('should have the lg margin class if stacked is true and headingLevel is unset', () => { fixture.componentInstance.stacked = true; fixture.componentInstance.headingLevel = undefined; diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts index 5d31e14bc0..fe4516a9b1 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts @@ -22,7 +22,6 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; import { SkyRadioGroupIdService } from './radio-group-id.service'; import { SkyRadioComponent } from './radio.component'; @@ -46,12 +45,6 @@ function numberAttribute4(value: unknown): number { selector: 'sky-radio-group', templateUrl: './radio-group.component.html', styleUrls: ['./radio-group.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText: headingText'], - }, - ], providers: [ SkyRadioGroupIdService, { provide: SKY_FORM_ERRORS_ENABLED, useValue: true }, diff --git a/libs/components/forms/src/lib/modules/radio/radio.component.spec.ts b/libs/components/forms/src/lib/modules/radio/radio.component.spec.ts index 0fc3976175..07c89f18d5 100644 --- a/libs/components/forms/src/lib/modules/radio/radio.component.spec.ts +++ b/libs/components/forms/src/lib/modules/radio/radio.component.spec.ts @@ -14,8 +14,6 @@ import { SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyRadioFixturesModule } from './fixtures/radio-fixtures.module'; import { SkyRadioOnPushTestComponent } from './fixtures/radio-on-push.component.fixture'; import { SkySingleRadioComponent } from './fixtures/radio-single.component.fixture'; @@ -254,25 +252,6 @@ describe('Radio component', function () { expect(radioLabels.item(2).textContent?.trim()).toBe(label3); })); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [SkyRadioFixturesModule], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent(SkyRadioTestComponent); - const radio = fixture.nativeElement.querySelector('sky-radio'); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(radio).not.toBeVisible(); - }); - it('should use labelText as an accessible label over label and labelledBy', fakeAsync(function () { const label1 = 'Label 1'; const label2 = 'Label 2'; diff --git a/libs/components/forms/src/lib/modules/radio/radio.component.ts b/libs/components/forms/src/lib/modules/radio/radio.component.ts index 19452a6604..2f78967d18 100644 --- a/libs/components/forms/src/lib/modules/radio/radio.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio.component.ts @@ -17,8 +17,6 @@ import { SkyFormsUtility, SkyIdService, SkyLogService } from '@skyux/core'; import { Subject } from 'rxjs'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; - import { SkyRadioGroupIdService } from './radio-group-id.service'; import { SkyRadioChange } from './types/radio-change'; import { SkyRadioType } from './types/radio-type'; @@ -42,12 +40,6 @@ const SKY_RADIO_CONTROL_VALUE_ACCESSOR: Provider = { selector: 'sky-radio', templateUrl: './radio.component.html', styleUrls: ['./radio.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], providers: [SKY_RADIO_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.spec.ts b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.spec.ts index 52215053fc..548d9b5fbf 100644 --- a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.spec.ts +++ b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.spec.ts @@ -14,8 +14,6 @@ import { SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '../shared/form-field-label-text-required.service'; - import { SkyToggleSwitchChangeEventFixtureComponent } from './fixtures/toggle-switch-change-event.component.fixture'; import { SkyToggleSwitchFormDirectivesFixtureComponent } from './fixtures/toggle-switch-form-directives.component.fixture'; import { SkyToggleSwitchOnPushFixtureComponent } from './fixtures/toggle-switch-on-push.component.fixture'; @@ -270,25 +268,6 @@ describe('Toggle switch component', () => { expect(label?.textContent).toBe('label element'); }); - it('should not render if a parent component requires label text and it is not provided', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [SkyToggleSwitchFixturesModule], - providers: [SkyFormFieldLabelTextRequiredService], - }); - - const fixture = TestBed.createComponent(SkyToggleSwitchFixtureComponent); - const switchEl = fixture.nativeElement.querySelector('sky-toggle-switch'); - const labelTextRequiredSvc = TestBed.inject( - SkyFormFieldLabelTextRequiredService, - ); - const labelTextSpy = spyOn(labelTextRequiredSvc, 'validateLabelText'); - fixture.detectChanges(); - - expect(labelTextSpy).toHaveBeenCalled(); - expect(switchEl).not.toBeVisible(); - }); - it('should pass accessibility with label element and no `ariaLabel`', async () => { fixture.detectChanges(); expect(buttonElement?.getAttribute('aria-labelledby')).toEqual( diff --git a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts index 5c9bc714ef..a9f7fc83a0 100644 --- a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts +++ b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts @@ -27,8 +27,6 @@ import { SkyIdService, SkyLogService } from '@skyux/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { SkyFormFieldLabelTextRequiredDirective } from '../shared/form-field-label-text-required.directive'; - import { SkyToggleSwitchLabelComponent } from './toggle-switch-label.component'; import { SkyToggleSwitchChange } from './types/toggle-switch-change'; @@ -47,12 +45,6 @@ const SKY_TOGGLE_SWITCH_VALIDATOR = { selector: 'sky-toggle-switch', templateUrl: './toggle-switch.component.html', styleUrls: ['./toggle-switch.component.scss'], - hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, - ], providers: [ SKY_TOGGLE_SWITCH_CONTROL_VALUE_ACCESSOR, SKY_TOGGLE_SWITCH_VALIDATOR, 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 5bbd173c42..c2c73dcb17 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts @@ -343,6 +343,11 @@ export class SkyLookupComponent this.controlId = this.inputBoxHostSvc.controlId; this.ariaDescribedBy = this.inputBoxHostSvc.ariaDescribedBy; + console.log('populating with'); + console.log(this.inputTemplateRef); + console.log(this.inputTemplateRef.elementRef); + console.log(this.inputTemplateRef.elementRef.nativeElement); + this.inputBoxHostSvc.populate({ inputTemplate: this.inputTemplateRef, buttonsTemplate: this.enableShowMore diff --git a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.spec.ts b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.spec.ts index 37119b0d3e..84af774a3c 100644 --- a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.spec.ts +++ b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.spec.ts @@ -22,7 +22,6 @@ import { SkyHelpTestingController, SkyHelpTestingModule, } from '@skyux/core/testing'; -import { SkyFormFieldLabelTextRequiredService } from '@skyux/forms'; import { SkyTextEditorResourcesModule } from '../shared/sky-text-editor-resources.module'; @@ -1858,23 +1857,6 @@ describe('Text editor', () => { }); }); - 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; 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 92e6c973d1..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 @@ -25,7 +25,6 @@ import { import { SKY_FORM_ERRORS_ENABLED, SkyFormErrorsModule, - SkyFormFieldLabelTextRequiredDirective, SkyInputBoxHostService, SkyRequiredStateDirective, } from '@skyux/forms'; @@ -73,10 +72,6 @@ import { SkyTextEditorToolbarActionType } from './types/toolbar-action-type'; { provide: SKY_FORM_ERRORS_ENABLED, useValue: true }, ], hostDirectives: [ - { - directive: SkyFormFieldLabelTextRequiredDirective, - inputs: ['labelText'], - }, { directive: SkyRequiredStateDirective, inputs: ['required'], From a8a66f9a352ecc4864f65a4000d360e627b2af9c Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:58:12 -0400 Subject: [PATCH 86/95] chore: clean up lookup (#2679) --- .../lookup/src/lib/modules/lookup/lookup.component.ts | 5 ----- 1 file changed, 5 deletions(-) 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 c2c73dcb17..5bbd173c42 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts @@ -343,11 +343,6 @@ export class SkyLookupComponent this.controlId = this.inputBoxHostSvc.controlId; this.ariaDescribedBy = this.inputBoxHostSvc.ariaDescribedBy; - console.log('populating with'); - console.log(this.inputTemplateRef); - console.log(this.inputTemplateRef.elementRef); - console.log(this.inputTemplateRef.elementRef.nativeElement); - this.inputBoxHostSvc.populate({ inputTemplate: this.inputTemplateRef, buttonsTemplate: this.enableShowMore From 97b6602f1d8b64f74d9abd664a7e02bf98616118 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:57:50 -0400 Subject: [PATCH 87/95] ci: use nx agents in 10.x.x (#2682) --- .github/workflows/ci.yml | 172 +++++++++++++----------------------- .github/workflows/e2e.yml | 14 +-- package-lock.json | 178 +++++++++++++++++++------------------- package.json | 8 +- 4 files changed, 163 insertions(+), 209 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06a24a3f8b..4a4a9527a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ on: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - NX_CLOUD_DISTRIBUTED_EXECUTION: true jobs: install-deps: @@ -40,53 +39,82 @@ 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 + 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 - strategy: - matrix: - agent: [1, 2, 3, 4, 5, 6, 7, 8] - steps: - - uses: actions/checkout@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: Start Nx Agent ${{ matrix.agent }} - run: npx nx-cloud start-agent - - lint: - name: Lint - 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 + - run: npx nx-cloud start-ci-run --distribute-on="8 linux-medium-plus-js" - 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 + 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 --exclude '*,!tag:npm' + - name: Run lint, test, and posttest for non-npm packages + run: | + npx nx affected --target lint test posttest --configuration ci --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 + + stop-agents: + name: Stop Nx Cloud agents + needs: build-coverage-lint + runs-on: ubuntu-latest + if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} + steps: + - name: Stop agents + run: | + [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Agents stopped. || false check-workspace: name: Check dependencies and resources + if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} needs: install-deps runs-on: ubuntu-latest steps: @@ -101,7 +129,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') }} @@ -130,40 +158,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 @@ -171,17 +172,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,48 +194,3 @@ jobs: #cor-skyux-notifications 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] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - 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: Stop Nx Cloud agents - run: npx nx-cloud stop-all-agents diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5240b3d6c8..85648ee47d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -94,6 +94,7 @@ jobs: strategy: # If one build fails, do not cancel other builds. fail-fast: false + max-parallel: 8 matrix: project: ${{ fromJSON(needs.install-deps.outputs.parameters).projects }} steps: @@ -120,7 +121,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 @@ -176,7 +177,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 @@ -249,7 +252,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: @@ -273,9 +276,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: 8 matrix: include: ${{ fromJSON(needs.install-deps.outputs.parameters).e2eTargets }} steps: @@ -413,7 +417,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/package-lock.json b/package-lock.json index ba54dec141..ba25387096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,10 +68,10 @@ "@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", @@ -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", @@ -7883,20 +7883,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 +7906,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 +7948,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 +7975,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 +7988,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 +8002,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 +8016,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 +8031,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 +8071,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" @@ -23106,9 +23106,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 +24333,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" diff --git a/package.json b/package.json index 4fa9e40786..067480f7a1 100644 --- a/package.json +++ b/package.json @@ -146,10 +146,10 @@ "@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", @@ -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", From d3a772679484b279b0d4bbe3934014a730ac08b5 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:42:15 -0400 Subject: [PATCH 88/95] ci: additional `--no-agents` flag (#2683) --- .github/workflows/e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 85648ee47d..a8a0cea148 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -327,7 +327,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" @@ -335,7 +335,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 From a6bf942b4e5dd5dfe493597f2298d4bf0270451f Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:37:44 -0400 Subject: [PATCH 89/95] ci: use fewer connections (#2692) * ci: use fewer connections * One task at a time per agent --- .github/workflows/ci.yml | 51 +++++++++++++++++++++++++++++---------- .github/workflows/e2e.yml | 4 +-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a4a9527a8..cd3db29392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,23 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: npm ci + agents: + name: Bootup Nx Cloud agent + runs-on: ubuntu-latest + needs: install-deps + strategy: + matrix: + agent: [1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@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: Start Nx Agent ${{ matrix.agent }} + run: npx nx-cloud start-agent + build-coverage-lint: name: Build, code coverage unit tests, and linting if: ${{ !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} @@ -49,10 +66,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - run: npx nx-cloud start-ci-run --distribute-on="8 linux-medium-plus-js" - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' + - run: npx nx-cloud start-ci-run --agent-count=5 --distribute-on="manual" - name: Retrieve node_modules cache uses: actions/cache/restore@v4 with: @@ -64,10 +81,10 @@ jobs: 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 --exclude '*,!tag:npm' + 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 --exclude 'tag:npm' + 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 @@ -102,16 +119,6 @@ jobs: run: | [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Linted. || false - stop-agents: - name: Stop Nx Cloud agents - needs: build-coverage-lint - runs-on: ubuntu-latest - if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} - steps: - - name: Stop agents - run: | - [ '${{ needs.build-coverage-lint.result }}' == 'success' ] && echo Agents stopped. || false - check-workspace: name: Check dependencies and resources if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} @@ -194,3 +201,21 @@ jobs: #cor-skyux-notifications SLACK_CHANNEL: C01GY7ZP4HM SLACK_FOOTER: 'Blackbaud Sky Build User' + + stop-agents: + name: Stop Nx Cloud agents + runs-on: ubuntu-latest + if: ${{ !cancelled() }} + 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/restore@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ needs.install-deps.outputs.node-version }}-modules-${{ hashFiles('package-lock.json') }} + - name: Stop Nx Cloud agents + run: npx nx-cloud stop-all-agents diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a8a0cea148..48a45494a7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -94,7 +94,7 @@ jobs: strategy: # If one build fails, do not cancel other builds. fail-fast: false - max-parallel: 8 + max-parallel: 5 matrix: project: ${{ fromJSON(needs.install-deps.outputs.parameters).projects }} steps: @@ -279,7 +279,7 @@ jobs: needs: [install-deps, build-storybook] strategy: fail-fast: false - max-parallel: 8 + max-parallel: 5 matrix: include: ${{ fromJSON(needs.install-deps.outputs.parameters).e2eTargets }} steps: From 71a815cda976fd83cacabfa1173593ca2ebfbb99 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:27:51 -0400 Subject: [PATCH 90/95] ci: use fewer connections (#2692) (#2695) (#2697) * ci: use fewer connections * One task at a time per agent --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/e2e.yml | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd3db29392..0b11d717fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: branches: - 10.x.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} @@ -46,7 +50,7 @@ jobs: needs: install-deps strategy: matrix: - agent: [1, 2, 3, 4, 5] + agent: [1, 2, 3] steps: - uses: actions/checkout@v4 - name: Retrieve node_modules cache @@ -69,7 +73,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - - run: npx nx-cloud start-ci-run --agent-count=5 --distribute-on="manual" + - run: npx nx-cloud start-ci-run --agent-count=3 --distribute-on="manual" - name: Retrieve node_modules cache uses: actions/cache/restore@v4 with: @@ -121,7 +125,7 @@ jobs: check-workspace: name: Check dependencies and resources - if: ${{ always() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} + if: ${{ !cancelled() && !startsWith( github.head_ref || github.ref_name, 'release-please--' ) }} needs: install-deps runs-on: ubuntu-latest steps: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 48a45494a7..303a90a43a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,6 +9,10 @@ on: - 10.x.x workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + env: CYPRESS_VERIFY_TIMEOUT: 120000 GH_PAGES_OWNER: blackbaud @@ -94,7 +98,7 @@ jobs: strategy: # If one build fails, do not cancel other builds. fail-fast: false - max-parallel: 5 + max-parallel: 3 matrix: project: ${{ fromJSON(needs.install-deps.outputs.parameters).projects }} steps: @@ -279,7 +283,7 @@ jobs: needs: [install-deps, build-storybook] strategy: fail-fast: false - max-parallel: 5 + max-parallel: 3 matrix: include: ${{ fromJSON(needs.install-deps.outputs.parameters).e2eTargets }} steps: From d9ea5f83a7ddddc216b699ba20e41462eedfe550 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:22:41 -0400 Subject: [PATCH 91/95] ci: adjust e2e concurrency (#2698) --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 303a90a43a..532ed53969 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name || github.event.ref }} cancel-in-progress: false env: From 1997de3beb4b586a66dc78fc47f95c0fea74a885 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Thu, 5 Sep 2024 19:11:19 -0400 Subject: [PATCH 92/95] feat(components/theme): add support for @skyux/icons@7.8.0 (#2678) --- libs/components/packages/package.json | 2 +- libs/components/theme/package.json | 2 +- libs/components/theme/src/lib/styles/sky.scss | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index 523d6166c0..344c2ab28c 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.5.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", diff --git a/libs/components/theme/package.json b/libs/components/theme/package.json index 488f3ac2a4..6fd3da5e80 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@blackbaud/skyux-design-tokens": "0.0.28", - "@skyux/icons": "7.5.0", + "@skyux/icons": "7.8.0", "fontfaceobserver": "2.3.0", "tslib": "^2.6.2" } diff --git a/libs/components/theme/src/lib/styles/sky.scss b/libs/components/theme/src/lib/styles/sky.scss index 5b346a0dcc..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.5.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/package-lock.json b/package-lock.json index ba25387096..ea006127a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.5.0", + "@skyux/icons": "7.8.0", "ag-grid-angular": "31.3.4", "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", @@ -9301,9 +9301,9 @@ "dev": true }, "node_modules/@skyux/icons": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@skyux/icons/-/icons-7.5.0.tgz", - "integrity": "sha512-IA1z6wXuWchms2B47aKsLxanx4XjHWjy1mdECVTY0574TCJSDHqjQHUl43/7U3rRR1mI1PA22IEWrxTt8KpdAQ==" + "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", diff --git a/package.json b/package.json index 067480f7a1..37ad1fd65b 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@blackbaud/angular-tree-component": "1.0.0", "@blackbaud/skyux-design-tokens": "0.0.28", "@nx/angular": "19.3.1", - "@skyux/icons": "7.5.0", + "@skyux/icons": "7.8.0", "ag-grid-angular": "31.3.4", "ag-grid-community": "31.3.4", "autonumeric": "4.10.5", From d5681a627f6da97c1fb1b2b314f3b4ce04dfd187 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 5 Sep 2024 20:32:40 -0400 Subject: [PATCH 93/95] fix(components/icon): variant input is respected for SVG-based icons (#2688) --- .../icon/fixtures/icon.component.fixture.html | 1 + .../icon/fixtures/icon.component.fixture.ts | 1 + .../src/lib/icon/icon-svg.component.spec.ts | 69 +++ .../icon/src/lib/icon/icon.component.html | 6 +- .../icon/src/lib/icon/icon.component.spec.ts | 566 +++++++++++------- 5 files changed, 425 insertions(+), 218 deletions(-) diff --git a/libs/components/icon/src/lib/icon/fixtures/icon.component.fixture.html b/libs/components/icon/src/lib/icon/fixtures/icon.component.fixture.html index 71d17acf58..7f9c3455c2 100644 --- a/libs/components/icon/src/lib/icon/fixtures/icon.component.fixture.html +++ b/libs/components/icon/src/lib/icon/fixtures/icon.component.fixture.html @@ -1,6 +1,7 @@ { validateIconId('#test-16-solid'); })); + it('should display the resolved icon by ID, size, and variant', fakeAsync(() => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconSize', '2x'); + fixture.componentRef.setInput('iconVariant', 'solid'); + detectUrlChanges(); + + validateIconId('#test-32-solid'); + })); + it("should use the host element's text color as its fill color", fakeAsync(() => { fixture.nativeElement.style.color = '#0f0'; @@ -94,4 +104,63 @@ describe('Icon SVG component', () => { validateIconId(''); })); + + describe('a11y', () => { + async function detectUrlChanges(): Promise { + fixture.detectChanges(); + + // Resolve icon ID Observable and apply changes. + await fixture.whenStable(); + fixture.detectChanges(); + } + + it('should be accessible (icon: "test", size: undefined, variant: undefined)', async () => { + fixture.componentRef.setInput('iconName', 'test'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: undefined)', async () => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconSize', '2x'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: undefined, variant: "solid")', async () => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconVariant', 'solid'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: undefined, variant: "line")', async () => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconVariant', 'line'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: "solid")', async () => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconSize', '2x'); + fixture.componentRef.setInput('iconVariant', 'solid'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: "line")', async () => { + fixture.componentRef.setInput('iconName', 'test'); + fixture.componentRef.setInput('iconSize', '2x'); + fixture.componentRef.setInput('iconVariant', 'line'); + await detectUrlChanges(); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + }); }); diff --git a/libs/components/icon/src/lib/icon/icon.component.html b/libs/components/icon/src/lib/icon/icon.component.html index 8b10843504..0c61a24897 100644 --- a/libs/components/icon/src/lib/icon/icon.component.html +++ b/libs/components/icon/src/lib/icon/icon.component.html @@ -1,5 +1,9 @@ @if (iconName) { - + } @else { { - function setupIcon( - icon: string, - iconType?: SkyIconType, - size?: string, - fixedWidth?: boolean, - variant?: SkyIconVariantType, - ): void { - cmp.icon = icon; - cmp.iconType = iconType; - cmp.size = size; - cmp.fixedWidth = fixedWidth; - cmp.variant = variant; - - fixture.detectChanges(); - } - let fixture: ComponentFixture; let cmp: IconTestComponent; let element: HTMLElement; - let mockResolver: jasmine.SpyObj; - beforeEach(() => { - mockResolver = jasmine.createSpyObj('mockResolver', ['resolveIcon']); + describe('icon', () => { + let mockResolver: jasmine.SpyObj; + function setupIcon( + icon: string, + iconType?: SkyIconType, + size?: string, + fixedWidth?: boolean, + variant?: SkyIconVariantType, + ): void { + cmp.icon = icon; + cmp.iconType = iconType; + cmp.size = size; + cmp.fixedWidth = fixedWidth; + cmp.variant = variant; + + fixture.detectChanges(); + } + + beforeEach(() => { + mockResolver = jasmine.createSpyObj('mockResolver', ['resolveIcon']); + + mockResolver.resolveIcon.and.callFake((icon, variant) => { + let iconType = 'fa'; + + if (icon === 'variant-test') { + icon = 'variant-test-' + variant; + iconType = 'skyux'; + } - mockResolver.resolveIcon.and.callFake((icon, variant) => { - let iconType = 'fa'; + return { + icon, + iconType, + }; + }); - if (icon === 'variant-test') { - icon = 'variant-test-' + variant; - iconType = 'skyux'; - } + TestBed.configureTestingModule({ + imports: [SkyIconFixturesModule], + providers: [ + { + provide: SkyIconResolverService, + useValue: mockResolver, + }, + ], + }); - return { - icon, - iconType, - }; + fixture = TestBed.createComponent(IconTestComponent); + cmp = fixture.componentInstance as IconTestComponent; + element = fixture.nativeElement as HTMLElement; }); - TestBed.configureTestingModule({ - imports: [SkyIconFixturesModule], - providers: [ - { - provide: SkyIconResolverService, - useValue: mockResolver, - }, - ], + it('should display an icon based on the given icon', async () => { + fixture.detectChanges(); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-circle'); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-3x'); + expect(element.querySelector('.sky-icon')).not.toHaveCssClass('fa-fw'); + expect( + element.querySelector('.sky-icon')?.getAttribute('aria-hidden'), + ).toBe('true'); + expect(element.querySelector('.sky-icon')?.classList.length).toBe(4); }); - fixture = TestBed.createComponent(IconTestComponent); - cmp = fixture.componentInstance as IconTestComponent; - element = fixture.nativeElement as HTMLElement; - }); + it('should display a different icon with a different size and a fixedWidth', () => { + setupIcon('broom', undefined, '5x', true); - it('should display an icon based on the given icon', async () => { - fixture.detectChanges(); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-circle'); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-3x'); - expect(element.querySelector('.sky-icon')).not.toHaveCssClass('fa-fw'); - expect( - element.querySelector('.sky-icon')?.getAttribute('aria-hidden'), - ).toBe('true'); - expect(element.querySelector('.sky-icon')?.classList.length).toBe(4); - }); + expect(cmp.icon).toBe('broom'); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-broom'); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-5x'); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-fw'); + expect(element.querySelector('.sky-icon')?.classList.length).toBe(5); + expect( + element.querySelector('.sky-icon')?.getAttribute('aria-hidden'), + ).toBe('true'); + }); - it('should display a different icon with a different size and a fixedWidth', () => { - setupIcon('broom', undefined, '5x', true); - - expect(cmp.icon).toBe('broom'); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-broom'); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-5x'); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-fw'); - expect(element.querySelector('.sky-icon')?.classList.length).toBe(5); - expect( - element.querySelector('.sky-icon')?.getAttribute('aria-hidden'), - ).toBe('true'); - }); + it('should show an icon without optional inputs', () => { + setupIcon('spinner', undefined, undefined, undefined, undefined); - it('should show an icon without optional inputs', () => { - setupIcon('spinner', undefined, undefined, undefined, undefined); + expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-spinner'); + expect(element.querySelector('.sky-icon')?.classList.length).toBe(3); + }); - expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-spinner'); - expect(element.querySelector('.sky-icon')?.classList.length).toBe(3); - }); + it('should display the specified variant', () => { + setupIcon('variant-test', 'skyux', undefined, undefined, 'solid'); - it('should display the specified variant', () => { - setupIcon('variant-test', 'skyux', undefined, undefined, 'solid'); + expect(element.querySelector('.sky-icon')).toHaveCssClass( + 'sky-i-variant-test-solid', + ); + }); - expect(element.querySelector('.sky-icon')).toHaveCssClass( - 'sky-i-variant-test-solid', - ); - }); + describe('a11y', () => { + it('should be accessible (icon: "close", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('close', undefined, undefined, undefined, undefined); - describe('a11y', () => { - it('should be accessible (icon: "close", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('close', undefined, undefined, undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('spinner', 'fa', undefined, undefined, undefined); - it('should be accessible (icon: "close", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('spinner', 'fa', undefined, undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('close', 'skyux', undefined, undefined, undefined); - it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('close', 'skyux', undefined, undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('close', undefined, '3x', undefined, undefined); - it('should be accessible (icon: "close", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('close', undefined, '3x', undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('close', 'fa', '3x', undefined, undefined); - it('should be accessible (icon: "close", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('close', 'fa', '3x', undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { + setupIcon('close', 'skyux', '3x', undefined, undefined); - it('should be accessible (icon: "close", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: undefined)', async () => { - setupIcon('close', 'skyux', '3x', undefined, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: undefined, size: undefined, fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', undefined, undefined, true, undefined); - it('should be accessible (icon: "close", iconType: undefined, size: undefined, fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', undefined, undefined, true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "fa", size: undefined, fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', 'fa', undefined, true, undefined); - it('should be accessible (icon: "close", iconType: "fa", size: undefined, fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', 'fa', undefined, true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', 'skyux', undefined, true, undefined); - it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', 'skyux', undefined, true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: undefined, size: "3x", fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', undefined, '3x', true, undefined); - it('should be accessible (icon: "close", iconType: undefined, size: "3x", fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', undefined, '3x', true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "fa", size: "3x", fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', 'fa', '3x', true, undefined); - it('should be accessible (icon: "close", iconType: "fa", size: "3x", fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', 'fa', '3x', true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: "3x", fixedWidth: true, variant: undefined)', async () => { + setupIcon('close', 'skyux', '3x', true, undefined); - it('should be accessible (icon: "close", iconType: "skyux", size: "3x", fixedWidth: true, variant: undefined)', async () => { - setupIcon('close', 'skyux', '3x', true, undefined); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('info-circle', undefined, undefined, undefined, 'solid'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('info-circle', undefined, undefined, undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('info-circle', 'fa', undefined, undefined, 'solid'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('info-circle', 'fa', undefined, undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "notification", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('notification', 'skyux', undefined, undefined, 'solid'); - it('should be accessible (icon: "notification", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('notification', 'skyux', undefined, undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('info-circle', undefined, '3x', undefined, 'solid'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('info-circle', undefined, '3x', undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('info-circle', 'fa', '3x', undefined, 'solid'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('info-circle', 'fa', '3x', undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { + setupIcon('notification', 'skyux', '3x', undefined, 'solid'); - it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: "solid")', async () => { - setupIcon('notification', 'skyux', '3x', undefined, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: true, variant: "solid")', async () => { + setupIcon('info-circle', undefined, undefined, true, 'solid'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: true, variant: "solid")', async () => { - setupIcon('info-circle', undefined, undefined, true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: true, variant: "solid")', async () => { + setupIcon('info-circle', 'fa', undefined, true, 'solid'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: true, variant: "solid")', async () => { - setupIcon('info-circle', 'fa', undefined, true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: "solid")', async () => { + setupIcon('notification', 'skyux', undefined, true, 'solid'); - it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: "solid")', async () => { - setupIcon('notification', 'skyux', undefined, true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: true, variant: "solid")', async () => { + setupIcon('info-circle', undefined, '3x', true, 'solid'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: true, variant: "solid")', async () => { - setupIcon('info-circle', undefined, '3x', true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: true, variant: "solid")', async () => { + setupIcon('info-circle', 'fa', '3x', true, 'solid'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: true, variant: "solid")', async () => { - setupIcon('info-circle', 'fa', '3x', true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: true, variant: "solid")', async () => { + setupIcon('notification', 'skyux', '3x', true, 'solid'); - it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: true, variant: "solid")', async () => { - setupIcon('notification', 'skyux', '3x', true, 'solid'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('info-circle', undefined, undefined, undefined, 'line'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('info-circle', undefined, undefined, undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('info-circle', 'fa', undefined, undefined, 'line'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('info-circle', 'fa', undefined, undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "notification", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('notification', 'skyux', undefined, undefined, 'line'); - it('should be accessible (icon: "notification", iconType: "skyux", size: undefined, fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('notification', 'skyux', undefined, undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('info-circle', undefined, '3x', undefined, 'line'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('info-circle', undefined, '3x', undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('info-circle', 'fa', '3x', undefined, 'line'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('info-circle', 'fa', '3x', undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { + setupIcon('notification', 'skyux', '3x', undefined, 'line'); - it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: undefined/false, variant: "line")', async () => { - setupIcon('notification', 'skyux', '3x', undefined, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: true, variant: "line")', async () => { + setupIcon('info-circle', undefined, undefined, true, 'line'); - it('should be accessible (icon: "info-circle", iconType: undefined, size: undefined, fixedWidth: true, variant: "line")', async () => { - setupIcon('info-circle', undefined, undefined, true, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: true, variant: "line")', async () => { + setupIcon('info-circle', 'fa', undefined, true, 'line'); - it('should be accessible (icon: "info-circle", iconType: "fa", size: undefined, fixedWidth: true, variant: "line")', async () => { - setupIcon('info-circle', 'fa', undefined, true, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: "line")', async () => { + setupIcon('notification', 'skyux', undefined, true, 'line'); - it('should be accessible (icon: "close", iconType: "skyux", size: undefined, fixedWidth: true, variant: "line")', async () => { - setupIcon('notification', 'skyux', undefined, true, 'line'); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); + it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: true, variant: "line")', async () => { + setupIcon('info-circle', undefined, '3x', true, 'line'); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: true, variant: "line")', async () => { + setupIcon('info-circle', 'fa', '3x', true, 'line'); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible (icon: "info-circle", iconType: undefined, size: "3x", fixedWidth: true, variant: "line")', async () => { - setupIcon('info-circle', undefined, '3x', true, 'line'); + it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: true, variant: "line")', async () => { + setupIcon('notification', 'skyux', '3x', true, 'line'); - await expectAsync(fixture.nativeElement).toBeAccessible(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); + }); + + describe('iconName', () => { + let resolverSvc: jasmine.SpyObj; + + function detectUrlChanges(): void { + fixture.detectChanges(); + + // Resolve icon ID Observable and apply changes. + tick(); + fixture.detectChanges(); + } + + function getSvgEl(): SVGElement { + return fixture.nativeElement.querySelector('.sky-icon-svg-img'); + } + + function setupIcon( + iconName: string, + size?: string, + variant?: SkyIconVariantType, + ): void { + cmp.iconName = iconName; + cmp.size = size; + cmp.variant = variant; + + fixture.detectChanges(); + } + + function validateIconId(expectedId: string): void { + const useEl = getSvgEl().querySelector('use'); + + expect(useEl?.href.baseVal).toBe(expectedId); + } + + beforeEach(() => { + resolverSvc = jasmine.createSpyObj( + 'SkyIconSvgResolverService', + ['resolveHref'], + ); - it('should be accessible (icon: "info-circle", iconType: "fa", size: "3x", fixedWidth: true, variant: "line")', async () => { - setupIcon('info-circle', 'fa', '3x', true, 'line'); + resolverSvc.resolveHref.and.callFake((src, size, variant) => { + return Promise.resolve(`#${src}-${size}-${variant ?? 'line'}`); + }); - await expectAsync(fixture.nativeElement).toBeAccessible(); + TestBed.configureTestingModule({ + imports: [SkyIconFixturesModule], + providers: [ + { + provide: SkyIconSvgResolverService, + useValue: resolverSvc, + }, + ], + }); + + fixture = TestBed.createComponent(IconTestComponent); + cmp = fixture.componentInstance; }); - it('should be accessible (icon: "notification", iconType: "skyux", size: "3x", fixedWidth: true, variant: "line")', async () => { - setupIcon('notification', 'skyux', '3x', true, 'line'); + it('should display the resolved icon by ID', fakeAsync(() => { + setupIcon('test', undefined, undefined); + detectUrlChanges(); + + validateIconId('#test-16-line'); + })); + + it('should display the resolved icon by ID and size', fakeAsync(() => { + setupIcon('test', '2x', undefined); + detectUrlChanges(); + + validateIconId('#test-32-line'); + })); + + it('should display the resolved icon by ID and variant', fakeAsync(() => { + setupIcon('test', undefined, 'solid'); + detectUrlChanges(); + + validateIconId('#test-16-solid'); + })); + + it('should display the resolved icon by ID, size, and variant', fakeAsync(() => { + setupIcon('test', '2x', 'solid'); + detectUrlChanges(); + + validateIconId('#test-32-solid'); + })); + + describe('a11y', () => { + it('should be accessible (icon: "test", size: undefined, variant: undefined)', async () => { + setupIcon('test', undefined, undefined); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: undefined)', async () => { + setupIcon('test', '2x', undefined); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: undefined, variant: "solid")', async () => { + setupIcon('test', undefined, 'solid'); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: undefined, variant: "line")', async () => { + setupIcon('test', undefined, 'line'); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: "solid")', async () => { + setupIcon('test', '2x', 'solid'); + + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); + + it('should be accessible (icon: "test", size: 2x, variant: "line")', async () => { + setupIcon('test', '2x', 'line'); - await expectAsync(fixture.nativeElement).toBeAccessible(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); }); }); From 24482ca29377fb091208feadc5da14eb2cf5af32 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 6 Sep 2024 09:33:44 -0400 Subject: [PATCH 94/95] chore: release 10.42.0 (#2676) --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66b918689..1fcce32f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # 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) diff --git a/package-lock.json b/package-lock.json index ea006127a4..622fc56940 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "10.41.5", + "version": "10.42.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "10.41.5", + "version": "10.42.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 37ad1fd65b..2ca8150045 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "10.41.5", + "version": "10.42.0", "license": "MIT", "scripts": { "ng": "nx", From d410f8471d3b7fcb4e440ac28e148d7ee6b3b117 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:36:41 -0400 Subject: [PATCH 95/95] chore: update Angular 17 --- karma.conf.js | 5 +- libs/components/a11y/package.json | 4 +- libs/components/action-bars/package.json | 8 +- libs/components/ag-grid/package.json | 6 +- .../angular-tree-component/package.json | 4 +- libs/components/animations/package.json | 6 +- libs/components/assets/package.json | 4 +- libs/components/autonumeric/package.json | 6 +- libs/components/avatar/package.json | 6 +- libs/components/colorpicker/package.json | 10 +- libs/components/config/package.json | 4 +- libs/components/core/package.json | 12 +- libs/components/data-manager/package.json | 6 +- libs/components/datetime/package.json | 10 +- libs/components/errors/package.json | 6 +- libs/components/flyout/package.json | 10 +- libs/components/forms/package.json | 10 +- libs/components/grids/package.json | 6 +- libs/components/help-inline/package.json | 6 +- libs/components/i18n/package.json | 6 +- libs/components/icon/package.json | 6 +- libs/components/indicators/package.json | 10 +- libs/components/inline-form/package.json | 6 +- libs/components/layout/package.json | 14 +- .../list-builder-common/package.json | 4 +- .../list-builder-view-checklist/package.json | 8 +- .../list-builder-view-grids/package.json | 6 +- libs/components/list-builder/package.json | 4 +- libs/components/lists/package.json | 8 +- libs/components/lookup/package.json | 12 +- libs/components/modals/package.json | 8 +- libs/components/navbar/package.json | 4 +- libs/components/omnibar-interop/package.json | 4 +- libs/components/packages/package.json | 4 +- libs/components/pages/package.json | 8 +- libs/components/phone-field/package.json | 12 +- libs/components/popovers/package.json | 10 +- .../progress-indicator/package.json | 4 +- libs/components/router/package.json | 8 +- libs/components/select-field/package.json | 6 +- libs/components/split-view/package.json | 8 +- libs/components/storybook/package.json | 8 +- libs/components/tabs/package.json | 10 +- libs/components/text-editor/package.json | 8 +- libs/components/theme/package.json | 4 +- libs/components/tiles/package.json | 4 +- libs/components/toast/package.json | 8 +- libs/components/validation/package.json | 6 +- libs/sdk/e2e-schematics/package.json | 6 +- libs/sdk/eslint-config/package.json | 2 +- libs/sdk/prettier-schematics/package.json | 2 +- libs/sdk/testing/package.json | 6 +- package-lock.json | 408 ++++++++---------- package.json | 32 +- 54 files changed, 363 insertions(+), 430 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index cb3846c64e..4b920a94f9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -38,9 +38,9 @@ module.exports = () => { ], customLaunchers: { ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', + base: 'Chrome', flags: [ - '--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 e20b47e88d..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", 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/config/package.json b/libs/components/config/package.json index d6fb2fc6eb..4cf54e2adb 100644 --- a/libs/components/config/package.json +++ b/libs/components/config/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/core/package.json b/libs/components/core/package.json index 253f006a36..30fe50147e 100644 --- a/libs/components/core/package.json +++ b/libs/components/core/package.json @@ -16,12 +16,12 @@ }, "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/router": "^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", + "@angular/router": "^17.3.12", "@skyux/i18n": "0.0.0-PLACEHOLDER" }, "dependencies": { diff --git a/libs/components/data-manager/package.json b/libs/components/data-manager/package.json index 3735625c63..a18755924a 100644 --- a/libs/components/data-manager/package.json +++ b/libs/components/data-manager/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/forms": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/datetime/package.json b/libs/components/datetime/package.json index 37a9d6c829..f7cfc243b6 100644 --- a/libs/components/datetime/package.json +++ b/libs/components/datetime/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/errors/package.json b/libs/components/errors/package.json index a3251c3963..fc0232ed41 100644 --- a/libs/components/errors/package.json +++ b/libs/components/errors/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/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/flyout/package.json b/libs/components/flyout/package.json index 0d520a50fa..240b60c99b 100644 --- a/libs/components/flyout/package.json +++ b/libs/components/flyout/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/router": "^17.3.4", + "@angular/animations": "^17.3.12", + "@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/router": "0.0.0-PLACEHOLDER", diff --git a/libs/components/forms/package.json b/libs/components/forms/package.json index c934303489..c9fc51b460 100644 --- a/libs/components/forms/package.json +++ b/libs/components/forms/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/help-inline": "0.0.0-PLACEHOLDER", diff --git a/libs/components/grids/package.json b/libs/components/grids/package.json index a626eaf5e1..13b4e6c8e1 100644 --- a/libs/components/grids/package.json +++ b/libs/components/grids/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/forms": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/help-inline/package.json b/libs/components/help-inline/package.json index b1c5a87c72..547e574dac 100644 --- a/libs/components/help-inline/package.json +++ b/libs/components/help-inline/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", "@skyux/popovers": "0.0.0-PLACEHOLDER", diff --git a/libs/components/i18n/package.json b/libs/components/i18n/package.json index 49cf3a4b12..42aa57b18c 100644 --- a/libs/components/i18n/package.json +++ b/libs/components/i18n/package.json @@ -20,9 +20,9 @@ "save": "dependencies" }, "peerDependencies": { - "@angular/cli": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/cli": "^17.3.9", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/assets": "0.0.0-PLACEHOLDER" }, "dependencies": { diff --git a/libs/components/icon/package.json b/libs/components/icon/package.json index b983f03a43..fd4016807e 100644 --- a/libs/components/icon/package.json +++ b/libs/components/icon/package.json @@ -16,9 +16,9 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/core": "^17.3.4", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/core": "^17.3.12", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/theme": "0.0.0-PLACEHOLDER" }, diff --git a/libs/components/indicators/package.json b/libs/components/indicators/package.json index 4557609c6a..8599159ecb 100644 --- a/libs/components/indicators/package.json +++ b/libs/components/indicators/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/help-inline": "0.0.0-PLACEHOLDER", diff --git a/libs/components/inline-form/package.json b/libs/components/inline-form/package.json index bf3abffcab..6742d6347c 100644 --- a/libs/components/inline-form/package.json +++ b/libs/components/inline-form/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", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER" }, diff --git a/libs/components/layout/package.json b/libs/components/layout/package.json index 9939b5ae0d..1567cdf8f7 100644 --- a/libs/components/layout/package.json +++ b/libs/components/layout/package.json @@ -16,13 +16,13 @@ }, "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/router": "^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", + "@angular/router": "^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/list-builder-common/package.json b/libs/components/list-builder-common/package.json index 659288f84d..36b9b4988c 100644 --- a/libs/components/list-builder-common/package.json +++ b/libs/components/list-builder-common/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/list-builder-view-checklist/package.json b/libs/components/list-builder-view-checklist/package.json index 34d0bf10e7..dff604b22f 100644 --- a/libs/components/list-builder-view-checklist/package.json +++ b/libs/components/list-builder-view-checklist/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-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", diff --git a/libs/components/list-builder-view-grids/package.json b/libs/components/list-builder-view-grids/package.json index ad4f3cc780..a0f5c59545 100644 --- a/libs/components/list-builder-view-grids/package.json +++ b/libs/components/list-builder-view-grids/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/grids": "0.0.0-PLACEHOLDER", diff --git a/libs/components/list-builder/package.json b/libs/components/list-builder/package.json index 7c462955b0..843107e52a 100644 --- a/libs/components/list-builder/package.json +++ b/libs/components/list-builder/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/forms": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", diff --git a/libs/components/lists/package.json b/libs/components/lists/package.json index b63606b3a8..56672b454d 100644 --- a/libs/components/lists/package.json +++ b/libs/components/lists/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/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-sdk/testing": "0.0.0-PLACEHOLDER", "@skyux/animations": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", 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/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/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 344c2ab28c..69cb6b9f62 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -92,8 +92,8 @@ } }, "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/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/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/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/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/theme/package.json b/libs/components/theme/package.json index 6fd3da5e80..1ed848ef28 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/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": { "@blackbaud/skyux-design-tokens": "0.0.28", 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/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/package-lock.json b/package-lock.json index 622fc56940..dfa6b6274f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,15 @@ "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", @@ -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", @@ -74,7 +74,7 @@ "@percy/sdk-utils": "1.29.3", "@ryansonshine/commitizen": "4.2.8", "@ryansonshine/cz-conventional-changelog": "3.3.4", - "@schematics/angular": "17.3.2", + "@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", @@ -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": { @@ -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", @@ -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" } @@ -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" @@ -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 2ca8150045..9e38296d6b 100644 --- a/package.json +++ b/package.json @@ -88,15 +88,15 @@ }, "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", @@ -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", @@ -152,7 +152,7 @@ "@percy/sdk-utils": "1.29.3", "@ryansonshine/commitizen": "4.2.8", "@ryansonshine/cz-conventional-changelog": "3.3.4", - "@schematics/angular": "17.3.2", + "@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",