From 3d35f3ce8b501896c14d28a15199b6211a2922b6 Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Mon, 10 Oct 2022 14:20:47 -0400 Subject: [PATCH 01/30] docs(components/indicators): add tokens harness to docs (#649) (#650) --- libs/components/indicators/testing/src/tokens/token-harness.ts | 1 - libs/components/indicators/testing/src/tokens/tokens-harness.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/libs/components/indicators/testing/src/tokens/token-harness.ts b/libs/components/indicators/testing/src/tokens/token-harness.ts index 32bb339018..000c0fae9c 100644 --- a/libs/components/indicators/testing/src/tokens/token-harness.ts +++ b/libs/components/indicators/testing/src/tokens/token-harness.ts @@ -4,7 +4,6 @@ import { SkyTokenHarnessFilters } from './token-harness-filters'; /** * Harness for interacting with a token component in tests. - * @internal */ export class SkyTokenHarness extends ComponentHarness { /** diff --git a/libs/components/indicators/testing/src/tokens/tokens-harness.ts b/libs/components/indicators/testing/src/tokens/tokens-harness.ts index b1c5f88928..3af7a54a60 100644 --- a/libs/components/indicators/testing/src/tokens/tokens-harness.ts +++ b/libs/components/indicators/testing/src/tokens/tokens-harness.ts @@ -6,7 +6,6 @@ import { SkyTokensHarnessFilters } from './tokens-harness-filters'; /** * Harness for interacting with a tokens component in tests. - * @internal */ export class SkyTokensHarness extends SkyComponentHarness { /** From f46520739ebf874d759efa372a809d19cee3afb6 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Tue, 11 Oct 2022 10:17:27 -0400 Subject: [PATCH 02/30] feat(components/modals): remove public export of confirm button (#656) BREAKING CHANGE: The `SkyConfirmButton` component is intended for internal use only and is removed from the exported API. To address this, remove any usages of the `SkyConfirmButton` component. --- .../indicators/src/lib/modules/key-info/key-info.component.ts | 1 - libs/components/modals/src/index.ts | 2 -- 2 files changed, 3 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 a1f355ccd2..f526e550f6 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 @@ -13,7 +13,6 @@ export class SkyKeyInfoComponent { * value or in a horizontal layout with the label beside the value. * @default "vertical" */ - // TODO: More strongly type this in a future breaking change. @Input() public layout: SkyKeyInfoLayoutType | undefined = 'vertical'; } diff --git a/libs/components/modals/src/index.ts b/libs/components/modals/src/index.ts index 87d4a6fce9..ddedf4c4ea 100644 --- a/libs/components/modals/src/index.ts +++ b/libs/components/modals/src/index.ts @@ -1,5 +1,3 @@ -// TODO: confirm-button is internal and should be removed in a future version -export * from './lib/modules/confirm/confirm-button'; export * from './lib/modules/confirm/confirm-button-action'; export * from './lib/modules/confirm/confirm-button-config'; export * from './lib/modules/confirm/confirm-button-style-type'; From 74396bb18906e82e86fa920276c8f709bd5b0143 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Tue, 11 Oct 2022 10:17:57 -0400 Subject: [PATCH 03/30] feat(components/lookup): deprecate search inputs (#647) --- .../src/lib/modules/autocomplete/autocomplete.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 37fb682b36..389e2ae291 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts @@ -146,6 +146,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { /** * Specifies the object properties to search. * @default ["name"] + * @deprecated We recommend against using this property. To search specific properties, use the `searchAsync` event instead. */ @Input() public set propertiesToSearch(value: string[] | undefined) { @@ -160,9 +161,8 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { * Specifies a function to dynamically manage the data source when users * change the text in the autocomplete 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 We recommend against using this property. To call a remote data source, use the `searchAsync` event instead. */ @Input() public set search(value: SkyAutocompleteSearchFunction | undefined) { @@ -211,6 +211,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { * using the `search` property to specify a custom search function, you must * manually apply filters inside that function. The function must return `true` * or `false` for each result to indicate whether to display it in the dropdown list. + * @deprecated We recommend against using this property. To filter results, use the `searchAsync` event instead. */ @Input() public searchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; From fbb350a3d737ffd52724c651b8d5f4f19656b4e0 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 11 Oct 2022 10:19:12 -0400 Subject: [PATCH 04/30] refactor: cleanup async tests (#646) --- .../skip-link-host.component.spec.ts | 6 +- ...mmary-action-bar-actions.component.spec.ts | 41 +++---- .../cell-renderer-currency.component.spec.ts | 10 +- .../modules/overlay/overlay.service.spec.ts | 46 ++++---- .../daypicker-cell.component.spec.ts | 18 +-- .../testing/src/datepicker-fixture.spec.ts | 13 +-- .../error/error-modal-form.component.spec.ts | 13 +-- .../file-attachment.component.spec.ts | 27 ++--- .../file-drop.component.spec.ts | 18 +-- .../input-box/input-box.component.spec.ts | 21 ++-- .../radio/radio-group.component.spec.ts | 12 +- .../lib/modules/radio/radio.component.spec.ts | 25 ++-- .../selection-box.component.spec.ts | 110 +++++++----------- .../lib/modules/alert/alert.component.spec.ts | 44 +++---- .../help-inline/help-inline.component.spec.ts | 13 +-- .../modules/icon/icon-stack.component.spec.ts | 13 +-- .../lib/modules/icon/icon.component.spec.ts | 13 +-- .../text-highlight.directive.spec.ts | 6 +- .../inline-form/inline-form.component.spec.ts | 32 ++--- .../lib/modules/card/card.component.spec.ts | 82 ++++++------- .../definition-list.component.spec.ts | 13 +-- .../inline-delete.component.spec.ts | 46 +++----- .../page-summary.component.spec.ts | 13 +-- .../modules/confirm/confirm.component.spec.ts | 50 ++++---- .../dropdown/dropdown.component.spec.ts | 59 +++++----- .../src/popover/popover-fixture.spec.ts | 7 +- .../lib/modules/href/href.directive.spec.ts | 19 ++- .../lib/modules/tabs/tabset.component.spec.ts | 13 +-- .../modules/tiles/tile/tile.component.spec.ts | 18 ++- .../lib/modules/toast/toast.component.spec.ts | 12 +- .../email-validation.directive.spec.ts | 12 +- .../url-validation.directive.spec.ts | 22 ++-- 32 files changed, 368 insertions(+), 479 deletions(-) diff --git a/libs/components/a11y/src/lib/modules/skip-link/skip-link-host.component.spec.ts b/libs/components/a11y/src/lib/modules/skip-link/skip-link-host.component.spec.ts index ce1b076d96..b572679489 100644 --- a/libs/components/a11y/src/lib/modules/skip-link/skip-link-host.component.spec.ts +++ b/libs/components/a11y/src/lib/modules/skip-link/skip-link-host.component.spec.ts @@ -1,5 +1,5 @@ import { DebugElement, ElementRef } from '@angular/core'; -import { TestBed, async } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { expect, expectAsync } from '@skyux-sdk/testing'; @@ -46,7 +46,7 @@ describe('Skip link host component', () => { document.body.removeChild(testEl2); }); - it('should render a list of skip links', async(() => { + it('should render a list of skip links', () => { const fixture = TestBed.createComponent(SkySkipLinkHostComponent); const links = [ @@ -67,7 +67,7 @@ describe('Skip link host component', () => { validateSkipLink(links[0], skipLinkEls[0], testEl1); validateSkipLink(links[1], skipLinkEls[1], testEl2); - })); + }); it('should be accssible', async () => { const fixture = TestBed.createComponent(SkySkipLinkHostComponent); diff --git a/libs/components/action-bars/src/lib/modules/summary-action-bar/actions/summary-action-bar-actions.component.spec.ts b/libs/components/action-bars/src/lib/modules/summary-action-bar/actions/summary-action-bar-actions.component.spec.ts index 030abdb8e9..cc436cca8b 100644 --- a/libs/components/action-bars/src/lib/modules/summary-action-bar/actions/summary-action-bar-actions.component.spec.ts +++ b/libs/components/action-bars/src/lib/modules/summary-action-bar/actions/summary-action-bar-actions.component.spec.ts @@ -2,12 +2,11 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { expect } from '@skyux-sdk/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyMediaBreakpoints, SkyMediaQueryService } from '@skyux/core'; import { MockSkyMediaQueryService } from '@skyux/core/testing'; import { SkyDropdownMessageType } from '@skyux/popovers'; @@ -186,35 +185,31 @@ describe('Summary Action Bar action components', () => { }); describe('a11y', () => { - it('should be accessible (standard lg setup)', async(() => { + it('should be accessible (standard lg setup)', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible (standard xs setup)', async(() => { + it('should be accessible (standard xs setup)', async () => { fixture.detectChanges(); mockMediaQueryService.fire(SkyMediaBreakpoints.xs); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible (standard xs setup collapsed summary)', async(() => { + it('should be accessible (standard xs setup collapsed summary)', async () => { fixture.detectChanges(); mockMediaQueryService.fire(SkyMediaBreakpoints.xs); fixture.detectChanges(); - fixture.whenStable().then(() => { - debugElement - .query(By.css('.sky-summary-action-bar-details-collapse button')) - .nativeElement.click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - }); - })); + await fixture.whenStable(); + debugElement + .query(By.css('.sky-summary-action-bar-details-collapse button')) + .nativeElement.click(); + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); }); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-renderers/cell-renderer-currency/cell-renderer-currency.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-renderers/cell-renderer-currency/cell-renderer-currency.component.spec.ts index 11168afbd7..681dca54bc 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-renderers/cell-renderer-currency/cell-renderer-currency.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-renderers/cell-renderer-currency/cell-renderer-currency.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; @@ -152,11 +151,10 @@ describe('SkyAgGridCellRendererCurrencyComponent', () => { })); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { currencyFixture.detectChanges(); - currencyFixture.whenStable().then(() => { - return expectAsync(currencyNativeElement).toBeAccessible(); - }); - })); + await currencyFixture.whenStable(); + await expectAsync(currencyNativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/core/src/lib/modules/overlay/overlay.service.spec.ts b/libs/components/core/src/lib/modules/overlay/overlay.service.spec.ts index 96edc3cf81..b02ea28381 100644 --- a/libs/components/core/src/lib/modules/overlay/overlay.service.spec.ts +++ b/libs/components/core/src/lib/modules/overlay/overlay.service.spec.ts @@ -2,13 +2,12 @@ import { NgZone } from '@angular/core'; import { ComponentFixture, TestBed, - async, fakeAsync, inject, tick, } from '@angular/core/testing'; import { Router } from '@angular/router'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { take } from 'rxjs/operators'; @@ -264,7 +263,7 @@ describe('Overlay service', () => { ); })); - it('should attach a component', async(async () => { + it('should attach a component', async () => { const overlay = service.create(); overlay.attachComponent(OverlayEntryFixtureComponent); @@ -276,9 +275,9 @@ describe('Overlay service', () => { expect(getAllOverlays().item(0).textContent).toContain( 'Overlay content ID: none' ); - })); + }); - it('should attach a component with providers', async(async () => { + it('should attach a component with providers', async () => { const overlay = service.create(); overlay.attachComponent(OverlayEntryFixtureComponent, [ @@ -295,9 +294,9 @@ describe('Overlay service', () => { expect(getAllOverlays().item(0).textContent).toContain( 'Overlay content ID: 1' ); - })); + }); - it('should attach a template', async(async () => { + it('should attach a template', async () => { const overlay = service.create(); overlay.attachTemplate(fixture.componentInstance.myTemplate, { @@ -313,35 +312,34 @@ describe('Overlay service', () => { expect(getAllOverlays().item(0).textContent).toContain( 'Templated content ID: 5' ); - })); + }); - it('should be accessible', async(async (done: DoneFn) => { + it('should be accessible', async () => { const overlay = service.create(); fixture.detectChanges(); await fixture.whenStable(); - expect(getAllOverlays().item(0)).toBeAccessible(async () => { - service.close(overlay); + await expectAsync(getAllOverlays().item(0)).toBeAccessible(); + service.close(overlay); - fixture.detectChanges(); + fixture.detectChanges(); - // Create overlay with all options turned on. - service.create({ - closeOnNavigation: false, - enableClose: true, - enableScroll: false, - showBackdrop: true, - }); + // Create overlay with all options turned on. + service.create({ + closeOnNavigation: false, + enableClose: true, + enableScroll: false, + showBackdrop: true, + }); - fixture.detectChanges(); + fixture.detectChanges(); - await fixture.whenStable(); + await fixture.whenStable(); - expect(getAllOverlays().item(0)).toBeAccessible(done); - }); - })); + await expectAsync(getAllOverlays().item(0)).toBeAccessible(); + }); it('should emit when overlay is closed by the service', fakeAsync(() => { const instance = createOverlay(); diff --git a/libs/components/datetime/src/lib/modules/datepicker/daypicker-cell.component.spec.ts b/libs/components/datetime/src/lib/modules/datepicker/daypicker-cell.component.spec.ts index 20ca4bac14..7003d4c35a 100644 --- a/libs/components/datetime/src/lib/modules/datepicker/daypicker-cell.component.spec.ts +++ b/libs/components/datetime/src/lib/modules/datepicker/daypicker-cell.component.spec.ts @@ -367,9 +367,7 @@ describe('daypicker cell', () => { tick(500); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(controlerSpy).not.toHaveBeenCalled(); - }); + expect(controlerSpy).not.toHaveBeenCalled(); })); it('should not open the tool tip if already open', fakeAsync(() => { @@ -381,9 +379,7 @@ describe('daypicker cell', () => { tick(500); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(controlerSpy).not.toHaveBeenCalled(); - }); + expect(controlerSpy).not.toHaveBeenCalled(); })); it('should not open the tool tip if cancelled', async () => { @@ -412,9 +408,7 @@ describe('daypicker cell', () => { tick(500); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(controlerSpy).not.toHaveBeenCalled(); - }); + expect(controlerSpy).not.toHaveBeenCalled(); })); it('should open the tool tip', fakeAsync(() => { @@ -427,10 +421,8 @@ describe('daypicker cell', () => { tick(500); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(controlerSpy).toHaveBeenCalledWith({ - type: SkyPopoverMessageType.Open, - }); + expect(controlerSpy).toHaveBeenCalledWith({ + type: SkyPopoverMessageType.Open, }); })); }); diff --git a/libs/components/datetime/testing/src/datepicker-fixture.spec.ts b/libs/components/datetime/testing/src/datepicker-fixture.spec.ts index 136a99820a..612f62a676 100644 --- a/libs/components/datetime/testing/src/datepicker-fixture.spec.ts +++ b/libs/components/datetime/testing/src/datepicker-fixture.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { expect } from '@skyux-sdk/testing'; import { SkyDatepickerModule } from '@skyux/datetime'; @@ -33,18 +33,17 @@ describe('Datepicker fixture', () => { }); }); - it('should expose the provided properties', async(() => { + it('should expose the provided properties', async () => { const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const datepicker = new SkyDatepickerFixture(fixture, 'test-datepicker'); - fixture.whenStable().then(() => { - expect(datepicker.disabled).toBe(false); - expect(datepicker.date).toEqual(fixture.componentInstance.date); - }); - })); + await fixture.whenStable(); + expect(datepicker.disabled).toBe(false); + expect(datepicker.date).toEqual(fixture.componentInstance.date); + }); it('should open and close the calendar when clicked', fakeAsync(() => { const fixture = TestBed.createComponent(TestComponent); diff --git a/libs/components/errors/src/lib/modules/error/error-modal-form.component.spec.ts b/libs/components/errors/src/lib/modules/error/error-modal-form.component.spec.ts index 44cd7976a3..3555d53ac2 100644 --- a/libs/components/errors/src/lib/modules/error/error-modal-form.component.spec.ts +++ b/libs/components/errors/src/lib/modules/error/error-modal-form.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyModalHostService } from '@skyux/modals'; import { SkyModalConfiguration } from '@skyux/modals'; import { SkyModalInstance } from '@skyux/modals'; @@ -63,9 +63,8 @@ describe('Error modal form component', () => { expect(modalInstance.close).toHaveBeenCalled(); }); - it('should pass accessibility', async(() => { - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + it('should pass accessibility', async () => { + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); 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 3c0540a156..8058505c70 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 @@ -2,7 +2,6 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; @@ -1191,14 +1190,14 @@ describe('File attachment', () => { expect(fileChangeActual?.file.url).toBe('$/url'); }); - it('shows the thumbnail if the item is an image', async(() => { + it('shows the thumbnail if the item is an image', () => { testImage('png'); testImage('bmp'); testImage('jpeg'); testImage('gif'); - })); + }); - it('does not show an icon if it is not an image', async(() => { + it('does not show an icon if it is not an image', () => { testNonImageType('pdf', 'pdf'); testNonImageType('gz', 'gz'); testNonImageType('rar', 'rar'); @@ -1217,7 +1216,7 @@ describe('File attachment', () => { testNonImageType('tiff', 'image'); testNonImageType('other', 'text'); testNonImageType('mp4', 'video'); - })); + }); it('should not show an icon if file or type does not exist', () => { const imageEl = getImage(); @@ -1257,20 +1256,18 @@ describe('File attachment', () => { ).toBeTruthy(); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(async () => { - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should pass accessibility when label does not match the button text', async(() => { + it('should pass accessibility when label does not match the button text', async () => { fixture.componentInstance.labelText = 'Something different'; fixture.detectChanges(); - fixture.whenStable().then(async () => { - await expectAsync(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('File attachment (template-driven)', () => { 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 c03a89b17e..211138cf82 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 @@ -1,12 +1,7 @@ import { Component, DebugElement } from '@angular/core'; -import { - ComponentFixture, - TestBed, - async, - fakeAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyFileAttachmentsModule } from './file-attachments.module'; import { SkyFileDropComponent } from './file-drop.component'; @@ -989,10 +984,9 @@ describe('File drop component', () => { expect(dropEl.attributes.getNamedItem('aria-label')?.value).toBe('Test 12'); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); 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 818ee0dfcf..df22f53dd4 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 @@ -1,12 +1,11 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { AbstractControl } from '@angular/forms'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyTheme, SkyThemeMode, @@ -284,15 +283,14 @@ describe('Input box component', () => { expect(inputBoxWrapperEl).toHaveCssClass('sky-input-box-disabled'); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { const fixture = TestBed.createComponent(InputBoxFixtureComponent); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('in modern theme', () => { @@ -620,15 +618,14 @@ describe('Input box component', () => { expect(inputBoxWrapperEl).toHaveCssClass('sky-input-box-disabled'); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { const fixture = TestBed.createComponent(InputBoxFixtureComponent); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); it('should add an invalid CSS class when marked invalid with hasErrors property', () => { const fixture = TestBed.createComponent(InputBoxFixtureComponent); 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 7c7fe6f19b..8bea07a8d4 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 @@ -1,12 +1,11 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { expect } from '@skyux-sdk/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyRadioFixturesModule } from './fixtures/radio-fixtures.module'; import { SkyRadioGroupBooleanTestComponent } from './fixtures/radio-group-boolean.component.fixture'; @@ -555,12 +554,11 @@ describe('Radio group component (reactive)', function () { } })); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('Radio group component (template-driven)', () => { 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 36669bae30..8edc2391b7 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 @@ -2,13 +2,12 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { NgModel } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyRadioFixturesModule } from './fixtures/radio-fixtures.module'; import { SkyRadioOnPushTestComponent } from './fixtures/radio-on-push.component.fixture'; @@ -265,12 +264,11 @@ describe('Radio component', function () { expect(radioLabelElement.componentInstance.onClick).toHaveBeenCalled(); })); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('Radio icon component', () => { @@ -325,12 +323,11 @@ describe('Radio component', function () { expect(span).toHaveCssClass('sky-switch-control-danger'); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('Radio component with a consumer using OnPush change detection', () => { @@ -341,7 +338,7 @@ describe('Radio component', function () { componentInstance = fixture.componentInstance; }); - it('should update the ngModel properly when radio button is changed', async(async () => { + it('should update the ngModel properly when radio button is changed', async () => { fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); @@ -365,6 +362,6 @@ describe('Radio component', function () { expect(radios.item(2).checked).toBeFalsy(); expect(componentInstance.selectedValue).toBe('2'); - })); + }); }); }); diff --git a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts index 9e0ba9720e..e3c5c9544f 100644 --- a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts +++ b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts @@ -1,5 +1,5 @@ import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { @@ -159,26 +159,21 @@ describe('Selection box component', () => { }); // Tests that depend on sky-checkbox and sky-radio need to use async. - it('show selected state when selection box is clicked', async(() => { + it('show selected state when selection box is clicked', async () => { const selectionBoxes = getCheckboxSelectionBoxes(); selectionBoxes[1].click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(selectionBoxes[0]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); - expect(selectionBoxes[2]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - }); - })); + await fixture.whenStable(); + fixture.detectChanges(); + expect(selectionBoxes[0]).not.toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[2]).not.toHaveCssClass('sky-selection-box-selected'); + }); // Tests that depend on sky-checkbox and sky-radio need to use async. - it('show selected state when space key is pressed', async(() => { + it('show selected state when space key is pressed', async () => { const selectionBoxes = getCheckboxSelectionBoxes(); SkyAppTestUtility.fireDomEvent(selectionBoxes[1], 'keydown', { @@ -188,55 +183,40 @@ describe('Selection box component', () => { }); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(selectionBoxes[0]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); - expect(selectionBoxes[2]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - }); - })); + await fixture.whenStable(); + fixture.detectChanges(); + expect(selectionBoxes[0]).not.toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[2]).not.toHaveCssClass('sky-selection-box-selected'); + }); // Tests that depend on sky-checkbox and sky-radio need to use async. - it('show selected state when checkbox is clicked', async(() => { + it('show selected state when checkbox is clicked', async () => { const selectionBoxes = getCheckboxSelectionBoxes(); getCheckboxes()[1].click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(selectionBoxes[0]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); - expect(selectionBoxes[2]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - }); - })); + await fixture.whenStable(); + fixture.detectChanges(); + expect(selectionBoxes[0]).not.toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[2]).not.toHaveCssClass('sky-selection-box-selected'); + }); // Tests that depend on sky-checkbox and sky-radio need to use async. - it('show selected state when radio button is clicked', async(() => { + it('show selected state when radio button is clicked', async () => { const selectionBoxes = getRadioSelectionBoxes(); getRadioButtons()[1].click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(selectionBoxes[0]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); - expect(selectionBoxes[2]).not.toHaveCssClass( - 'sky-selection-box-selected' - ); - }); - })); + await fixture.whenStable(); + fixture.detectChanges(); + expect(selectionBoxes[0]).not.toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[1]).toHaveCssClass('sky-selection-box-selected'); + expect(selectionBoxes[2]).not.toHaveCssClass('sky-selection-box-selected'); + }); it('should have a role of button', () => { const role: string | null = @@ -250,24 +230,22 @@ describe('Selection box component', () => { expect(tabIndex).toBe('0'); }); - it('should have a tabindex of -1 when the control is disabled', async(() => { - fixture.whenStable().then(() => { - fixture.detectChanges(); - const disabledSelectionBox = fixture.nativeElement.querySelector( - '#disabled-selection-box .sky-selection-box' - ); - const tabIndex: string = disabledSelectionBox.getAttribute('tabindex'); - expect(tabIndex).toBe('-1'); - }); - })); + it('should have a tabindex of -1 when the control is disabled', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + const disabledSelectionBox = fixture.nativeElement.querySelector( + '#disabled-selection-box .sky-selection-box' + ); + const tabIndex: string = disabledSelectionBox.getAttribute('tabindex'); + expect(tabIndex).toBe('-1'); + }); - it('should update tabindex of tabbable children elements to -1', async(() => { - fixture.whenStable().then(() => { - fixture.detectChanges(); - const tabbableChild = fixture.nativeElement.querySelector('#link'); - expect(tabbableChild.getAttribute('tabindex')).toBe('-1'); - }); - })); + it('should update tabindex of tabbable children elements to -1', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + const tabbableChild = fixture.nativeElement.querySelector('#link'); + expect(tabbableChild.getAttribute('tabindex')).toBe('-1'); + }); it('should be accessible', async () => { document.body.classList.add('sky-theme-modern'); diff --git a/libs/components/indicators/src/lib/modules/alert/alert.component.spec.ts b/libs/components/indicators/src/lib/modules/alert/alert.component.spec.ts index 513bcb8560..ec58a382b3 100644 --- a/libs/components/indicators/src/lib/modules/alert/alert.component.spec.ts +++ b/libs/components/indicators/src/lib/modules/alert/alert.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyTheme, SkyThemeMode, @@ -42,7 +42,7 @@ describe('Alert component', () => { }); }); - it('should hide the close button if it is not closeable', async(() => { + it('should hide the close button if it is not closeable', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement as HTMLElement; @@ -58,10 +58,10 @@ describe('Alert component', () => { fixture.detectChanges(); expect(attributes?.getNamedItem('hidden')).not.toBeNull(); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be hidden when the close button is clicked', async(() => { + it('should be hidden when the close button is clicked', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement; @@ -72,10 +72,10 @@ describe('Alert component', () => { expect(el.querySelector('.sky-alert').attributes['hidden']).not.toBeNull(); expect(cmp.closed).toBe(true); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should allow the screen reader text for the close button to be localizable', async(() => { + it('should allow the screen reader text for the close button to be localizable', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement as HTMLElement; @@ -85,10 +85,10 @@ describe('Alert component', () => { const closeEl = el.querySelector('.sky-alert-close'); expect(closeEl?.getAttribute('aria-label')).toBe('Close the alert'); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should add the appropriate styling when an alert type is specified', async(() => { + it('should add the appropriate styling when an alert type is specified', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement as HTMLElement; @@ -98,10 +98,10 @@ describe('Alert component', () => { const alertEl = el.querySelector('.sky-alert'); expect(alertEl?.classList.contains('sky-alert-success')).toBe(true); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should default to "warning" when no alert type is specified', async(() => { + it('should default to "warning" when no alert type is specified', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement as HTMLElement; @@ -111,10 +111,10 @@ describe('Alert component', () => { const alertEl = el.querySelector('.sky-alert'); expect(alertEl?.classList.contains('sky-alert-warning')).toBe(true); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should have a role of "alert"', async(() => { + it('should have a role of "alert"', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance as AlertTestComponent; const el = fixture.nativeElement as HTMLElement; @@ -124,8 +124,8 @@ describe('Alert component', () => { const alertEl = el.querySelector('.sky-alert'); expect(alertEl?.getAttribute('role')).toBe('alert'); - expect(fixture.nativeElement).toBeAccessible(); - })); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); describe('with description', () => { function validateDescription( @@ -199,7 +199,7 @@ describe('Alert component', () => { }); }); - it('should show the expected icon', async(() => { + it('should show the expected icon', async () => { const fixture = TestBed.createComponent(AlertTestComponent); const cmp = fixture.componentInstance; const el = fixture.nativeElement; @@ -223,6 +223,6 @@ describe('Alert component', () => { fixture.detectChanges(); validateStackedIcon(el, 'triangle-solid', 'exclamation'); - })); + }); }); }); 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 402ee11632..cb65fe75e4 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 @@ -1,7 +1,7 @@ import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; -import { expect } from '@skyux-sdk/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyHelpInlineModule } from '../help-inline/help-inline.module'; @@ -33,10 +33,9 @@ describe('Help inline component', () => { expect(cmp.buttonIsClicked).toBe(true); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/indicators/src/lib/modules/icon/icon-stack.component.spec.ts b/libs/components/indicators/src/lib/modules/icon/icon-stack.component.spec.ts index ffc85bd80e..17ad65fe18 100644 --- a/libs/components/indicators/src/lib/modules/icon/icon-stack.component.spec.ts +++ b/libs/components/indicators/src/lib/modules/icon/icon-stack.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { IconStackTestComponent } from './fixtures/icon-stack.component.fixture'; import { SkyIconModule } from './icon.module'; @@ -19,7 +19,7 @@ describe('Icon stack component', () => { element = fixture.nativeElement; }); - it('should display an icon stack based on the given icons', async(() => { + it('should display an icon stack based on the given icons', async () => { fixture.detectChanges(); const wrapperEl = element.querySelector('span'); @@ -41,8 +41,7 @@ describe('Icon stack component', () => { expect(topIconEl).toHaveCssClass('fa-inverse'); // Accessibility checks - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/indicators/src/lib/modules/icon/icon.component.spec.ts b/libs/components/indicators/src/lib/modules/icon/icon.component.spec.ts index 9f6813fdf3..465b6508ef 100644 --- a/libs/components/indicators/src/lib/modules/icon/icon.component.spec.ts +++ b/libs/components/indicators/src/lib/modules/icon/icon.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyIconFixturesModule } from './fixtures/icon-fixtures.module'; import { IconTestComponent } from './fixtures/icon.component.fixture'; @@ -37,7 +37,7 @@ describe('Icon component', () => { element = fixture.nativeElement as HTMLElement; }); - it('should display an icon based on the given icon', async(() => { + 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'); @@ -48,10 +48,9 @@ describe('Icon component', () => { expect(element.querySelector('.sky-icon')?.classList.length).toBe(4); // Accessibility checks - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); it('should display a different icon with a different size and a fixedWidth', () => { cmp.icon = 'broom'; diff --git a/libs/components/indicators/src/lib/modules/text-highlight/text-highlight.directive.spec.ts b/libs/components/indicators/src/lib/modules/text-highlight/text-highlight.directive.spec.ts index c743bce076..a97aa578a8 100644 --- a/libs/components/indicators/src/lib/modules/text-highlight/text-highlight.directive.spec.ts +++ b/libs/components/indicators/src/lib/modules/text-highlight/text-highlight.directive.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyMutationObserverService } from '@skyux/core'; @@ -79,7 +79,7 @@ describe('Text Highlight', () => { expect(containerEl.querySelector('mark')).toBeFalsy(); }); - it('should highlight on startup if search term is set in component', async(() => { + it('should highlight on startup if search term is set in component', () => { fixture = TestBed.createComponent(SkyTextHighlightTestComponent); nativeElement = fixture.nativeElement as HTMLElement; component = fixture.componentInstance; @@ -94,7 +94,7 @@ describe('Text Highlight', () => { if (mark) { validateInnerHTML(mark, 'test'); } - })); + }); it('should highlight search term', () => { updateInputText(fixture, 'text'); diff --git a/libs/components/inline-form/src/lib/modules/inline-form/inline-form.component.spec.ts b/libs/components/inline-form/src/lib/modules/inline-form/inline-form.component.spec.ts index b22f269139..27c121006e 100644 --- a/libs/components/inline-form/src/lib/modules/inline-form/inline-form.component.spec.ts +++ b/libs/components/inline-form/src/lib/modules/inline-form/inline-form.component.spec.ts @@ -258,44 +258,36 @@ describe('Inline form component', () => { component.showFormWithOutAutocomplete = true; showForm(); - fixture.whenStable().then(() => { - expect(document.activeElement).toEqual( - document.querySelector('#demo-input-3') - ); - }); + expect(document.activeElement).toEqual( + document.querySelector('#demo-input-3') + ); })); it('should focus the autofocus element when there is one present', fakeAsync(() => { component.showFormWithAutocomplete = true; showForm(); - fixture.whenStable().then(() => { - expect(document.activeElement).toEqual( - document.querySelector('#demo-input-6') - ); - }); + expect(document.activeElement).toEqual( + document.querySelector('#demo-input-6') + ); })); it('should focus the first element thats visible', fakeAsync(() => { component.showFormWithHiddenElements = true; showForm(); - fixture.whenStable().then(() => { - expect(document.activeElement).toEqual( - document.querySelector('#demo-input-8') - ); - }); + expect(document.activeElement).toEqual( + document.querySelector('#demo-input-8') + ); })); it('should not move focus if there are no focusable elements in the form', fakeAsync(() => { component.showFormWithNoElements = true; showForm(); - fixture.whenStable().then(() => { - expect(document.activeElement).toEqual( - document.querySelector('#demo-input-1') - ); - }); + expect(document.activeElement).toEqual( + document.querySelector('#demo-input-1') + ); })); it('should change the buttons when config input is changed', fakeAsync(() => { diff --git a/libs/components/layout/src/lib/modules/card/card.component.spec.ts b/libs/components/layout/src/lib/modules/card/card.component.spec.ts index 93892e9c0b..79571f4d08 100644 --- a/libs/components/layout/src/lib/modules/card/card.component.spec.ts +++ b/libs/components/layout/src/lib/modules/card/card.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyInlineDeleteType } from '../inline-delete/inline-delete-type'; @@ -169,56 +169,50 @@ describe('Card component', () => { expect(el.querySelector('.sky-card-header')).toBeNull(); }); - it('should be accessible when not selectable', async(() => { + it('should be accessible when not selectable', async () => { cmp.showCheckbox = false; fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible when selectable', async(() => { + it('should be accessible when selectable', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible when title is removed', async(() => { + it('should be accessible when title is removed', async () => { cmp.showTitle = false; fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should set the inline delete to the card style when initially added', async(() => { + it('should set the inline delete to the card style when initially added', async () => { cmp.showDelete = true; fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(el.querySelector('sky-inline-delete')).not.toBeNull(); - expect(cmp.card.inlineDeleteComponent?.length).toBe(1); - expect(cmp.card.inlineDeleteComponent?.first.type).toBe( - SkyInlineDeleteType.Card - ); - }); - })); - - it('should set the inline delete to the card style when dynamically added', async(() => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(el.querySelector('sky-inline-delete')).toBeNull(); - expect(cmp.card.inlineDeleteComponent?.length).toBe(0); - - cmp.showDelete = true; - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(el.querySelector('sky-inline-delete')).not.toBeNull(); - expect(cmp.card.inlineDeleteComponent?.length).toBe(1); - expect(cmp.card.inlineDeleteComponent?.first.type).toBe( - SkyInlineDeleteType.Card - ); - }); - }); - })); + await fixture.whenStable(); + expect(el.querySelector('sky-inline-delete')).not.toBeNull(); + expect(cmp.card.inlineDeleteComponent?.length).toBe(1); + expect(cmp.card.inlineDeleteComponent?.first.type).toBe( + SkyInlineDeleteType.Card + ); + }); + + it('should set the inline delete to the card style when dynamically added', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(el.querySelector('sky-inline-delete')).toBeNull(); + expect(cmp.card.inlineDeleteComponent?.length).toBe(0); + + cmp.showDelete = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(el.querySelector('sky-inline-delete')).not.toBeNull(); + expect(cmp.card.inlineDeleteComponent?.length).toBe(1); + expect(cmp.card.inlineDeleteComponent?.first.type).toBe( + SkyInlineDeleteType.Card + ); + }); }); diff --git a/libs/components/layout/src/lib/modules/definition-list/definition-list.component.spec.ts b/libs/components/layout/src/lib/modules/definition-list/definition-list.component.spec.ts index ebb24b83d9..d3772a6526 100644 --- a/libs/components/layout/src/lib/modules/definition-list/definition-list.component.spec.ts +++ b/libs/components/layout/src/lib/modules/definition-list/definition-list.component.spec.ts @@ -1,5 +1,5 @@ -import { TestBed, async } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { TestBed } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyDefinitionListFixturesModule } from './fixtures/definition-list-fixtures.module'; import { SkyDefinitionListTestComponent } from './fixtures/definition-list.component.fixture'; @@ -136,11 +136,10 @@ describe('Definition list component', () => { expect(getComputedStyle(labelEls[0]).width).toBe('150px'); }); - it('should be accessible', async(() => { + it('should be accessible', async () => { const fixture = TestBed.createComponent(SkyDefinitionListTestComponent); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/layout/src/lib/modules/inline-delete/inline-delete.component.spec.ts b/libs/components/layout/src/lib/modules/inline-delete/inline-delete.component.spec.ts index eb136071ca..9e8d265b1c 100644 --- a/libs/components/layout/src/lib/modules/inline-delete/inline-delete.component.spec.ts +++ b/libs/components/layout/src/lib/modules/inline-delete/inline-delete.component.spec.ts @@ -1,11 +1,10 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyInlineDeleteFixturesModule } from './fixtures/inline-delete-fixtures.module'; import { InlineDeleteTestComponent } from './fixtures/inline-delete.component.fixture'; @@ -94,14 +93,11 @@ describe('Inline delete component', () => { })); describe('focus handling', () => { - it('should focus the delete button on load', async(() => { + it('should focus the delete button on load', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(document.activeElement).toBe( - el.querySelector('.sky-btn-danger') - ); - }); - })); + await fixture.whenStable(); + expect(document.activeElement).toBe(el.querySelector('.sky-btn-danger')); + }); it('should skip items that are under the overlay when tabbing forward', fakeAsync(() => { fixture.componentInstance.showExtraButtons = true; @@ -187,33 +183,25 @@ describe('Inline delete component', () => { cmp.showCoveredButtons = false; }); - it('should be accessible in standard mode', async(() => { + it('should be accessible in standard mode', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible in card mode', async(() => { + it('should be accessible in card mode', async () => { fixture.detectChanges(); cmp.inlineDelete.setType(SkyInlineDeleteType.Card); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); - it('should be accessible when pending', async(() => { + it('should be accessible when pending', async () => { cmp.pending = true; fixture.detectChanges(); - fixture.whenStable().then(() => { - // NOTE: For some reason the color contrast rule fails on IE and Edge but passes all other - // browsers. A manual test was done and nothing is different in these browsers so I am just - // disabling the color contrast rule for this test for now. - expect(fixture.nativeElement).toBeAccessible(() => {}, { - rules: { 'color-contrast': { enabled: false } }, - }); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); }); diff --git a/libs/components/layout/src/lib/modules/page-summary/page-summary.component.spec.ts b/libs/components/layout/src/lib/modules/page-summary/page-summary.component.spec.ts index ea0e309063..62f6e1cc83 100644 --- a/libs/components/layout/src/lib/modules/page-summary/page-summary.component.spec.ts +++ b/libs/components/layout/src/lib/modules/page-summary/page-summary.component.spec.ts @@ -1,5 +1,5 @@ -import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyMediaBreakpoints, SkyMediaQueryService } from '@skyux/core'; import { MockSkyMediaQueryService } from '@skyux/core/testing'; @@ -78,7 +78,7 @@ describe('Page summary component', () => { expect(el.querySelector('.sky-page-summary-with-key-info')).not.toExist(); })); - it('should be accessible', async(() => { + it('should be accessible', async () => { const mockQueryService = new MockSkyMediaQueryService(); const fixture = TestBed.overrideComponent(SkyPageSummaryComponent, { @@ -93,8 +93,7 @@ describe('Page summary component', () => { }).createComponent(SkyPageSummaryTestComponent); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/modals/src/lib/modules/confirm/confirm.component.spec.ts b/libs/components/modals/src/lib/modules/confirm/confirm.component.spec.ts index f56be5a3c7..8de35203aa 100644 --- a/libs/components/modals/src/lib/modules/confirm/confirm.component.spec.ts +++ b/libs/components/modals/src/lib/modules/confirm/confirm.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { expect } from '@skyux-sdk/testing'; import { SkyModalConfiguration } from '../modal/modal-configuration'; @@ -45,7 +45,7 @@ describe('Confirm component', () => { }); }); - it('should display an OK confirm by default', async(() => { + it('should display an OK confirm by default', () => { const fixture = createConfirm({ message: 'confirm message', }); @@ -63,9 +63,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText('OK'); buttons[0].click(); - })); + }); - it('should display an OK confirm', async(() => { + it('should display an OK confirm', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.OK, @@ -84,9 +84,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText('OK'); buttons[0].click(); - })); + }); - it('should display an OK confirm with body', async(() => { + it('should display an OK confirm with body', () => { const fixture = createConfirm({ message: 'confirm message', body: 'additional text', @@ -108,9 +108,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText('OK'); buttons[0].click(); - })); + }); - it('should display a YesCancel confirm', async(() => { + it('should display a YesCancel confirm', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.YesCancel, @@ -130,9 +130,9 @@ describe('Confirm component', () => { expect(buttons[0]).toHaveText('Yes'); expect(buttons[1]).toHaveText('Cancel'); buttons[0].click(); - })); + }); - it('should display a YesNoCancel confirm', async(() => { + it('should display a YesNoCancel confirm', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.YesNoCancel, @@ -153,9 +153,9 @@ describe('Confirm component', () => { expect(buttons[1]).toHaveText('No'); expect(buttons[2]).toHaveText('Cancel'); buttons[0].click(); - })); + }); - it('should display a custom confirm', async(() => { + it('should display a custom confirm', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.Custom, @@ -180,9 +180,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText('Custom label'); buttons[0].click(); - })); + }); - it('should handle incorrect button config', async(() => { + it('should handle incorrect button config', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.Custom, @@ -207,9 +207,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText(''); buttons[0].click(); - })); + }); - it('should default to OK confirm if buttons not provided with custom type', async(() => { + it('should default to OK confirm if buttons not provided with custom type', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.Custom, @@ -229,9 +229,9 @@ describe('Confirm component', () => { expect(buttons.length).toEqual(1); expect(buttons[0]).toHaveText('OK'); buttons[0].click(); - })); + }); - it('should invoke close method and return arguments', async(() => { + it('should invoke close method and return arguments', () => { const fixture = createConfirm({ message: 'confirm message', }); @@ -249,9 +249,9 @@ describe('Confirm component', () => { expect(spy).toHaveBeenCalledWith({ action: 'ok', }); - })); + }); - it('should autofocus specified button from config', async(() => { + it('should autofocus specified button from config', () => { const fixture = createConfirm({ message: 'confirm message', type: SkyConfirmType.Custom, @@ -282,9 +282,9 @@ describe('Confirm component', () => { expect(buttons[1].hasAttribute('autofocus')).toEqual(false); expect(buttons[2].hasAttribute('autofocus')).toEqual(true); buttons[0].click(); - })); + }); - it('should default to not preserving white space', async(() => { + it('should default to not preserving white space', () => { const fixture = createConfirm({ message: 'confirm message', body: 'additional text', @@ -310,9 +310,9 @@ describe('Confirm component', () => { ); buttons[0].click(); - })); + }); - it('should allow preserving white space', async(() => { + it('should allow preserving white space', () => { const fixture = createConfirm({ message: 'confirm message', body: 'additional text', @@ -335,5 +335,5 @@ describe('Confirm component', () => { expect(bodyElem.classList).toContain('sky-confirm-preserve-white-space'); buttons[0].click(); - })); + }); }); diff --git a/libs/components/popovers/src/lib/modules/dropdown/dropdown.component.spec.ts b/libs/components/popovers/src/lib/modules/dropdown/dropdown.component.spec.ts index 2eeae6d58c..54d31be466 100644 --- a/libs/components/popovers/src/lib/modules/dropdown/dropdown.component.spec.ts +++ b/libs/components/popovers/src/lib/modules/dropdown/dropdown.component.spec.ts @@ -1,12 +1,11 @@ import { ComponentFixture, TestBed, - async, fakeAsync, inject, tick, } from '@angular/core/testing'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyAffixConfig, SkyAffixService, SkyAffixer } from '@skyux/core'; import { SkyTheme, @@ -1217,42 +1216,38 @@ describe('Dropdown component', function () { expect(button?.getAttribute('title')).toEqual('dropdown-title-override'); })); - it('should be accessible when closed', async(() => { + it('should be accessible when closed', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(window.document.body).toBeAccessible(() => {}, { - rules: { - region: { - enabled: false, - }, - }, - }); - }); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(window.document.body).toBeAccessible({ + rules: { + region: { + enabled: false, + }, + }, }); - })); + }); - it('should be accessible when open', async(() => { + it('should be accessible when open', async () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - const button = getButtonElement(); - - button?.click(); - - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(window.document.body).toBeAccessible(() => {}, { - rules: { - region: { - enabled: false, - }, - }, - }); - }); + await fixture.whenStable(); + const button = getButtonElement(); + + button?.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + await expectAsync(window.document.body).toBeAccessible({ + rules: { + region: { + enabled: false, + }, + }, }); - })); + }); }); }); diff --git a/libs/components/popovers/testing/src/popover/popover-fixture.spec.ts b/libs/components/popovers/testing/src/popover/popover-fixture.spec.ts index f4922bd1d4..b4c28dee13 100644 --- a/libs/components/popovers/testing/src/popover/popover-fixture.spec.ts +++ b/libs/components/popovers/testing/src/popover/popover-fixture.spec.ts @@ -48,16 +48,15 @@ describe('Popover fixture', () => { return document.querySelector('.sky-btn'); } - function openPopover(): Promise { + async function openPopover(): Promise { expect(popoverFixture.popoverIsVisible).toEqual(false); const triggerEl = getPopoverTriggerEl(); triggerEl?.click(); fixture.detectChanges(); - return fixture.whenStable().then(() => { - expect(popoverFixture.popoverIsVisible).toEqual(true); - }); + await fixture.whenStable(); + expect(popoverFixture.popoverIsVisible).toEqual(true); } //#endregion diff --git a/libs/components/router/src/lib/modules/href/href.directive.spec.ts b/libs/components/router/src/lib/modules/href/href.directive.spec.ts index a8c902b54b..b0022edb19 100644 --- a/libs/components/router/src/lib/modules/href/href.directive.spec.ts +++ b/libs/components/router/src/lib/modules/href/href.directive.spec.ts @@ -239,11 +239,9 @@ describe('SkyHref Directive', () => { fixture.componentInstance.dynamicLink = 'nope://simple-app/example/page'; fixture.detectChanges(); - fixture.whenStable().then(() => { - const element = fixture.nativeElement.querySelector('.dynamicLink a'); - expect(element.hidden).toBeFalse(); - expect(element.getAttribute('href')).toBeNull(); - }); + const element = fixture.nativeElement.querySelector('.dynamicLink a'); + expect(element.hidden).toBeFalse(); + expect(element.getAttribute('href')).toBeNull(); })); it('should handle link without protocol', fakeAsync(() => { @@ -251,10 +249,9 @@ describe('SkyHref Directive', () => { fixture.componentInstance.dynamicLink = '/example/page'; fixture.detectChanges(); - fixture.whenStable().then(() => { - const element = fixture.nativeElement.querySelector('.dynamicLink a'); - expect(element.hidden).toBeFalse(); - }); + tick(); + const element = fixture.nativeElement.querySelector('.dynamicLink a'); + expect(element.hidden).toBeFalse(); })); it('should handle link with a fragment', fakeAsync(() => { @@ -300,8 +297,6 @@ describe('SkyHref Directive', () => { const element = fixture.nativeElement.querySelector('.slowLink a'); expect(element.hidden).toBe(true); flush(); - fixture.whenStable().then(() => { - expect(element.hidden).toBe(false); - }); + expect(element.hidden).toBe(false); })); }); diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts b/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts index 7b1c55a7b1..7e80057386 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts @@ -3,7 +3,6 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; @@ -654,13 +653,13 @@ describe('Tabset component', () => { expect(tabEl).not.toBeNull(); })); - it('should be accessible', async(async () => { + it('should be accessible', async () => { const fixture = TestBed.createComponent(TabsetTestComponent); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); await expectAsync(fixture.nativeElement).toBeAccessible(); - })); + }); describe('when collapsed', () => { let fixture: ComponentFixture; @@ -864,12 +863,12 @@ describe('Tabset component', () => { expect(closeSpy).toHaveBeenCalled(); })); - it('should be accessible', async(async () => { + it('should be accessible', async () => { fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); await expectAsync(fixture.nativeElement).toBeAccessible(); - })); + }); }); describe('active state on tabset', () => { @@ -1158,13 +1157,13 @@ describe('Tabset component', () => { validateTabSelected(fixture.nativeElement, 0); })); - it('should be accessible', async(async () => { + it('should be accessible', async () => { const fixture = TestBed.createComponent(TabsetActiveTestComponent); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); await expectAsync(fixture.nativeElement).toBeAccessible(); - })); + }); }); describe('general accessibility', () => { 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 6074836eed..f7fc02a692 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 @@ -86,22 +86,18 @@ describe('Tile component', () => { tick(); fixture.detectChanges(); - fixture.whenStable().then(() => { - const titleEl = el.querySelector('.sky-tile-title'); + const titleEl = el.querySelector('.sky-tile-title'); - titleEl.click(); - fixture.detectChanges(); + titleEl.click(); + fixture.detectChanges(); - const contentAttrs = el.querySelector('.sky-tile-content').attributes; + const contentAttrs = el.querySelector('.sky-tile-content').attributes; - expect(contentAttrs['hidden']).not.toBeNull(); + expect(contentAttrs['hidden']).not.toBeNull(); - titleEl.click(); + titleEl.click(); - fixture.whenStable().then(() => { - expect(contentAttrs['hidden']).toBe(undefined); - }); - }); + expect(contentAttrs['hidden']).toBe(undefined); })); it('should output state when collapsed/expanded', fakeAsync(() => { diff --git a/libs/components/toast/src/lib/modules/toast/toast.component.spec.ts b/libs/components/toast/src/lib/modules/toast/toast.component.spec.ts index 0c4a6144ff..a2c1dda6ec 100644 --- a/libs/components/toast/src/lib/modules/toast/toast.component.spec.ts +++ b/libs/components/toast/src/lib/modules/toast/toast.component.spec.ts @@ -1,11 +1,10 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; -import { expect } from '@skyux-sdk/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyToastFixturesModule } from './fixtures/toast-fixtures.module'; import { SkyToastWithToasterServiceTestComponent } from './fixtures/toast-with-toaster-service.component.fixture'; @@ -115,13 +114,12 @@ describe('Toast component', () => { expect(component.toastComponent?.ariaRole).toEqual('alert'); }); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { setupTest(); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); describe('auto-close option', () => { function waitForAutoClose() { diff --git a/libs/components/validation/src/lib/modules/email-validation/email-validation.directive.spec.ts b/libs/components/validation/src/lib/modules/email-validation/email-validation.directive.spec.ts index fc63854e52..d92b3ce690 100644 --- a/libs/components/validation/src/lib/modules/email-validation/email-validation.directive.spec.ts +++ b/libs/components/validation/src/lib/modules/email-validation/email-validation.directive.spec.ts @@ -1,13 +1,12 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { FormsModule, NgModel } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { expect } from '@skyux-sdk/testing'; +import { expect, expectAsync } from '@skyux-sdk/testing'; import { SkyEmailValidationFixturesModule } from './fixtures/email-validation-fixtures.module'; import { EmailValidationTestComponent } from './fixtures/email-validation.component.fixture'; @@ -110,11 +109,10 @@ describe('Email validation', () => { expect(ngModel.control.touched).toBe(false); })); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { setInput('[]awefhawenfc0293ejwf]', fixture); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); diff --git a/libs/components/validation/src/lib/modules/url-validation/url-validation.directive.spec.ts b/libs/components/validation/src/lib/modules/url-validation/url-validation.directive.spec.ts index d8471a7ccf..52be03671a 100644 --- a/libs/components/validation/src/lib/modules/url-validation/url-validation.directive.spec.ts +++ b/libs/components/validation/src/lib/modules/url-validation/url-validation.directive.spec.ts @@ -1,13 +1,12 @@ import { ComponentFixture, TestBed, - async, fakeAsync, tick, } from '@angular/core/testing'; import { FormsModule, NgModel } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { SkyAppTestUtility, expect } from '@skyux-sdk/testing'; +import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; import { SkyUrlValidationFixturesModule } from './fixtures/url-validation-fixtures.module'; import { UrlValidationRulesetTestComponent } from './fixtures/url-validation-ruleset.component.fixture'; @@ -107,14 +106,13 @@ describe('URL validation via directive - ruleset v1 (implicit)', () => { expect(ngModel.control.touched).toBe(false); })); - it('should pass accessibility', async(() => { + it('should pass accessibility', async () => { fixture.detectChanges(); setInput('[]awefhawenfc0293ejwf]', fixture); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); describe('URL validation via directive - ruleset v1 (explicit)', () => { @@ -262,15 +260,15 @@ describe('URL validation via directive - ruleset v2', () => { expect(ngModel.control.touched).toBe(false); })); - it('should pass accessibility using ruleset version 2', async(() => { + it('should pass accessibility using ruleset version 2', async () => { fixture.detectChanges(); setInput('[]awefhawenfc0293ejwf]', fixture); fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement).toBeAccessible(); - }); - })); + await fixture.whenStable(); + await expectAsync(fixture.nativeElement).toBeAccessible(); + }); }); + describe('URL validation via directive - non-onceability', () => { function setInput(text: string, compFixture: ComponentFixture) { const params = { From 784f26a6fa770ba440d776de4b2e952aec118ba8 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Tue, 11 Oct 2022 10:25:11 -0400 Subject: [PATCH 05/30] chore(components/lists): paging strict mode support (#653) --- .../fixtures/paging.component.fixture.ts | 8 +- .../paging/fixtures/paging.module.fixture.ts | 15 +++ .../modules/paging/paging.component.spec.ts | 127 ++++++++++-------- .../lib/modules/paging/paging.component.ts | 14 +- .../testing/src/paging/paging-fixture.spec.ts | 3 +- .../testing/src/paging/paging-fixture.ts | 2 +- 6 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 libs/components/lists/src/lib/modules/paging/fixtures/paging.module.fixture.ts diff --git a/libs/components/lists/src/lib/modules/paging/fixtures/paging.component.fixture.ts b/libs/components/lists/src/lib/modules/paging/fixtures/paging.component.fixture.ts index 1535c26259..7eeb53eb6d 100644 --- a/libs/components/lists/src/lib/modules/paging/fixtures/paging.component.fixture.ts +++ b/libs/components/lists/src/lib/modules/paging/fixtures/paging.component.fixture.ts @@ -6,19 +6,19 @@ import { SkyPagingComponent } from '../paging.component'; selector: 'sky-test-cmp', templateUrl: './paging.component.fixture.html', }) -export class PagingTestComponent { +export class SkyPagingTestComponent { @ViewChild(SkyPagingComponent, { read: SkyPagingComponent, static: true, }) - public pagingComponent: SkyPagingComponent; + public pagingComponent!: SkyPagingComponent; public pageSize = 2; public maxPages = 3; public currentPage = 1; public itemCount = 8; - public label: string; - public currentPageChanged(currentPage: number) { + public label: string | undefined; + public currentPageChanged(currentPage: number): void { this.currentPage = currentPage; } } diff --git a/libs/components/lists/src/lib/modules/paging/fixtures/paging.module.fixture.ts b/libs/components/lists/src/lib/modules/paging/fixtures/paging.module.fixture.ts new file mode 100644 index 0000000000..50cf805f58 --- /dev/null +++ b/libs/components/lists/src/lib/modules/paging/fixtures/paging.module.fixture.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SkyThemeService } from '@skyux/theme'; + +import { SkyPagingModule } from '../paging.module'; + +import { SkyPagingTestComponent } from './paging.component.fixture'; + +@NgModule({ + declarations: [SkyPagingTestComponent], + imports: [CommonModule, SkyPagingModule], + exports: [SkyPagingTestComponent], + providers: [SkyThemeService], +}) +export class SkyPagingFixturesModule {} diff --git a/libs/components/lists/src/lib/modules/paging/paging.component.spec.ts b/libs/components/lists/src/lib/modules/paging/paging.component.spec.ts index f0dbaca4f7..b365fbc445 100644 --- a/libs/components/lists/src/lib/modules/paging/paging.component.spec.ts +++ b/libs/components/lists/src/lib/modules/paging/paging.component.spec.ts @@ -2,27 +2,24 @@ import { DebugElement } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { expect, expectAsync } from '@skyux-sdk/testing'; -import { SkyThemeService } from '@skyux/theme'; -import { PagingTestComponent } from './fixtures/paging.component.fixture'; -import { SkyPagingModule } from './paging.module'; +import { SkyPagingTestComponent } from './fixtures/paging.component.fixture'; +import { SkyPagingFixturesModule } from './fixtures/paging.module.fixture'; describe('Paging component', () => { - let component: PagingTestComponent, fixture: any, element: DebugElement; + let component: SkyPagingTestComponent, fixture: any, element: DebugElement; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [PagingTestComponent], - imports: [SkyPagingModule], - providers: [SkyThemeService], + imports: [SkyPagingFixturesModule], }); - fixture = TestBed.createComponent(PagingTestComponent); + fixture = TestBed.createComponent(SkyPagingTestComponent); element = fixture.debugElement as DebugElement; component = fixture.componentInstance; fixture.detectChanges(); }); - function getPagingSelector(type: string) { + function getPagingSelector(type: string): string { if (type === 'next' || type === 'previous') { return '.sky-paging-btn[sky-cmp-id="' + type + '"]'; } else { @@ -30,11 +27,17 @@ describe('Paging component', () => { } } - function verifyDisabled(elem: DebugElement) { + function getActivePageNumbers(): number[] { + return element + .queryAll(By.css('.sky-list-paging-link')) + .map((el) => parseInt(el.nativeElement.innerText)); + } + + function verifyDisabled(elem: DebugElement): void { expect(elem.nativeElement.disabled).toBeTruthy(); } - function verifyEnabled(elem: DebugElement) { + function verifyEnabled(elem: DebugElement): void { expect(elem.nativeElement.disabled).toBeFalsy(); } @@ -261,88 +264,94 @@ describe('Paging component', () => { }); it('should show the correct pages for an even number of maximum pages', () => { - let pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 6, 1); + component.itemCount = 16; + component.maxPages = 6; + fixture.detectChanges(); + + let pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 6, 2); + component.currentPage = 2; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 6, 4); + component.currentPage = 4; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 6, 7); + component.currentPage = 7; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([3, 4, 5, 6, 7, 8]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 6, 8); + component.currentPage = 8; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([3, 4, 5, 6, 7, 8]); }); it('should show the correct pages for an odd number of maximum pages', () => { - let pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 5, 1); + component.itemCount = 16; + component.maxPages = 5; + fixture.detectChanges(); + + let pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 5, 2); + component.currentPage = 2; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 5, 4); + component.currentPage = 4; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 5, 7); + component.currentPage = 7; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([4, 5, 6, 7, 8]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(8, 5, 8); + component.currentPage = 8; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([4, 5, 6, 7, 8]); }); it('should show the correct pages when maximum pages are >= the page count', () => { - let pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 6, 1); + component.itemCount = 12; + component.maxPages = 6; + fixture.detectChanges(); + + let pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 6, 3); + component.currentPage = 3; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 6, 6); + component.currentPage = 6; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 8, 1); + component.maxPages = 8; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 8, 3); + component.currentPage = 1; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); - pageNumbers = ( - component.pagingComponent as any - ).getDisplayedPageNumbers(6, 8, 6); + component.currentPage = 3; + fixture.detectChanges(); + pageNumbers = getActivePageNumbers(); expect(pageNumbers).toEqual([1, 2, 3, 4, 5, 6]); }); diff --git a/libs/components/lists/src/lib/modules/paging/paging.component.ts b/libs/components/lists/src/lib/modules/paging/paging.component.ts index 3a84e38eca..7c2e33c473 100644 --- a/libs/components/lists/src/lib/modules/paging/paging.component.ts +++ b/libs/components/lists/src/lib/modules/paging/paging.component.ts @@ -47,7 +47,7 @@ export class SkyPagingComponent implements OnChanges { * @default "Pagination" */ @Input() - public pagingLabel: string; + public pagingLabel: string | undefined; /** * Fires when the current page changes and emits the new current page. @@ -66,7 +66,7 @@ export class SkyPagingComponent implements OnChanges { public setPage(pageNumber: number): void { const previousPage = this.currentPage; - this.setPageCount(); + this.#setPageCount(); if (pageNumber < 1 || this.pageCount < 1) { this.currentPage = 1; @@ -76,7 +76,7 @@ export class SkyPagingComponent implements OnChanges { this.currentPage = pageNumber; } - this.setDisplayedPages(); + this.#setDisplayedPages(); if (previousPage !== this.currentPage) { this.currentPageChange.emit(this.currentPage); @@ -99,7 +99,7 @@ export class SkyPagingComponent implements OnChanges { return this.currentPage === this.pageCount; } - private getDisplayedPageNumbers( + #getDisplayedPageNumbers( pageCount: number, maxDisplayedPages: number, pageNumber: number @@ -137,7 +137,7 @@ export class SkyPagingComponent implements OnChanges { return displayedPageNumbers; } - private setPageCount(): void { + #setPageCount(): void { if (this.itemCount === 0 || this.pageSize === 0) { this.pageCount = 0; return; @@ -146,8 +146,8 @@ export class SkyPagingComponent implements OnChanges { this.pageCount = Math.ceil(this.itemCount / this.pageSize); } - private setDisplayedPages(): void { - this.displayedPages = this.getDisplayedPageNumbers( + #setDisplayedPages(): void { + this.displayedPages = this.#getDisplayedPageNumbers( this.pageCount, this.maxPages, this.currentPage diff --git a/libs/components/lists/testing/src/paging/paging-fixture.spec.ts b/libs/components/lists/testing/src/paging/paging-fixture.spec.ts index 7abb0f7744..3f0ca0fdf9 100644 --- a/libs/components/lists/testing/src/paging/paging-fixture.spec.ts +++ b/libs/components/lists/testing/src/paging/paging-fixture.spec.ts @@ -126,11 +126,10 @@ describe('Paging fixture', () => { await fixture.whenStable(); // verify active page - expect(pagingFixture.activePageId).toBeUndefined(); + expect(pagingFixture.activePageId).toBe(''); // verify page list expect(pagingFixture.pageLinks.length).toBe(0); - expect(pagingFixture.activePageId).toBeUndefined(); // actions should not fail await pagingFixture.selectNextPage(); diff --git a/libs/components/lists/testing/src/paging/paging-fixture.ts b/libs/components/lists/testing/src/paging/paging-fixture.ts index 2c17a0803c..fc2d45661d 100644 --- a/libs/components/lists/testing/src/paging/paging-fixture.ts +++ b/libs/components/lists/testing/src/paging/paging-fixture.ts @@ -115,7 +115,7 @@ export class SkyPagingFixture { } private getPageId(page: HTMLButtonElement): string { - return page?.getAttribute('sky-cmp-id'); + return page?.getAttribute('sky-cmp-id') ?? ''; } private async waitForComponentInitialization(): Promise { From 817e3324911da77e5f61aed29c32937480cdb6c1 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Tue, 11 Oct 2022 13:17:24 -0400 Subject: [PATCH 06/30] chore(changelog): add release notes from 6.24.0 (#658) Co-authored-by: Blackbaud Sky Build User --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc65f88c9..cc88e65105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.24.0](https://github.com/blackbaud/skyux/compare/6.23.3...6.24.0) (2022-10-10) + + +### Features + +* **components/angular-tree-component:** add inline help support for angular tree component ([#631](https://github.com/blackbaud/skyux/issues/631)) ([8674852](https://github.com/blackbaud/skyux/commit/86748522fc65f59830850303ed1839368e0e3317)) +* **components/tabs:** add descriptive aria label to tab buttons ([#586](https://github.com/blackbaud/skyux/issues/586)) ([f827ca0](https://github.com/blackbaud/skyux/commit/f827ca0cde063303fa525b4c01510ba8abe663d8)) + ## [7.0.0-beta.2](https://github.com/blackbaud/skyux/compare/7.0.0-beta.1...7.0.0-beta.2) (2022-10-07) From 1c6eb704dfd14d7c76113068b9c7668792d26bb8 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Tue, 11 Oct 2022 13:42:05 -0400 Subject: [PATCH 07/30] chore: update release please 'release-as' to 7.0.0-beta.3 (#643) --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5524e6c368..7ec177ff85 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -17,7 +17,7 @@ jobs: package-name: 'skyux' pull-request-title-pattern: 'chore: release ${version}' labels: 'autorelease ${{ github.ref_name }}: pending' - release-as: 7.0.0-beta.2 + release-as: 7.0.0-beta.3 release-labels: 'autorelease ${{ github.ref_name }}: tagged' prerelease: true draft-pull-request: true From 3fbabf28cb406a220aa4d7dbfe282b8a81e6365a Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Tue, 11 Oct 2022 13:50:33 -0400 Subject: [PATCH 08/30] feat(components/angular-tree-component): add inline help support for angular tree component (#659) Co-authored-by: Sandhya --- .../src/app/app-routing.module.ts | 7 ++++ apps/code-examples/src/app/app.component.html | 7 ++++ .../advanced/angular-tree-demo.component.html | 4 +- .../advanced/angular-tree-demo.component.scss | 8 ++++ .../advanced/angular-tree-demo.component.ts | 13 +----- .../angular-tree-demo.component.html | 21 ++++++++++ .../angular-tree-demo.component.scss | 8 ++++ .../angular-tree-demo.component.ts | 41 +++++++++++++++++++ .../inline-help/angular-tree-demo.module.ts | 28 +++++++++++++ .../src/app/features/angular-tree.modules.ts | 33 +++++++++++++++ .../angular-tree-node.component.html | 5 ++- 11 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.scss create mode 100644 apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.html create mode 100644 apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.scss create mode 100644 apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.ts create mode 100644 apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.module.ts create mode 100644 apps/code-examples/src/app/features/angular-tree.modules.ts diff --git a/apps/code-examples/src/app/app-routing.module.ts b/apps/code-examples/src/app/app-routing.module.ts index cfd62228c1..d6a07280f3 100644 --- a/apps/code-examples/src/app/app-routing.module.ts +++ b/apps/code-examples/src/app/app-routing.module.ts @@ -7,6 +7,13 @@ const routes: Routes = [ loadChildren: () => import('./features/action-bars.module').then((m) => m.ActionBarsModule), }, + { + path: 'angular-tree', + loadChildren: () => + import('./features/angular-tree.modules').then( + (m) => m.AngularTreeFeatureModule + ), + }, { path: 'colorpicker', loadChildren: () => diff --git a/apps/code-examples/src/app/app.component.html b/apps/code-examples/src/app/app.component.html index 6eca08535e..e08e0ef42e 100644 --- a/apps/code-examples/src/app/app.component.html +++ b/apps/code-examples/src/app/app.component.html @@ -53,6 +53,13 @@ +
  • + Angular Tree + +
  • Autonumeric
      diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.html b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.html index f60203e93b..b9790f36f9 100644 --- a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.html +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.html @@ -3,7 +3,7 @@
      - +

      Modes

        @@ -83,7 +83,7 @@

        Modes

      - +
      • diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.scss b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.scss new file mode 100644 index 0000000000..b080c4be6a --- /dev/null +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.scss @@ -0,0 +1,8 @@ +.app-demo-container { + border: 1px solid #cdcfd2; + padding: 20px; + + .angular-tree-component { + background: #fff; + } +} diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.ts b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.ts index 7b57ce20d7..ed7e580236 100644 --- a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.ts +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component.ts @@ -15,18 +15,7 @@ import { @Component({ selector: 'app-angular-tree-component-demo', - styles: [ - ` - .app-demo-container { - border: 1px solid #cdcfd2; - padding: 20px; - - .angular-tree-component { - background: #fff; - } - } - `, - ], + styleUrls: ['./angular-tree-demo.component.scss'], templateUrl: './angular-tree-demo.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.html b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.html new file mode 100644 index 0000000000..b688f5219b --- /dev/null +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.html @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.scss b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.scss new file mode 100644 index 0000000000..b080c4be6a --- /dev/null +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.scss @@ -0,0 +1,8 @@ +.app-demo-container { + border: 1px solid #cdcfd2; + padding: 20px; + + .angular-tree-component { + background: #fff; + } +} diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.ts b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.ts new file mode 100644 index 0000000000..aa61d30823 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component.ts @@ -0,0 +1,41 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-angular-tree-component-demo', + styleUrls: ['./angular-tree-demo.component.scss'], + templateUrl: './angular-tree-demo.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AngularTreeDemoComponent { + public nodes = [ + { + name: 'Animals', + isExpanded: true, + showHelp: true, + children: [ + { + name: 'Cats', + isExpanded: true, + showHelp: true, + children: [ + { name: 'Burmese' }, + { name: 'Persian' }, + { name: 'Tabby' }, + ], + }, + { + name: 'Dogs', + isExpanded: true, + children: [ + { + name: 'Beagle', + showHelp: true, + }, + { name: 'German shepherd' }, + { name: 'Labrador retriever' }, + ], + }, + ], + }, + ]; +} diff --git a/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.module.ts b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.module.ts new file mode 100644 index 0000000000..97b196b632 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TreeModule } from '@circlon/angular-tree-component'; +import { SkyAngularTreeModule } from '@skyux/angular-tree-component'; +import { SkyCheckboxModule, SkyRadioModule } from '@skyux/forms'; +import { SkyHelpInlineModule } from '@skyux/indicators'; +import { SkyFluidGridModule } from '@skyux/layout'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { AngularTreeDemoComponent } from './angular-tree-demo.component'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + SkyAngularTreeModule, + SkyCheckboxModule, + SkyDropdownModule, + SkyFluidGridModule, + SkyRadioModule, + TreeModule, + SkyHelpInlineModule, + ], + declarations: [AngularTreeDemoComponent], + exports: [AngularTreeDemoComponent], +}) +export class AngularTreeDemoModule {} diff --git a/apps/code-examples/src/app/features/angular-tree.modules.ts b/apps/code-examples/src/app/features/angular-tree.modules.ts new file mode 100644 index 0000000000..0aa953b34a --- /dev/null +++ b/apps/code-examples/src/app/features/angular-tree.modules.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AngularTreeDemoComponent } from '../code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.component'; +import { AngularTreeDemoModule } from '../code-examples/angular-tree-component/angular-tree/advanced/angular-tree-demo.module'; +import { AngularTreeDemoComponent as AngularTreeInlineHelpDemoComponent } from '../code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.component'; +import { AngularTreeDemoModule as AngularTreeInlineHelpDemoModule } from '../code-examples/angular-tree-component/angular-tree/inline-help/angular-tree-demo.module'; + +const routes: Routes = [ + { + path: 'advanced', + component: AngularTreeDemoComponent, + }, + { + path: 'inline-help', + component: AngularTreeInlineHelpDemoComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AngularTreeRoutingModule {} + +@NgModule({ + imports: [ + AngularTreeDemoModule, + AngularTreeRoutingModule, + AngularTreeInlineHelpDemoModule, + ], +}) +export class AngularTreeFeatureModule {} diff --git a/libs/components/angular-tree-component/src/lib/modules/angular-tree/angular-tree-node.component.html b/libs/components/angular-tree-component/src/lib/modules/angular-tree/angular-tree-node.component.html index 7a2721652c..2d161e6297 100644 --- a/libs/components/angular-tree-component/src/lib/modules/angular-tree/angular-tree-node.component.html +++ b/libs/components/angular-tree-component/src/lib/modules/angular-tree/angular-tree-node.component.html @@ -92,7 +92,10 @@ [template]="templates?.treeNodeTemplate" (click)="node.mouseAction('click', $event)" > - + From 9a01d549d498a9616d16aae4e3334b878372da3e Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Tue, 11 Oct 2022 14:20:43 -0400 Subject: [PATCH 09/30] feat(components/tabs): add descriptive aria label to tab buttons (#586) (#660) Co-authored-by: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> --- .../src/assets/locales/resources_en_US.json | 16 +++++++ .../shared/sky-tabs-resources.module.ts | 8 ++++ .../lib/modules/tabs/tab-button-view-model.ts | 2 + .../modules/tabs/tab-button.component.html | 8 +++- .../lib/modules/tabs/tab-button.component.ts | 47 +++++++++++++++++-- .../lib/modules/tabs/tabset.component.html | 2 + .../lib/modules/tabs/tabset.component.spec.ts | 46 ++++++++++++++++++ .../src/lib/modules/tabs/tabset.component.ts | 4 +- .../src/lib/modules/tabs/tabset.service.ts | 2 +- 9 files changed, 129 insertions(+), 6 deletions(-) diff --git a/libs/components/tabs/src/assets/locales/resources_en_US.json b/libs/components/tabs/src/assets/locales/resources_en_US.json index 6e43ca78bd..a30e05e2ca 100644 --- a/libs/components/tabs/src/assets/locales/resources_en_US.json +++ b/libs/components/tabs/src/assets/locales/resources_en_US.json @@ -23,8 +23,24 @@ "_description": "Label for the tab open button", "message": "Open tab" }, + "skyux_tabs_sr_tab_order": { + "_description": "Text that describes which tab the user is on out of the total tabs", + "message": "Tab {0} of {1}: {2}" + }, "skyux_vertical_tabs_show_tabs_text": { "_description": "The default text for the show tabs button in mobile", "message": "Tab list" + }, + "skyux_wizard_sr_step_current": { + "_description": "Text that describes the state of a wizard step as current", + "message": "Step {0} of {1}, current: {2}" + }, + "skyux_wizard_sr_step_completed": { + "_description": "Text that describes the state of a wizard step as completed", + "message": "Step {0} of {1}, completed: {2}" + }, + "skyux_wizard_sr_step_unavailable": { + "_description": "Text that describes the state of a wizard step as unavailable", + "message": "Step {0} of {1}, unavailable: {2}" } } diff --git a/libs/components/tabs/src/lib/modules/shared/sky-tabs-resources.module.ts b/libs/components/tabs/src/lib/modules/shared/sky-tabs-resources.module.ts index a396d20d86..21e9c8fb62 100644 --- a/libs/components/tabs/src/lib/modules/shared/sky-tabs-resources.module.ts +++ b/libs/components/tabs/src/lib/modules/shared/sky-tabs-resources.module.ts @@ -22,7 +22,15 @@ const RESOURCES: { [locale: string]: SkyLibResources } = { skyux_tabs_navigator_next: { message: 'Next' }, skyux_tabs_navigator_previous: { message: 'Previous' }, skyux_tab_open: { message: 'Open tab' }, + skyux_tabs_sr_tab_order: { message: 'Tab {0} of {1}: {2}' }, skyux_vertical_tabs_show_tabs_text: { message: 'Tab list' }, + skyux_wizard_sr_step_current: { message: 'Step {0} of {1}, current: {2}' }, + skyux_wizard_sr_step_completed: { + message: 'Step {0} of {1}, completed: {2}', + }, + skyux_wizard_sr_step_unavailable: { + message: 'Step {0} of {1}, unavailable: {2}', + }, }, }; diff --git a/libs/components/tabs/src/lib/modules/tabs/tab-button-view-model.ts b/libs/components/tabs/src/lib/modules/tabs/tab-button-view-model.ts index 2e5e517451..e97ca5a4ba 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tab-button-view-model.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tab-button-view-model.ts @@ -13,4 +13,6 @@ export type TabButtonViewModel = { closeable: boolean; disabled: boolean; tabIndex: SkyTabIndex; + tabNumber: number; + totalTabsCount: number; }; diff --git a/libs/components/tabs/src/lib/modules/tabs/tab-button.component.html b/libs/components/tabs/src/lib/modules/tabs/tab-button.component.html index 0f4f8fe72e..5bb834cfc9 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tab-button.component.html +++ b/libs/components/tabs/src/lib/modules/tabs/tab-button.component.html @@ -13,6 +13,12 @@ [attr.aria-disabled]="disabled" [attr.aria-selected]="active" [attr.href]="buttonHref" + [attr.aria-label]=" + (tabStyle === 'wizard' + ? 'skyux_wizard_sr_step_' + wizardStepState + : 'skyux_tabs_sr_tab_order' + ) | skyLibResources: tabNumber:totalTabsCount:buttonText + " [id]="buttonId" [ngClass]="{ 'sky-btn-tab-wizard': tabStyle === 'wizard', @@ -25,7 +31,7 @@ (keydown)="onTabButtonKeyDown($event)" (focus)="onFocus()" > - + {{ buttonText }} {{ buttonTextCount }} diff --git a/libs/components/tabs/src/lib/modules/tabs/tab-button.component.ts b/libs/components/tabs/src/lib/modules/tabs/tab-button.component.ts index 7b463d4892..3d4133015c 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tab-button.component.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tab-button.component.ts @@ -20,6 +20,9 @@ import { SkyTabsetStyle } from './tabset-style'; import { SkyTabsetService } from './tabset.service'; const DEFAULT_ELEMENT_ROLE = 'tab'; +const DEFAULT_DISABLED = false; + +type SkyWizardStepState = 'completed' | 'current' | 'unavailable'; /** * @internal @@ -34,7 +37,13 @@ const DEFAULT_ELEMENT_ROLE = 'tab'; }) export class SkyTabButtonComponent implements AfterViewInit, OnDestroy { @Input() - public active: boolean; + public get active(): boolean { + return this.#_isActive; + } + public set active(value: boolean) { + this.#_isActive = value; + this.#updateWizardStepState(); + } @Input() public ariaControls: string; @@ -55,11 +64,24 @@ export class SkyTabButtonComponent implements AfterViewInit, OnDestroy { public closeable: boolean; @Input() - public disabled: boolean; + public get disabled(): boolean { + return this.#_isDisabled; + } + + public set disabled(value: boolean | undefined) { + this.#_isDisabled = value ?? DEFAULT_DISABLED; + this.#updateWizardStepState(); + } @Input() public tabIndex: SkyTabIndex; + @Input() + public tabNumber: number | undefined; + + @Input() + public totalTabsCount: number | undefined; + @Input() public get tabStyle(): SkyTabsetStyle { return this.#_tabStyle; @@ -68,6 +90,7 @@ export class SkyTabButtonComponent implements AfterViewInit, OnDestroy { public set tabStyle(style: SkyTabsetStyle | undefined) { this.#_tabStyle = style; this.elementRole = style === 'tabs' ? DEFAULT_ELEMENT_ROLE : undefined; + this.#updateWizardStepState(); } @Output() @@ -90,7 +113,9 @@ export class SkyTabButtonComponent implements AfterViewInit, OnDestroy { public elementRole: string | undefined = DEFAULT_ELEMENT_ROLE; public closeBtnTabIndex = '-1'; - + public wizardStepState: SkyWizardStepState | undefined; + #_isActive = false; + #_isDisabled = DEFAULT_DISABLED; #_tabStyle: SkyTabsetStyle; #adapterService: SkyTabButtonAdapterService; #changeDetectorRef: ChangeDetectorRef; @@ -155,4 +180,20 @@ export class SkyTabButtonComponent implements AfterViewInit, OnDestroy { public onFocus(): void { this.#tabsetService.setFocusedTabBtnIndex(this.tabIndex); } + + #updateWizardStepState(): void { + if (this.tabStyle === 'tabs') { + this.wizardStepState = undefined; + } else { + if (this.active) { + this.wizardStepState = 'current'; + } else if (!this.disabled) { + this.wizardStepState = 'completed'; + } else { + this.wizardStepState = 'unavailable'; + } + } + + this.#changeDetectorRef.markForCheck(); + } } diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.component.html b/libs/components/tabs/src/lib/modules/tabs/tabset.component.html index 8781b8251d..ec852019d7 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.component.html +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.component.html @@ -94,6 +94,8 @@ [disabled]="tabButton.disabled" [tabIndex]="tabButton.tabIndex" [tabStyle]="tabStyle" + [tabNumber]="tabButton.tabNumber" + [totalTabsCount]="tabButton.totalTabsCount" (buttonClick)="onTabButtonClick(tabButton)" (closeClick)="onTabCloseClick(tabButton)" > diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts b/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts index 7e80057386..31ab668e5f 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.component.spec.ts @@ -25,6 +25,7 @@ import { TabsetActiveTestComponent } from './fixtures/tabset-active.component.fi import { MockTabsetAdapterService } from './fixtures/tabset-adapter.service.mock'; import { TabsetLoopTestComponent } from './fixtures/tabset-loop.component.fixture'; import { SkyTabsetPermalinksFixtureComponent } from './fixtures/tabset-permalinks.component.fixture'; +import { SkyWizardTestFormComponent } from './fixtures/tabset-wizard.component.fixture'; import { TabsetTestComponent } from './fixtures/tabset.component.fixture'; import { SkyTabsetAdapterService } from './tabset-adapter.service'; import { SkyTabsetPermalinkService } from './tabset-permalink.service'; @@ -1620,6 +1621,51 @@ describe('Tabset component', () => { validateElFocused(tabBtn2); })); }); + + describe('button aria label', () => { + it('should indicate the current state of a wizard step', fakeAsync(() => { + const wizardFixture = TestBed.createComponent( + SkyWizardTestFormComponent + ); + + wizardFixture.detectChanges(); + tick(); + wizardFixture.detectChanges(); + tick(); + + wizardFixture.componentInstance.requiredValue1 = 'test'; + wizardFixture.componentInstance.selectedTab = 1; + wizardFixture.componentInstance.step3Disabled = true; + + wizardFixture.detectChanges(); + tick(); + wizardFixture.detectChanges(); + tick(); + + const tabBtns = wizardFixture.debugElement.queryAll( + By.css('.sky-btn-tab') + ); + const tabBtn1 = tabBtns[0]?.nativeElement; + const tabBtn2 = tabBtns[1]?.nativeElement; + const tabBtn3 = tabBtns[2]?.nativeElement; + + expect(tabBtn1?.ariaLabel).toEqual('Step 1 of 3, completed: Step 1'); + expect(tabBtn2?.ariaLabel).toEqual('Step 2 of 3, current: Step 2'); + expect(tabBtn3?.ariaLabel).toEqual('Step 3 of 3, unavailable: Step 3'); + })); + + it('should indicate the current tab number out of the total tabs', fakeAsync(() => { + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + tick(); + + const tabBtns = debugElement.queryAll(By.css('.sky-btn-tab')); + const tabBtn1 = tabBtns[0]?.nativeElement; + + expect(tabBtn1?.ariaLabel).toEqual('Tab 1 of 3: Tab 1'); + })); + }); }); describe('Permalinks', () => { diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.component.ts b/libs/components/tabs/src/lib/modules/tabs/tabset.component.ts index 0a8d1beca0..51a8b9bcc9 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.component.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.component.ts @@ -444,7 +444,7 @@ export class SkyTabsetComponent implements AfterViewInit, OnDestroy { } private createTabButtons(activeIndex: SkyTabIndex): TabButtonViewModel[] { - return this.tabs.map((tab) => ({ + return this.tabs.map((tab, index) => ({ active: this.tabsetService.tabIndexesEqual(tab.tabIndex, activeIndex), closeable: tab.closeable, ariaControls: tab.tabPanelId, @@ -459,6 +459,8 @@ export class SkyTabsetComponent implements AfterViewInit, OnDestroy { buttonTextCount: tab.tabHeaderCount, buttonText: tab.tabHeading, tabIndex: tab.tabIndex, + tabNumber: index + 1, + totalTabsCount: this.tabs.length, })); } diff --git a/libs/components/tabs/src/lib/modules/tabs/tabset.service.ts b/libs/components/tabs/src/lib/modules/tabs/tabset.service.ts index f9c559c0e2..6cc0b3c20e 100644 --- a/libs/components/tabs/src/lib/modules/tabs/tabset.service.ts +++ b/libs/components/tabs/src/lib/modules/tabs/tabset.service.ts @@ -160,7 +160,7 @@ export class SkyTabsetService { ); // Notify the tabset component when an active tab is unregistered. - if (this.isTabIndexActive(this.tabs[index].tabIndex)) { + if (this.isTabIndexActive(this.tabs[index]?.tabIndex)) { this._activeTabUnregistered.next({ arrayIndex: index, }); From 8fda84ebf9afa68e0c436578dbb6177f6cc7bfdd Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:34:14 -0400 Subject: [PATCH 10/30] feat(components/modals): remove 'string' from `SkyConfirmButton`'s `styleType` type (#664) BREAKING CHANGE: `SkyConfirmButton`'s `styleType` will only accept predefined strings of type `SkyConfirmButtonStyleType`. To address this, ensure `styleType` is only being set to a supported value. --- .../modals/src/lib/modules/confirm/confirm-button.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/components/modals/src/lib/modules/confirm/confirm-button.ts b/libs/components/modals/src/lib/modules/confirm/confirm-button.ts index a4907374a1..98ddc7d7c8 100644 --- a/libs/components/modals/src/lib/modules/confirm/confirm-button.ts +++ b/libs/components/modals/src/lib/modules/confirm/confirm-button.ts @@ -7,8 +7,7 @@ import { SkyConfirmButtonStyleType } from './confirm-button-style-type'; */ export interface SkyConfirmButton { action: SkyConfirmButtonAction; - // TODO: Remove 'string' in a breaking change. - styleType: SkyConfirmButtonStyleType | string; + styleType: SkyConfirmButtonStyleType; text: string; autofocus?: boolean; } From a65dae0642b45764fed92d9671e2830e0f1cc24e Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:35:21 -0400 Subject: [PATCH 11/30] feat(components/modals): improve `SkyModalConfigurationInterface.providers` type (#665) BREAKING CHANGE: `SkyModalConfigurationInterface.providers` accepts an array of `StaticProvider`s instead of any value. --- .../modals/src/lib/modules/modal/modal.interface.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/components/modals/src/lib/modules/modal/modal.interface.ts b/libs/components/modals/src/lib/modules/modal/modal.interface.ts index a866b10387..ce861cc2fa 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.interface.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.interface.ts @@ -1,3 +1,5 @@ +import { StaticProvider } from '@angular/core'; + // TODO: defaults won't show in the generated docs until this work is done: // https://github.com/blackbaud/skyux-docs-tools/issues/38 @@ -22,8 +24,7 @@ export interface SkyModalConfigurationInterface { * In Angular, a provider is something that can create or deliver a service. * This property can be used to pass context values from the component that launches the modal to the modal component. */ - // TODO: Change this to `StaticProvider[]` in a breaking change. - providers?: any[]; + providers?: StaticProvider[]; /** * Specifies the HTML element ID (without the leading `#`) of the element that describes From 102cd0a97a5b64c78e469b462fe1f59601e44557 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:35:49 -0400 Subject: [PATCH 12/30] feat(components/config): add more specific typing to config params function return types (#668) BREAKING CHANGE: The config params `get` function was updated to accurately reflect that it may return undefined. To address this change, account for a possible undefined value wherever you are using the `get` function. --- libs/components/config/src/lib/params.spec.ts | 5 ++--- libs/components/config/src/lib/params.ts | 14 ++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/libs/components/config/src/lib/params.spec.ts b/libs/components/config/src/lib/params.spec.ts index 2bffefc661..d26fad9007 100644 --- a/libs/components/config/src/lib/params.spec.ts +++ b/libs/components/config/src/lib/params.spec.ts @@ -137,12 +137,11 @@ describe('SkyAppRuntimeConfigParams', () => { } ); - // TODO: In a breaking change, remove the `as unknown` declarations. expect(params.get('a1')).toBe('b'); - expect(params.get('a2') as unknown).toBe(undefined); + expect(params.get('a2')).toBe(undefined); expect(params.get('a3')).toBe('d'); expect(params.get('a4')).toBe('x'); - expect(params.get('a5') as unknown).toBe(undefined); + expect(params.get('a5')).toBe(undefined); }); it('should allow default values to be overridden by the query string', () => { diff --git a/libs/components/config/src/lib/params.ts b/libs/components/config/src/lib/params.ts index a10035134d..843844670b 100644 --- a/libs/components/config/src/lib/params.ts +++ b/libs/components/config/src/lib/params.ts @@ -20,9 +20,9 @@ function getUrlSearchParams(url: string): HttpParams { } export class SkyAppRuntimeConfigParams { - #params: { [key: string]: string } = {}; + #params: Record = {}; - #defaultParamValues: { [key: string]: string } = {}; + #defaultParamValues: Record = {}; #requiredParams: string[] = []; @@ -121,22 +121,20 @@ export class SkyAppRuntimeConfigParams { * Returns the decoded value of the requested param. * @param key The parameter's key. */ - public get(key: string): string { + public get(key: string): string | undefined { if (this.has(key)) { return decodeURIComponent(this.#params[key]); } - // TODO: Return `string | undefined` in a breaking change. - return undefined as unknown as string; + return; } /** * Returns the params object. * @param excludeDefaults Exclude params that have default values */ - // TODO: Return a more specific type in a breaking change. - public getAll(excludeDefaults?: boolean): Object { - const filteredParams: { [key: string]: string } = {}; + public getAll(excludeDefaults?: boolean): Record { + const filteredParams: Record = {}; this.getAllKeys().forEach((key) => { if ( From 95b7ab59f6352a591dcff17da5d76c3e9c4d3325 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:36:32 -0400 Subject: [PATCH 13/30] feat(components/forms): update file attachment validateFn inputs to more specific type (#669) BREAKING CHANGE: The `SkyFileDrop` and `SkyFileAttachment` components' `validateFn` input type was updated to receive a `SkyFileType` parameter and return a string or undefined. To address this, ensure all `validateFn` inputs have the correct parameter and return types. --- .../file-attachment/file-attachment.component.spec.ts | 2 +- .../lib/modules/file-attachment/file-attachment.component.ts | 4 ++-- .../lib/modules/file-attachment/file-attachment.service.ts | 3 ++- .../lib/modules/file-attachment/file-drop.component.spec.ts | 5 ++++- .../src/lib/modules/file-attachment/file-drop.component.ts | 4 ++-- .../lib/modules/file-attachment/file-validate-function.ts | 3 +++ 6 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 libs/components/forms/src/lib/modules/file-attachment/file-validate-function.ts 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 8058505c70..6292e6685e 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 @@ -989,7 +989,7 @@ describe('File attachment', () => { 'You may not upload a file that begins with the letter "w."'; fileAttachmentInstance.validateFn = function ( - inputFile: any + inputFile: SkyFileItem ): string | undefined { if (inputFile.file.name.indexOf('w') === 0) { return errorMessage; 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 98c88cd286..f8eb0592e1 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 @@ -28,6 +28,7 @@ import { SkyFileAttachmentLabelComponent } from './file-attachment-label.compone import { SkyFileAttachmentService } from './file-attachment.service'; import { SkyFileItem } from './file-item'; import { SkyFileItemService } from './file-item.service'; +import { SkyFileValidateFn } from './file-validate-function'; import { SkyFileAttachmentChange } from './types/file-attachment-change'; import { SkyFileAttachmentClick } from './types/file-attachment-click'; @@ -104,8 +105,7 @@ export class SkyFileAttachmentComponent * file validation. This function takes a `SkyFileItem` object as a parameter. */ @Input() - // TODO: Change `Function` to a more specific type in a breaking change. - public validateFn: Function | undefined; + public validateFn: SkyFileValidateFn | undefined; /** * Fires when users add or remove files. 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 f66ec90349..ab48a86055 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 @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { SkyFileItem } from './file-item'; +import { SkyFileValidateFn } from './file-validate-function'; /** * @internal @@ -12,7 +13,7 @@ export class SkyFileAttachmentService { minFileSize: number, maxFileSize: number, acceptedTypes?: string, - validateFn?: Function + validateFn?: SkyFileValidateFn ): SkyFileItem[] { const fileResults: SkyFileItem[] = []; 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 211138cf82..e3fe5856b9 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 @@ -2,6 +2,7 @@ import { Component, DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; +import { SkyFileItem } from '@skyux/forms'; import { SkyFileAttachmentsModule } from './file-attachments.module'; import { SkyFileDropComponent } from './file-drop.component'; @@ -548,7 +549,9 @@ describe('File drop component', () => { const errorMessage = 'You may not upload a file that begins with the letter "w."'; - componentInstance.validateFn = function (file: any): string | undefined { + componentInstance.validateFn = function ( + file: SkyFileItem + ): string | undefined { if (file.file.name.indexOf('w') === 0) { return errorMessage; } 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 3118111976..5c5a128b2d 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 @@ -11,6 +11,7 @@ import { import { SkyFileAttachmentService } from './file-attachment.service'; import { SkyFileItem } from './file-item'; import { SkyFileLink } from './file-link'; +import { SkyFileValidateFn } from './file-validate-function'; import { SkyFileDropChange } from './types/file-drop-change'; const MAX_FILE_SIZE_DEFAULT = 500000; @@ -102,8 +103,7 @@ export class SkyFileDropComponent implements OnDestroy { * file validation. This function takes a `SkyFileItem` object as a parameter. */ @Input() - // TODO: Change `Function` to a more specific type in a breaking change. - public validateFn: Function | undefined; + public validateFn: SkyFileValidateFn | undefined; /** * Specifies a comma-delimited string literal of MIME types that users can attach. diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-validate-function.ts b/libs/components/forms/src/lib/modules/file-attachment/file-validate-function.ts new file mode 100644 index 0000000000..b540a5f119 --- /dev/null +++ b/libs/components/forms/src/lib/modules/file-attachment/file-validate-function.ts @@ -0,0 +1,3 @@ +import { SkyFileItem } from './file-item'; + +export type SkyFileValidateFn = (file: SkyFileItem) => string | undefined; From 5bd87621ba412cebb38285b6e9ece256e07bbe6b Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Wed, 12 Oct 2022 13:54:43 -0400 Subject: [PATCH 14/30] feat(components/indicators): remove bottom margin from alert component (#648) --- .../lib/modules/alert/alert.component.scss | 1 - .../migrations/migration-collection.json | 7 +- .../update-7/add-compat-stylesheets.spec.ts | 196 ++++++++++++++++++ .../update-7/add-compat-stylesheets.ts | 135 ++++++++++++ .../src/schematics/utility/workspace.ts | 2 +- 5 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.spec.ts create mode 100644 libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.ts diff --git a/libs/components/indicators/src/lib/modules/alert/alert.component.scss b/libs/components/indicators/src/lib/modules/alert/alert.component.scss index 0ad8474fae..85bbf79579 100644 --- a/libs/components/indicators/src/lib/modules/alert/alert.component.scss +++ b/libs/components/indicators/src/lib/modules/alert/alert.component.scss @@ -32,7 +32,6 @@ .sky-alert { padding: 0 $sky-padding; - margin-bottom: $sky-margin-double; border-left: solid var(--sky-alert-border-left-width); color: var(--sky-text-color-default); display: flex; diff --git a/libs/components/packages/src/schematics/migrations/migration-collection.json b/libs/components/packages/src/schematics/migrations/migration-collection.json index 778cdf587a..29fd60fb7a 100644 --- a/libs/components/packages/src/schematics/migrations/migration-collection.json +++ b/libs/components/packages/src/schematics/migrations/migration-collection.json @@ -11,9 +11,14 @@ "description": "Add moment.js as a dependency since SKY UX 7 references moment.js as a peer." }, "rename-mutation-observer-service": { - "version": "", + "version": "7.0.0-beta.0", "factory": "./update-7/rename-mutation-observer-service", "description": "Replace all instances of 'MutationObserverService' with 'SkyMutationObserverService'" + }, + "add-compat-stylesheets": { + "version": "7.0.0-beta.0", + "factory": "./update-7/add-compat-stylesheets", + "description": "Add a backwards-compatible stylesheet with styles that have been removed from components in SKY UX 7." } } } diff --git a/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.spec.ts b/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.spec.ts new file mode 100644 index 0000000000..2f8e9c247e --- /dev/null +++ b/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.spec.ts @@ -0,0 +1,196 @@ +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; + +import { join } from 'path'; + +import { createTestApp, createTestLibrary } from '../../testing/scaffold'; + +describe('Migrations > Add compat stylesheets', () => { + const compatStylesheetPath = '/src/app/skyux7-compat.css'; + const alertContents = `/******************************************************************************* + * TODO: The following component libraries introduced visual breaking changes + * in SKY UX 7. Each block of CSS reintroduces the styles that were changed or + * removed for backwards compatibility. You will need to do the following + * before migrating to the next major version of SKY UX: + * - Address each of the changes by following the instructions + * in each block of CSS, then remove the block. + * - Delete this file after all blocks have been addressed. + * - Remove each occurrence of this file in your project's + * angular.json file. +*******************************************************************************/ + +/******************************************************************************* + * COMPONENT: ALERT +*******************************************************************************/ + +/******************************************************************************* + * The preset bottom margin has been removed from alert components. To + * implement the newly-recommended spacing, add the \`sky-margin-stacked-lg\` + * CSS class to each \`sky-alert\` component in your application, then remove + * this block. +*******************************************************************************/ + +.sky-alert { + margin-bottom: 20px; +} +`; + + const runner = new SchematicTestRunner( + 'migrations', + join(__dirname, '../migration-collection.json') + ); + + async function setupTest() { + const tree = await createTestApp(runner, { + projectName: 'my-app', + }); + + return { + runSchematic: () => + runner + .runSchematicAsync('add-compat-stylesheets', {}, tree) + .toPromise(), + tree, + }; + } + + async function validateCompatStylesheet( + packageJson: string, + expectedContents: string, + existingCompatStylesheet?: string, + existingWorkspaceStylehseets: string[] = [] + ): Promise { + const projectTargets = ['build', 'test']; + + const { runSchematic, tree } = await setupTest(); + + tree.overwrite('/package.json', packageJson); + + if (existingCompatStylesheet) { + tree.create(compatStylesheetPath, existingCompatStylesheet); + } + + let angularJson = JSON.parse(tree.readContent('/angular.json')); + + for (const target of projectTargets) { + angularJson.projects['my-app'].architect[target].options.styles = + existingWorkspaceStylehseets; + } + + tree.overwrite('/angular.json', JSON.stringify(angularJson)); + + const updatedTree = await runSchematic(); + + const compatStyles = updatedTree.readText(compatStylesheetPath); + + expect(compatStyles).toBe(expectedContents); + + angularJson = updatedTree.readJson('/angular.json'); + + const expectedStyles = [ + ...(existingWorkspaceStylehseets || []), + compatStylesheetPath, + ]; + + for (const target of projectTargets) { + expect( + angularJson.projects['my-app'].architect[target].options.styles + ).toEqual(expectedStyles); + } + } + + it('should not add a compat stylesheet if a corresponding library is not installed', async () => { + const { runSchematic, tree } = await setupTest(); + + await runSchematic(); + + expect(tree.exists(compatStylesheetPath)).toBe(false); + }); + + it('should add a compat stylesheet for libraries in dependencies', async () => { + await validateCompatStylesheet( + JSON.stringify({ + dependencies: { + '@skyux/indicators': '6.0.0', + }, + }), + alertContents + ); + }); + + it('should add a compat stylesheet for libraries in devDependencies', async () => { + await validateCompatStylesheet( + JSON.stringify({ + devDependencies: { + '@skyux/indicators': '6.0.0', + }, + }), + alertContents + ); + }); + + it('should overwrite an existing compat stylesheet', async () => { + await validateCompatStylesheet( + JSON.stringify({ + devDependencies: { + '@skyux/indicators': '6.0.0', + }, + }), + alertContents, + '/* */' + ); + }); + + it('should handle missing styles array', async () => { + await validateCompatStylesheet( + JSON.stringify({ + devDependencies: { + '@skyux/indicators': '6.0.0', + }, + }), + alertContents, + '/* */', + undefined + ); + }); + + it('should ignore build target for libraries', async () => { + const tree = await createTestLibrary(runner, { + projectName: 'my-lib', + }); + + runner.runSchematicAsync('add-compat-stylesheets', {}, tree).toPromise(); + + tree.overwrite( + '/package.json', + JSON.stringify({ + dependencies: { + '@skyux/indicators': '6.0.0', + }, + }) + ); + + let angularJson = JSON.parse(tree.readContent('/angular.json')); + + const updatedTree = await runner + .runSchematicAsync('add-compat-stylesheets', {}, tree) + .toPromise(); + + angularJson = JSON.parse(updatedTree.readContent('/angular.json')); + + expect( + angularJson.projects['my-lib'].architect.build.options.styles + ).toBeUndefined(); + + expect( + angularJson.projects['my-lib'].architect.test.options.styles + ).toContain(compatStylesheetPath); + + expect( + angularJson.projects['my-lib-showcase'].architect.build.options.styles + ).toContain(compatStylesheetPath); + + expect( + angularJson.projects['my-lib-showcase'].architect.test.options.styles + ).toContain(compatStylesheetPath); + }); +}); diff --git a/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.ts b/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.ts new file mode 100644 index 0000000000..3bc6c223f1 --- /dev/null +++ b/libs/components/packages/src/schematics/migrations/update-7/add-compat-stylesheets.ts @@ -0,0 +1,135 @@ +import { Rule, chain } from '@angular-devkit/schematics'; + +import { readRequiredFile } from '../../utility/tree'; +import { updateWorkspace } from '../../utility/workspace'; + +const SKYUX7_COMPAT_CSS_PATH = '/src/app/skyux7-compat.css'; + +const compatStyles = { + libraries: [ + { + name: '@skyux/indicators', + components: [ + { + name: 'alert', + styles: [ + { + css: ` +.sky-alert { + margin-bottom: 20px; +}`, + instructions: ` +The preset bottom margin has been removed from alert components. To implement the newly-recommended spacing, add the \`sky-margin-stacked-lg\` CSS class to each \`sky-alert\` component in your application, then remove this block.`, + }, + ], + }, + ], + }, + ], +}; + +function buildCommentBlock(message: string): string { + return `/${'*'.repeat(79)} + * ${message.replace(/(?![^\n]{1,75}$)([^\n]{1,75})\s/g, '$1\n * ')} +${'*'.repeat(79)}/`; +} + +function buildComponentCss(component: { + name: string; + styles: { css: string; instructions: string }[]; +}): string { + let contents = buildCommentBlock( + `COMPONENT: ${component.name.toUpperCase()}` + ); + + for (const style of component.styles) { + contents += ` + +${buildCommentBlock(style.instructions.trim())} + +${style.css.trim()} +`; + } + + return contents; +} + +function writeStylesheet(contents: string): Rule { + return (tree) => { + if (tree.exists(SKYUX7_COMPAT_CSS_PATH)) { + tree.overwrite(SKYUX7_COMPAT_CSS_PATH, contents); + } else { + tree.create(SKYUX7_COMPAT_CSS_PATH, contents); + } + }; +} + +function addStylesheetToWorkspace(): Rule { + return () => + updateWorkspace((workspace) => { + for (const project of workspace.projects.values()) { + for (const targetName of ['build', 'test']) { + // Ignore build target for libraries. + if ( + !( + targetName === 'build' && + project.extensions.projectType === 'library' + ) + ) { + const target = project.targets.get(targetName); + + /* istanbul ignore else */ + if (target && target.options) { + const styles = (target.options.styles = (target.options.styles || + []) as string[]); + + if (!styles.includes(SKYUX7_COMPAT_CSS_PATH)) { + styles.push(SKYUX7_COMPAT_CSS_PATH); + } + } + } + } + } + }); +} + +export default function (): Rule { + return (tree) => { + const packageJson: { + dependencies?: { [_: string]: string }; + devDependencies?: { [_: string]: string }; + } = JSON.parse(readRequiredFile(tree, '/package.json')); + + let contents = ''; + + for (const library of compatStyles.libraries) { + if ( + packageJson.dependencies?.[library.name] || + packageJson.devDependencies?.[library.name] + ) { + for (const component of library.components) { + contents += buildComponentCss(component); + } + } + } + + const rules: Rule[] = []; + + if (contents) { + contents = `${buildCommentBlock( + `TODO: The following component libraries introduced visual breaking changes in SKY UX 7. Each block of CSS reintroduces the styles that were changed or removed for backwards compatibility. You will need to do the following before migrating to the next major version of SKY UX: +- Address each of the changes by following the instructions + in each block of CSS, then remove the block. +- Delete this file after all blocks have been addressed. +- Remove each occurrence of this file in your project's + angular.json file.` + )} + +${contents}`; + + rules.push(writeStylesheet(contents), addStylesheetToWorkspace()); + } + + return chain(rules); + }; +} diff --git a/libs/components/packages/src/schematics/utility/workspace.ts b/libs/components/packages/src/schematics/utility/workspace.ts index 010a6e6e8d..a3452b103d 100644 --- a/libs/components/packages/src/schematics/utility/workspace.ts +++ b/libs/components/packages/src/schematics/utility/workspace.ts @@ -57,7 +57,7 @@ export async function getProject( * Allows updates to the Angular project config (angular.json). */ export function updateWorkspace( - updater: (workspace: workspaces.WorkspaceDefinition) => void + updater: (workspace: workspaces.WorkspaceDefinition) => void | Promise ): Rule { return async (tree) => { const { host, workspace } = await getWorkspace(tree); From 8ffa182538269488b561fda377dc677927f0e227 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Wed, 12 Oct 2022 15:46:05 -0400 Subject: [PATCH 15/30] feat(components/toast): improve toast service `openComponent` `component` param type (#667) --- .../toast/src/lib/modules/toast/toast.service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/components/toast/src/lib/modules/toast/toast.service.ts b/libs/components/toast/src/lib/modules/toast/toast.service.ts index 24ae61df00..8beb307da0 100644 --- a/libs/components/toast/src/lib/modules/toast/toast.service.ts +++ b/libs/components/toast/src/lib/modules/toast/toast.service.ts @@ -1,4 +1,10 @@ -import { ComponentRef, Injectable, OnDestroy, Provider } from '@angular/core'; +import { + ComponentRef, + Injectable, + OnDestroy, + Provider, + Type, +} from '@angular/core'; import { SkyDynamicComponentService } from '@skyux/core'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -69,9 +75,7 @@ export class SkyToastService implements OnDestroy { * constructor. */ public openComponent( - // TODO: change this to Type in a breaking change to match the first - // parameter of the dynamic component service's createComponent() method. - component: any, + component: Type, config?: SkyToastConfig, providers: Provider[] = [] ): SkyToastInstance { From 748d2df425be6b4c5d28297ec2bd8a8e1754f1f9 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Wed, 12 Oct 2022 16:47:06 -0400 Subject: [PATCH 16/30] chore(components/lists): filter strict mode support (#661) --- .../filter/filter-button.component.spec.ts | 12 ++++---- .../modules/filter/filter-button.component.ts | 30 +++++++++++-------- .../filter/filter-summary-item.component.ts | 10 ++++++- .../filter/filter-summary.component.spec.ts | 4 +-- .../filter-button.component.fixture.ts | 10 +++---- .../filter-summary.component.fixture.ts | 6 ++-- .../src/filter/filter-fixture-button.ts | 26 +++++++++------- .../src/filter/filter-fixture-summary.ts | 16 +++++----- .../testing/src/filter/filter-fixture.spec.ts | 8 ++--- .../src/filter/lists-filter-fixture-button.ts | 4 +-- .../testing/src/paging/paging-fixture.spec.ts | 16 +++++----- 11 files changed, 81 insertions(+), 61 deletions(-) diff --git a/libs/components/lists/src/lib/modules/filter/filter-button.component.spec.ts b/libs/components/lists/src/lib/modules/filter/filter-button.component.spec.ts index f84a0a41cf..1b9c5d3e06 100644 --- a/libs/components/lists/src/lib/modules/filter/filter-button.component.spec.ts +++ b/libs/components/lists/src/lib/modules/filter/filter-button.component.spec.ts @@ -22,15 +22,15 @@ describe('Filter button', () => { fixture.detectChanges(); }); - function getButtonEl() { + function getButtonEl(): HTMLButtonElement { return nativeElement.querySelector('.sky-btn') as HTMLButtonElement; } - function verifyTextPresent() { + function verifyTextPresent(): void { expect(getButtonEl().innerText.trim()).toBe('Filter'); } - function verifyTextNotPresent() { + function verifyTextNotPresent(): void { expect(getButtonEl().innerText.trim()).not.toBe('Filter'); } @@ -45,7 +45,7 @@ describe('Filter button', () => { it('should allow setting id', () => { component.buttonId = 'i-am-an-id-look-at-me'; fixture.detectChanges(); - expect(nativeElement.querySelector('.sky-btn').id).toBe( + expect(nativeElement.querySelector('.sky-btn')?.id).toBe( 'i-am-an-id-look-at-me' ); }); @@ -56,8 +56,8 @@ describe('Filter button', () => { fixture.detectChanges(); const button = nativeElement.querySelector('.sky-btn'); - expect(button.getAttribute('aria-controls')).toBe('filter-zone-2'); - expect(button.getAttribute('aria-expanded')).toBe('true'); + expect(button?.getAttribute('aria-controls')).toBe('filter-zone-2'); + expect(button?.getAttribute('aria-expanded')).toBe('true'); }); it('should emit event on click', () => { diff --git a/libs/components/lists/src/lib/modules/filter/filter-button.component.ts b/libs/components/lists/src/lib/modules/filter/filter-button.component.ts index e23afac1db..21c575c874 100644 --- a/libs/components/lists/src/lib/modules/filter/filter-button.component.ts +++ b/libs/components/lists/src/lib/modules/filter/filter-button.component.ts @@ -19,11 +19,11 @@ export class SkyFilterButtonComponent { * Specifies an ID for the filter button. */ @Input() - public get filterButtonId() { - return this._filterButtonId || `sky-filter-button-${++nextId}`; + public get filterButtonId(): string { + return this.#filterButtonIdOrDefault; } - public set filterButtonId(value: string) { - this._filterButtonId = value; + public set filterButtonId(value: string | undefined) { + this.#filterButtonIdOrDefault = value || this.#defaultButtonId; } /** @@ -33,7 +33,7 @@ export class SkyFilterButtonComponent { * this property is necessary when using inline filters. */ @Input() - public ariaControls: string; + public ariaControls: string | undefined; /** * Indicates whether the filtering options are exposed. @@ -41,7 +41,7 @@ export class SkyFilterButtonComponent { * this property is necessary when using inline filters. */ @Input() - public ariaExpanded: boolean; + public ariaExpanded: boolean | undefined = false; /** * Indicates whether to highlight the filter button to indicate that filters were applied. @@ -49,29 +49,35 @@ export class SkyFilterButtonComponent { * to users. For example, set it to `true` if you do not display the filter summary. */ @Input() - public active = false; + public active: boolean | undefined = false; /** * Indicates whether to disable the filter button. */ @Input() - public disabled = false; + public disabled: boolean | undefined = false; /** * Indicates whether to display a **Filter** label beside the icon on the filter button. */ @Input() - public showButtonText = false; + public showButtonText: boolean | undefined = false; /** * Fires when the filter button is selected. */ @Output() - public filterButtonClick: EventEmitter = new EventEmitter(); + public filterButtonClick: EventEmitter = new EventEmitter(); - private _filterButtonId: string; + constructor() { + this.#filterButtonIdOrDefault = + this.#defaultButtonId = `sky-filter-button-${++nextId}`; + } + + #defaultButtonId: string; + #filterButtonIdOrDefault: string; public filterButtonOnClick(): void { - this.filterButtonClick.emit(undefined); + this.filterButtonClick.emit(); } } diff --git a/libs/components/lists/src/lib/modules/filter/filter-summary-item.component.ts b/libs/components/lists/src/lib/modules/filter/filter-summary-item.component.ts index 6aadd91dae..4242e12eae 100644 --- a/libs/components/lists/src/lib/modules/filter/filter-summary-item.component.ts +++ b/libs/components/lists/src/lib/modules/filter/filter-summary-item.component.ts @@ -20,7 +20,13 @@ export class SkyFilterSummaryItemComponent { * Indicates whether the filter summary item has a close button. */ @Input() - public dismissible = true; + public get dismissible(): boolean { + return this.#_dismissible; + } + + public set dismissible(value: boolean | undefined) { + this.#_dismissible = value !== false; + } /** * Fires when the summary item close button is selected. @@ -34,6 +40,8 @@ export class SkyFilterSummaryItemComponent { @Output() public itemClick = new EventEmitter(); + #_dismissible = true; + public onItemDismiss(): void { this.dismiss.emit(); } diff --git a/libs/components/lists/src/lib/modules/filter/filter-summary.component.spec.ts b/libs/components/lists/src/lib/modules/filter/filter-summary.component.spec.ts index 0c8c9fcb9d..4ce9f98338 100644 --- a/libs/components/lists/src/lib/modules/filter/filter-summary.component.spec.ts +++ b/libs/components/lists/src/lib/modules/filter/filter-summary.component.spec.ts @@ -43,8 +43,8 @@ describe('Filter summary', () => { it('should set aria-label and title on close filter button', () => { const el = nativeElement.querySelector('.sky-token-btn-close'); - expect(el.getAttribute('aria-label')).toBe('Remove filter'); - expect(el.getAttribute('title')).toBe('Remove filter'); + expect(el?.getAttribute('aria-label')).toBe('Remove filter'); + expect(el?.getAttribute('title')).toBe('Remove filter'); }); it('should emit an event on item click', () => { diff --git a/libs/components/lists/src/lib/modules/filter/fixtures/filter-button.component.fixture.ts b/libs/components/lists/src/lib/modules/filter/fixtures/filter-button.component.fixture.ts index 6b76b8a106..2c4e9081a1 100644 --- a/libs/components/lists/src/lib/modules/filter/fixtures/filter-button.component.fixture.ts +++ b/libs/components/lists/src/lib/modules/filter/fixtures/filter-button.component.fixture.ts @@ -9,10 +9,10 @@ export class FilterButtonTestComponent { public filtersActive = false; public showButtonText = false; public buttonClicked = false; - public buttonId: string; - public ariaExpanded: boolean; - public ariaControls: string; - public skyThemeSettings: SkyThemeSettings; + public buttonId: string | undefined; + public ariaExpanded: boolean | undefined; + public ariaControls: string | undefined; + public skyThemeSettings!: SkyThemeSettings; constructor() { this.useDefaultTheme(); @@ -32,7 +32,7 @@ export class FilterButtonTestComponent { ); } - public filterButtonClicked() { + public filterButtonClicked(): void { this.buttonClicked = true; } } diff --git a/libs/components/lists/src/lib/modules/filter/fixtures/filter-summary.component.fixture.ts b/libs/components/lists/src/lib/modules/filter/fixtures/filter-summary.component.fixture.ts index e68625963c..098ded96d4 100644 --- a/libs/components/lists/src/lib/modules/filter/fixtures/filter-summary.component.fixture.ts +++ b/libs/components/lists/src/lib/modules/filter/fixtures/filter-summary.component.fixture.ts @@ -5,7 +5,7 @@ import { Component } from '@angular/core'; templateUrl: './filter-summary.component.fixture.html', }) export class FilterSummaryTestComponent { - public appliedFilters: Array = [ + public appliedFilters = [ { label: 'hide orange', dismissible: false, @@ -20,11 +20,11 @@ export class FilterSummaryTestComponent { public summaryClicked = false; - public onDismiss() { + public onDismiss(): void { this.dismissed = true; } - public filterButtonClicked() { + public filterButtonClicked(): void { this.summaryClicked = true; } } diff --git a/libs/components/lists/testing/src/filter/filter-fixture-button.ts b/libs/components/lists/testing/src/filter/filter-fixture-button.ts index 83f19cedc5..ad1df3b6ba 100644 --- a/libs/components/lists/testing/src/filter/filter-fixture-button.ts +++ b/libs/components/lists/testing/src/filter/filter-fixture-button.ts @@ -25,7 +25,7 @@ export class SkyFilterFixtureButton { * Click the button to apply the filter. */ public async clickFilterButton(): Promise { - const button = this.getButtonElement(); + const button = this.#getButtonElement(); if (button instanceof HTMLButtonElement && !button.disabled) { button.click(); } @@ -34,27 +34,31 @@ export class SkyFilterFixtureButton { } public get button(): SkyListsFilterFixtureButton { - const buttonElement = this.getButtonElement(); + const buttonElement = this.#getButtonElement(); return { - ariaControls: buttonElement.getAttribute('aria-controls'), - ariaExpanded: buttonElement.getAttribute('aria-expanded') === 'true', - disabled: buttonElement.disabled, - id: buttonElement.id, + ariaControls: buttonElement?.getAttribute('aria-controls') ?? undefined, + ariaExpanded: buttonElement?.getAttribute('aria-expanded') === 'true', + disabled: !!buttonElement?.disabled, + id: buttonElement?.id, }; } /** * Get the button text. */ public get buttonText(): string { - const text = this.getButtonElement()?.innerText; - return this.normalizeText(text); + const text = this.#getButtonElement()?.innerText; + return this.#normalizeText(text); } - private getButtonElement(): HTMLButtonElement | null { + #getButtonElement(): HTMLButtonElement | null { return this.debugElement.nativeElement.querySelector('.sky-filter-btn'); } - private normalizeText(text: string): string { - return text.trim().replace(/\s+/g, ' '); + #normalizeText(text: string | undefined): string { + let retVal = ''; + if (text) { + retVal = text?.trim().replace(/\s+/g, ' '); + } + return retVal; } } diff --git a/libs/components/lists/testing/src/filter/filter-fixture-summary.ts b/libs/components/lists/testing/src/filter/filter-fixture-summary.ts index 7d75e24025..aeb2f0cf34 100644 --- a/libs/components/lists/testing/src/filter/filter-fixture-summary.ts +++ b/libs/components/lists/testing/src/filter/filter-fixture-summary.ts @@ -9,18 +9,20 @@ import { SkyAppTestUtility } from '@skyux-sdk/testing'; * @internal */ export class SkyFilterFixtureSummary { - private debugElement: DebugElement; + #debugElement: DebugElement; + #fixture: ComponentFixture; - constructor(private fixture: ComponentFixture, skyTestId: string) { - this.debugElement = SkyAppTestUtility.getDebugElementByTestId( - this.fixture, + constructor(fixture: ComponentFixture, skyTestId: string) { + this.#fixture = fixture; + this.#debugElement = SkyAppTestUtility.getDebugElementByTestId( + this.#fixture, skyTestId, 'sky-filter-summary' ); } public async filterCloseClick(index: number): Promise { - const summaryItems = this.debugElement.nativeElement.querySelectorAll( + const summaryItems = this.#debugElement.nativeElement.querySelectorAll( 'sky-filter-summary-item' ); if (summaryItems.length > index) { @@ -29,8 +31,8 @@ export class SkyFilterFixtureSummary { const closeButton = summaryItem.querySelector('.sky-token-btn-close'); if (closeButton instanceof HTMLElement) { closeButton.click(); - this.fixture.detectChanges(); - return this.fixture.whenStable(); + this.#fixture.detectChanges(); + return this.#fixture.whenStable(); } } } diff --git a/libs/components/lists/testing/src/filter/filter-fixture.spec.ts b/libs/components/lists/testing/src/filter/filter-fixture.spec.ts index 6d3ffe7836..c3c33dc720 100644 --- a/libs/components/lists/testing/src/filter/filter-fixture.spec.ts +++ b/libs/components/lists/testing/src/filter/filter-fixture.spec.ts @@ -35,10 +35,10 @@ const DATA_SKY_ID_BUTTON = 'test-filter-button'; }) class FilterTestComponent { public appliedFilters: string[] = []; - public buttonClicked: boolean = false; - public buttonIsDisabled: boolean = false; - public ariaExpanded: boolean = false; - public ariaControls: string; + public buttonClicked = false; + public buttonIsDisabled = false; + public ariaExpanded = false; + public ariaControls: string | undefined; public applyFilter(filter: string): void { this.appliedFilters.push(filter); diff --git a/libs/components/lists/testing/src/filter/lists-filter-fixture-button.ts b/libs/components/lists/testing/src/filter/lists-filter-fixture-button.ts index 762c8036a6..95e20fe1fa 100644 --- a/libs/components/lists/testing/src/filter/lists-filter-fixture-button.ts +++ b/libs/components/lists/testing/src/filter/lists-filter-fixture-button.ts @@ -1,6 +1,6 @@ export type SkyListsFilterFixtureButton = { - ariaControls: string; + ariaControls: string | undefined; ariaExpanded: boolean; disabled: boolean; - id: string; + id: string | undefined; }; diff --git a/libs/components/lists/testing/src/paging/paging-fixture.spec.ts b/libs/components/lists/testing/src/paging/paging-fixture.spec.ts index 3f0ca0fdf9..ac88220a65 100644 --- a/libs/components/lists/testing/src/paging/paging-fixture.spec.ts +++ b/libs/components/lists/testing/src/paging/paging-fixture.spec.ts @@ -26,11 +26,11 @@ const DATA_SKY_ID = 'test-paging'; `, }) class PagingTestComponent { - public currentPage: number = 1; - public itemCount: number = 8; - public maxPages: number = 3; // only show 3 pages in pager - public pageLabel: string; - public pageSize: number = 2; // 4 total pages + public currentPage = 1; + public itemCount = 8; + public maxPages = 3; // only show 3 pages in pager + public pageLabel: string | undefined; + public pageSize = 2; // 4 total pages public currentPageChange(currentPage: number): void {} } @@ -43,14 +43,14 @@ describe('Paging fixture', () => { //#region helpers - function getLastPage() { + function getLastPage(): number { return Math.ceil(testComponent.itemCount / testComponent.pageSize); } function verifyActivePageLink( pages: SkyPagingFixtureButton[], expectedActivePageId: string | number - ) { + ): void { pages.forEach((page: SkyPagingFixtureButton) => { const shouldBeActive = page.id === expectedActivePageId.toString(); @@ -59,7 +59,7 @@ describe('Paging fixture', () => { }); } - function verifyPagingState(expectedActivePageId: number) { + function verifyPagingState(expectedActivePageId: number): void { const pages = pagingFixture.pageLinks; // active page should be accurate From 9711a842e8c3a5c6887adfdfceab6719001a4a1e Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 13 Oct 2022 11:52:38 -0400 Subject: [PATCH 17/30] fix(components/text-editor): toolbars are hidden when no items exist within the toolbars (#676) (#678) --- .../src/lib/modules/text-editor/text-editor.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html index d2c2065d6a..3edcb96943 100644 --- a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html +++ b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.html @@ -1,6 +1,10 @@
        - + Date: Thu, 13 Oct 2022 12:26:04 -0400 Subject: [PATCH 18/30] fix(components/lookup): modern search clickbox takes up entire input box (#677) (#679) --- .../lookup/src/lib/modules/search/search.component.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/components/lookup/src/lib/modules/search/search.component.scss b/libs/components/lookup/src/lib/modules/search/search.component.scss index 3368f3e877..8f2f64b388 100644 --- a/libs/components/lookup/src/lib/modules/search/search.component.scss +++ b/libs/components/lookup/src/lib/modules/search/search.component.scss @@ -182,7 +182,6 @@ sky-search { .sky-input-box-form-group-inner { input { - flex-basis: auto; width: auto; padding: $sky-theme-modern-space-sm $sky-theme-modern-space-sm $sky-theme-modern-space-sm 0; From d989b9288e0ae83190c123a4871ca8718f105862 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 13 Oct 2022 16:29:57 -0400 Subject: [PATCH 19/30] refactor(components/lookup): lookup library strict mode finishes (#622) --- .../autocomplete-default-search-function.ts | 2 +- .../autocomplete-input.directive.ts | 4 +- ...autocomplete-search-async-disabled.pipe.ts | 2 +- .../autocomplete/autocomplete.component.html | 5 +- .../autocomplete.component.spec.ts | 211 +++++++++------ .../autocomplete/autocomplete.component.ts | 116 +++++---- ...autocomplete-reactive.component.fixture.ts | 2 +- .../autocomplete.component.fixture.ts | 23 +- ...omplete-default-search-function-options.ts | 2 +- .../country-field.component.spec.ts | 159 +++++++----- .../country-field/country-field.component.ts | 34 ++- ...ountry-field-reactive.component.fixture.ts | 28 +- .../lookup-input-box.component.fixture.ts | 18 +- .../lookup-template.component.fixture.ts | 26 +- .../fixtures/lookup.component.fixture.ts | 64 +++-- .../lookup/lookup-autocomplete-adapter.ts | 65 +++-- .../lookup-show-more-modal.component.ts | 10 +- .../lib/modules/lookup/lookup.component.html | 2 +- .../modules/lookup/lookup.component.spec.ts | 240 ++++++++++-------- .../lib/modules/lookup/lookup.component.ts | 123 ++++++--- ...p-show-more-native-picker-async-context.ts | 36 ++- .../lookup-show-more-native-picker-context.ts | 36 ++- .../fixtures/search.component.fixture.html | 2 + .../fixtures/search.component.fixture.ts | 3 + .../lib/modules/search/search.component.html | 2 +- .../modules/search/search.component.spec.ts | 57 ++++- .../lib/modules/search/search.component.ts | 59 ++++- .../autocomplete/autocomplete-harness.spec.ts | 70 ++--- .../country-field-fixture.spec.ts | 8 +- .../country-field/country-field-fixture.ts | 69 ++--- .../fixtures/lookup-harness-test.component.ts | 7 +- .../testing/src/lookup/lookup-harness.spec.ts | 74 +++--- .../testing/src/search/search-fixture.ts | 28 +- .../testing/src/search/search-harness.spec.ts | 35 ++- 34 files changed, 985 insertions(+), 637 deletions(-) diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-default-search-function.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-default-search-function.ts index 9e6e179fd6..f5fa47d5be 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-default-search-function.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-default-search-function.ts @@ -49,7 +49,7 @@ export function skyAutocompleteDefaultSearchFunction( for (let i = 0, n = filteredData.length; i < n; i++) { const result = filteredData[i]; - const isMatch = options.propertiesToSearch!.find((property: string) => { + const isMatch = options.propertiesToSearch.find((property: string) => { let value = (result[property] || '').toString(); value = normalizeDiacritics(value).toUpperCase(); return value.indexOf(searchTextNormalized) > -1; diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-input.directive.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-input.directive.ts index dc6af0a92a..a6735e9dbf 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-input.directive.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-input.directive.ts @@ -68,8 +68,8 @@ export class SkyAutocompleteInputDirective * @default false */ @Input() - public set disabled(value: boolean) { - this.#_disabled = value; + public set disabled(value: boolean | undefined) { + this.#_disabled = value ?? false; this.#renderer.setProperty( this.#elementRef.nativeElement, 'disabled', diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-search-async-disabled.pipe.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-search-async-disabled.pipe.ts index 3914acca1c..1e4c154681 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-search-async-disabled.pipe.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete-search-async-disabled.pipe.ts @@ -11,7 +11,7 @@ import { SkyAutocompleteSearchAsyncArgs } from './types/autocomplete-search-asyn export class SkyAutcompleteSearchAsyncDisabledPipe implements PipeTransform { public transform( searchAsync: EventEmitter, - searchAsyncDisabled: boolean + searchAsyncDisabled: boolean | undefined ): boolean { return searchAsyncDisabled || searchAsync.observers.length === 0; } diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html index 2de14bd271..0a5a422988 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html @@ -40,7 +40,10 @@ >
        { } } - function getSearchResultsContainer(): HTMLElement { + function getSearchResultsContainer(): HTMLElement | null { return document.querySelector('.sky-autocomplete-results-container'); } - function getSearchResultsSection(): Element { + function getSearchResultsSection(): Element | null { return document.querySelector('.sky-autocomplete-results'); } @@ -206,7 +206,9 @@ describe('Autocomplete component', () => { function updateNgModel( fixture: ComponentFixture, - selectedValue: { objectid?: string; name?: string; text?: string } + selectedValue: + | { objectid?: string; name?: string; text?: string } + | undefined ) { fixture.componentInstance.model.favoriteColor = selectedValue; fixture.detectChanges(); @@ -245,10 +247,11 @@ describe('Autocomplete component', () => { }); it('should set defaults', () => { - expect(autocomplete.data).toEqual([]); - + component.data = undefined; fixture.detectChanges(); + expect(autocomplete.data).toEqual([]); + const inputElement: HTMLInputElement = getInputElement(); expect(inputElement.getAttribute('autocomplete')).toEqual('off'); @@ -260,10 +263,10 @@ describe('Autocomplete component', () => { expect(autocomplete.descriptorProperty).toEqual('name'); expect(autocomplete.highlightText).toEqual([]); expect(autocomplete.propertiesToSearch).toEqual(['name']); - expect(autocomplete.search).toBeDefined(); + expect(autocomplete.searchOrDefault).toBeDefined(); expect(autocomplete.searchFilters).toBeUndefined(); expect(autocomplete.searchResults).toEqual([]); - expect(autocomplete.searchResultsLimit).toBeUndefined(); + expect(autocomplete.searchResultsLimit).toBe(0); expect(autocomplete.searchResultTemplate).toBeUndefined(); expect(autocomplete.searchTextMinimumCharacters).toEqual(1); }); @@ -296,7 +299,7 @@ describe('Autocomplete component', () => { it('should search', fakeAsync(() => { fixture.detectChanges(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); enterSearch('r', fixture); @@ -413,7 +416,7 @@ describe('Autocomplete component', () => { enterSearch('rasdasdlhasdjklh', fixture); const container = getSearchResultsSection(); - expect(container.textContent.trim()).toBe(expectedMessage); + expect(container?.textContent?.trim()).toBe(expectedMessage); const actionsContainer = getActionsContainer(); expect(actionsContainer).toBeNull(); @@ -431,7 +434,7 @@ describe('Autocomplete component', () => { expect(resultsContainer).toBeNull(); const actionsContainer = getActionsContainer(); - expect(actionsContainer.textContent.trim()).toBe(expectedMessage); + expect(actionsContainer?.textContent?.trim()).toBe(expectedMessage); })); it('should show a no results found message in the actions area if the show more button is shown', fakeAsync(() => { @@ -445,7 +448,7 @@ describe('Autocomplete component', () => { expect(resultsContainer).toBeNull(); const actionsContainer = getActionsContainer(); - expect(actionsContainer.textContent.trim()).toBe(expectedMessage); + expect(actionsContainer?.textContent?.trim()).toBe(expectedMessage); })); it('should show a custom no results found message', fakeAsync(() => { @@ -456,7 +459,7 @@ describe('Autocomplete component', () => { enterSearch('rasdasdlhasdjklh', fixture); const container = getSearchResultsSection(); - expect(container.textContent.trim()).toBe(expectedMessage); + expect(container?.textContent?.trim()).toBe(expectedMessage); })); it('should allow custom search result template', fakeAsync(() => { @@ -465,7 +468,7 @@ describe('Autocomplete component', () => { enterSearch('r', fixture); - const customElement = getSearchResultsContainer().querySelector( + const customElement = getSearchResultsContainer()?.querySelector( '.custom-search-result-id' ) as HTMLElement; @@ -496,7 +499,7 @@ describe('Autocomplete component', () => { it('should not search if search text empty', fakeAsync(() => { fixture.detectChanges(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); enterSearch('', fixture); @@ -507,7 +510,7 @@ describe('Autocomplete component', () => { component.searchTextMinimumCharacters = 3; fixture.detectChanges(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); // First, verify that the search will run with 3 characters. enterSearch('123', fixture); @@ -523,10 +526,12 @@ describe('Autocomplete component', () => { it('should allow for custom search function', fakeAsync(() => { let customSearchCalled = false; - const customFunction: SkyAutocompleteSearchFunction = (): Promise< - [{ objectid?: string; name?: string; text?: string }] - > => { + let customSearchParameter: string | undefined; + const customFunction: SkyAutocompleteSearchFunction = ( + searchText: string + ): Promise<[{ objectid?: string; name?: string; text?: string }]> => { return new Promise((resolve) => { + customSearchParameter = searchText; customSearchCalled = true; resolve([{ name: 'Red' }]); }); @@ -536,11 +541,9 @@ describe('Autocomplete component', () => { fixture.detectChanges(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); - enterSearch('r', fixture); - expect(spy.calls.argsFor(0)[0]).toEqual('r'); + expect(customSearchParameter).toEqual('r'); expect(customSearchCalled).toEqual(true); })); @@ -553,7 +556,7 @@ describe('Autocomplete component', () => { fixture.detectChanges(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); enterSearch('r', fixture); @@ -570,13 +573,25 @@ describe('Autocomplete component', () => { const inputElement: HTMLInputElement = getInputElement(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); enterSearch('r', fixture); blurInput(inputElement, fixture); expect(inputElement.disabled).toBeTruthy(); expect(spy).not.toHaveBeenCalled(); + + component.disabled = undefined; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + enterSearch('r', fixture); + blurInput(inputElement, fixture); + + expect(inputElement.disabled).toBeFalsy(); + expect(spy).toHaveBeenCalled(); })); it('should handle missing skyAutocomplete directive on load', fakeAsync(() => { @@ -585,6 +600,11 @@ describe('Autocomplete component', () => { try { fixture.detectChanges(); } catch (e) { + if (!(e instanceof Error)) { + fail('should have thrown an Error'); + return; + } + expect( e.message.indexOf( 'The SkyAutocompleteComponent requires a ContentChild input' @@ -629,7 +649,7 @@ describe('Autocomplete component', () => { autocompleteElement.getBoundingClientRect().width }px`; - expect(dropdownElement.style.width).toEqual(formattedWidth); + expect(dropdownElement?.style.width).toEqual(formattedWidth); })); it('should set the width of the dropdown on window resize', fakeAsync(() => { @@ -658,7 +678,7 @@ describe('Autocomplete component', () => { autocompleteElement.getBoundingClientRect().width }px`; - expect(dropdownElement.style.width).toEqual(formattedWidth); + expect(dropdownElement?.style.width).toEqual(formattedWidth); })); it('should search after debounce time', fakeAsync(() => { @@ -667,7 +687,7 @@ describe('Autocomplete component', () => { const inputElement: HTMLInputElement = getInputElement(); - const spy = spyOn(autocomplete, 'search').and.callThrough(); + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); inputElement.value = 'r'; SkyAppTestUtility.fireDomEvent(inputElement, 'input'); @@ -765,7 +785,17 @@ describe('Autocomplete component', () => { // Type 'r' to activate the autocomplete dropdown, then click the first result. enterSearch('r', fixture); - const addButton = getAddButton(); + let addButton = getAddButton(); + expect(addButton).toBeNull(); + expect(addButtonSpy).not.toHaveBeenCalled(); + + component.showAddButton = undefined; + fixture.detectChanges(); + + // Type 'r' to activate the autocomplete dropdown, then click the first result. + enterSearch('r', fixture); + + addButton = getAddButton(); expect(addButton).toBeNull(); expect(addButtonSpy).not.toHaveBeenCalled(); })); @@ -871,7 +901,17 @@ describe('Autocomplete component', () => { // Type 'r' to activate the autocomplete dropdown, then click the first result. enterSearch('r', fixture); - const showMoreButton = getShowMoreButton(); + let showMoreButton = getShowMoreButton(); + expect(showMoreButton).toBeNull(); + expect(showMoreButtonSpy).not.toHaveBeenCalled(); + + component.enableShowMore = undefined; + fixture.detectChanges(); + + // Type 'r' to activate the autocomplete dropdown, then click the first result. + enterSearch('r', fixture); + + showMoreButton = getShowMoreButton(); expect(showMoreButton).toBeNull(); expect(showMoreButtonSpy).not.toHaveBeenCalled(); })); @@ -1009,17 +1049,17 @@ describe('Autocomplete component', () => { fixture.detectChanges(); const wrapper = document.querySelector('.sky-autocomplete'); - expect(wrapper.getAttribute('aria-owns')).toBeNull(); + expect(wrapper?.getAttribute('aria-owns')).toBeNull(); enterSearch('r', fixture); const searchResultsContainer = getSearchResultsSection(); - expect(wrapper.getAttribute('aria-owns')).toEqual( - searchResultsContainer.id + expect(wrapper?.getAttribute('aria-owns')).toEqual( + searchResultsContainer?.id ); blurInput(getInputElement(), fixture); - expect(wrapper.getAttribute('aria-owns')).toBeNull(); + expect(wrapper?.getAttribute('aria-owns')).toBeNull(); })); describe('highlighting', () => { @@ -1034,12 +1074,12 @@ describe('Autocomplete component', () => { expect( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ).toBe('r'); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(6); })); @@ -1053,7 +1093,7 @@ describe('Autocomplete component', () => { fixture.detectChanges(); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(6); enterSearch('\u0305', fixture); @@ -1061,7 +1101,7 @@ describe('Autocomplete component', () => { fixture.detectChanges(); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(0); })); @@ -1075,7 +1115,7 @@ describe('Autocomplete component', () => { fixture.detectChanges(); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(0); enterSearch('red', fixture); tick(); @@ -1083,12 +1123,12 @@ describe('Autocomplete component', () => { expect( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ).toBe('red'); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(1); })); @@ -1103,12 +1143,12 @@ describe('Autocomplete component', () => { expect( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ).toBe('bla'); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(1); enterSearch('bl', fixture); tick(); @@ -1116,12 +1156,12 @@ describe('Autocomplete component', () => { expect( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ).toBe('bl'); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(2); })); @@ -1139,7 +1179,7 @@ describe('Autocomplete component', () => { enterSearch('mis', fixture); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(2); })); @@ -1157,12 +1197,12 @@ describe('Autocomplete component', () => { expect(['al', 'ål']).toContain( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(2); })); @@ -1183,12 +1223,12 @@ describe('Autocomplete component', () => { expect( getSearchResultsContainer() - .querySelector('mark') - .innerHTML.trim() + ?.querySelector('mark') + ?.innerHTML.trim() .toLowerCase() ).toContain('united'); expect( - getSearchResultsContainer().querySelectorAll('mark').length + getSearchResultsContainer()?.querySelectorAll('mark').length ).toBe(2); })); }); @@ -1519,7 +1559,7 @@ describe('Autocomplete component', () => { const input: SkyAutocompleteInputDirective = component.autocompleteInput; const inputElement: HTMLInputElement = getInputElement(); - const selectedValue: { name: string } = undefined; + const selectedValue: { name: string } | undefined = undefined; updateNgModel(fixture, selectedValue); blurInput(inputElement, fixture); @@ -1837,24 +1877,24 @@ describe('Autocomplete component', () => { tick(); // Expect untouched and pristine. - expect(component.reactiveForm.touched).toEqual(false); - expect(component.reactiveForm.untouched).toEqual(true); - expect(component.reactiveForm.dirty).toEqual(false); - expect(component.reactiveForm.pristine).toEqual(true); + expect(component.reactiveForm?.touched).toEqual(false); + expect(component.reactiveForm?.untouched).toEqual(true); + expect(component.reactiveForm?.dirty).toEqual(false); + expect(component.reactiveForm?.pristine).toEqual(true); })); it('should set form states properly when initialized with a value', fakeAsync(function () { fixture.detectChanges(); tick(); - component.reactiveForm.get('favoriteColor').patchValue({ + component.reactiveForm?.get('favoriteColor')?.patchValue({ name: 'Red', }); // Expect untouched and pristine. - expect(component.reactiveForm.touched).toEqual(false); - expect(component.reactiveForm.untouched).toEqual(true); - expect(component.reactiveForm.dirty).toEqual(false); - expect(component.reactiveForm.pristine).toEqual(true); + expect(component.reactiveForm?.touched).toEqual(false); + expect(component.reactiveForm?.untouched).toEqual(true); + expect(component.reactiveForm?.dirty).toEqual(false); + expect(component.reactiveForm?.pristine).toEqual(true); })); it('should mark the control as touched on blur', fakeAsync(function () { @@ -1864,10 +1904,10 @@ describe('Autocomplete component', () => { blurInput(inputElement, fixture); // Expect touched and pristine. - expect(component.reactiveForm.touched).toEqual(true); - expect(component.reactiveForm.untouched).toEqual(false); - expect(component.reactiveForm.dirty).toEqual(false); - expect(component.reactiveForm.pristine).toEqual(true); + expect(component.reactiveForm?.touched).toEqual(true); + expect(component.reactiveForm?.untouched).toEqual(false); + expect(component.reactiveForm?.dirty).toEqual(false); + expect(component.reactiveForm?.pristine).toEqual(true); })); it('should mark the control as dirty when search value changes', fakeAsync(function () { @@ -1877,21 +1917,21 @@ describe('Autocomplete component', () => { enterSearch('r', fixture); // Expect untouched and pristine, because we haven't selected a search result yet. - expect(component.reactiveForm.touched).toEqual(false); - expect(component.reactiveForm.untouched).toEqual(true); - expect(component.reactiveForm.dirty).toEqual(false); - expect(component.reactiveForm.pristine).toEqual(true); + expect(component.reactiveForm?.touched).toEqual(false); + expect(component.reactiveForm?.untouched).toEqual(true); + expect(component.reactiveForm?.dirty).toEqual(false); + expect(component.reactiveForm?.pristine).toEqual(true); searchAndSelect('r', 0, fixture); // Expect touched and dirty. - expect(component.reactiveForm.touched).toEqual(true); - expect(component.reactiveForm.untouched).toEqual(false); - expect(component.reactiveForm.dirty).toEqual(true); - expect(component.reactiveForm.pristine).toEqual(false); + expect(component.reactiveForm?.touched).toEqual(true); + expect(component.reactiveForm?.untouched).toEqual(false); + expect(component.reactiveForm?.dirty).toEqual(true); + expect(component.reactiveForm?.pristine).toEqual(false); // Expect model to be set. - expect(component.reactiveForm.value).toEqual({ + expect(component.reactiveForm?.value).toEqual({ favoriteColor: { name: 'Red' }, }); })); @@ -1899,20 +1939,20 @@ describe('Autocomplete component', () => { it('should mark the control as dirty when search value changes when initialized with a value', fakeAsync(function () { fixture.detectChanges(); tick(); - component.reactiveForm.get('favoriteColor').patchValue({ + component.reactiveForm?.get('favoriteColor')?.patchValue({ name: 'Purple', }); searchAndSelect('r', 0, fixture); // Expect touched and dirty. - expect(component.reactiveForm.touched).toEqual(true); - expect(component.reactiveForm.untouched).toEqual(false); - expect(component.reactiveForm.dirty).toEqual(true); - expect(component.reactiveForm.pristine).toEqual(false); + expect(component.reactiveForm?.touched).toEqual(true); + expect(component.reactiveForm?.untouched).toEqual(false); + expect(component.reactiveForm?.dirty).toEqual(true); + expect(component.reactiveForm?.pristine).toEqual(false); // Expect model to be set. - expect(component.reactiveForm.value).toEqual({ + expect(component.reactiveForm?.value).toEqual({ favoriteColor: { name: 'Red' }, }); })); @@ -1927,7 +1967,10 @@ describe('Autocomplete component', () => { tick(); fixture.detectChanges(); - const spy = spyOn(component.autocomplete, 'search').and.callThrough(); + const spy = spyOn( + component.autocomplete, + 'searchOrDefault' + ).and.callThrough(); enterSearch('r', fixture); blurInput(inputElement, fixture); 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 389e2ae291..751f35886b 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts @@ -81,11 +81,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set data(value: any[] | undefined) { - this.#_data = value; + this.#_data = value ?? []; } public get data(): any[] { - return this.#_data || []; + return this.#_data; } /** @@ -95,11 +95,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set debounceTime(value: number | undefined) { - this.#_debounceTime = value; + this.#_debounceTime = value ?? 0; } public get debounceTime(): number { - return this.#_debounceTime || 0; + return this.#_debounceTime; } /** @@ -109,11 +109,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set descriptorProperty(value: string | undefined) { - this.#_descriptorProperty = value; + this.#_descriptorProperty = value || 'name'; } public get descriptorProperty(): string { - return this.#_descriptorProperty || 'name'; + return this.#_descriptorProperty; } /** @@ -121,7 +121,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { * Indicates whether to display a button in the dropdown that opens a picker where users can view all options. */ @Input() - public enableShowMore = false; + public enableShowMore: boolean | undefined = false; /** * Specifies an observable of `SkyAutocompleteMessage` that can close the dropdown. @@ -150,11 +150,13 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set propertiesToSearch(value: string[] | undefined) { - this.#_propertiesToSearch = value; + this.#_propertiesToSearch = value ?? ['name']; + + this.#updateDefaultSearchOptions(); } public get propertiesToSearch(): string[] { - return this.#_propertiesToSearch || ['name']; + return this.#_propertiesToSearch; } /** @@ -167,16 +169,16 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { @Input() public set search(value: SkyAutocompleteSearchFunction | undefined) { this.#_search = value; - } - - public get search(): SkyAutocompleteSearchFunction { - return ( - this.#_search || + this.searchOrDefault = + value || skyAutocompleteDefaultSearchFunction({ propertiesToSearch: this.propertiesToSearch, searchFilters: this.searchFilters, - }) - ); + }); + } + + public get search(): SkyAutocompleteSearchFunction | undefined { + return this.#_search; } /** @@ -195,14 +197,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set searchTextMinimumCharacters(value: number | undefined) { - this.#_searchTextMinimumCharacters = value; + this.#_searchTextMinimumCharacters = value && value > 0 ? value : 1; } public get searchTextMinimumCharacters(): number { - return this.#_searchTextMinimumCharacters && - this.#_searchTextMinimumCharacters > 0 - ? this.#_searchTextMinimumCharacters - : 1; + return this.#_searchTextMinimumCharacters; } /** @@ -214,7 +213,19 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { * @deprecated We recommend against using this property. To filter results, use the `searchAsync` event instead. */ @Input() - public searchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; + public set searchFilters( + value: SkyAutocompleteSearchFunctionFilter[] | undefined + ) { + this.#_searchFilters = value; + + this.#updateDefaultSearchOptions(); + } + + public get searchFilters(): + | SkyAutocompleteSearchFunctionFilter[] + | undefined { + return this.#_searchFilters; + } /** * Specifies the maximum number of search results to display in the dropdown list. @@ -222,23 +233,20 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { */ @Input() public set searchResultsLimit(value: number | undefined) { - this.#_searchResultsLimit = value; + this.#_searchResultsLimit = value || 0; } - public get searchResultsLimit(): number | undefined { - if (this.#_searchResultsLimit && this.#_searchResultsLimit > 0) { - return this.#_searchResultsLimit; - } else { - return this.enableShowMore ? 5 : this.#_searchResultsLimit; - } + public get searchResultsLimit(): number { + return this.#_searchResultsLimit; } /** * @internal * Indicates whether to display a button that lets users add options to the data source. + * @default false */ @Input() - public showAddButton = false; + public showAddButton: boolean | undefined = false; /** * Specifies the text to display when no search results are found. @@ -251,9 +259,10 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { * @internal * Allows async search to be disabled even when a listener is specified for * the `searchAsync` output. + * @default false */ @Input() - public searchAsyncDisabled = false; + public searchAsyncDisabled: boolean | undefined = false; /** * @internal @@ -311,7 +320,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { public searchText = ''; public get showActionsArea(): boolean { - return this.showAddButton || this.enableShowMore; + return !!this.showAddButton || !!this.enableShowMore; } public isSearchingAsync = false; @@ -321,7 +330,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { //#endregion @ContentChild(SkyAutocompleteInputDirective) - private set inputDirective( + public set inputDirective( directive: SkyAutocompleteInputDirective | undefined ) { if (!directive) { @@ -373,19 +382,19 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { } } - private get inputDirective(): SkyAutocompleteInputDirective | undefined { + public get inputDirective(): SkyAutocompleteInputDirective | undefined { return this.#_inputDirective; } @ViewChild('resultsTemplateRef', { read: TemplateRef, }) - private resultsTemplateRef: TemplateRef | undefined; + public resultsTemplateRef: TemplateRef | undefined; @ViewChild('resultsRef', { read: ElementRef, }) - private set resultsRef(value: ElementRef | undefined) { + public set resultsRef(value: ElementRef | undefined) { if (value) { this.#_resultsRef = value; this.#destroyAffixer(); @@ -393,10 +402,12 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { } } - private get resultsRef(): ElementRef | undefined { + public get resultsRef(): ElementRef | undefined { return this.#_resultsRef; } + public searchOrDefault: SkyAutocompleteSearchFunction; + /** * Index that indicates which descendant of the overlay currently has focus. */ @@ -433,11 +444,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { #currentSearchSub: Subscription | undefined; - #_data: any[] | undefined; + #_data: any[] = []; - #_debounceTime: number | undefined; + #_debounceTime = 0; - #_descriptorProperty: string | undefined; + #_descriptorProperty = 'name'; #_highlightText: string[] | undefined; @@ -445,17 +456,19 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { #_messageStream = new Subject(); - #_propertiesToSearch: string[] | undefined; + #_propertiesToSearch = ['name']; #_resultsRef: ElementRef | undefined; #_search: SkyAutocompleteSearchFunction | undefined; + #_searchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; + #_searchResults: SkyAutocompleteSearchResult[] | undefined; - #_searchResultsLimit: number | undefined; + #_searchResultsLimit = 0; - #_searchTextMinimumCharacters: number | undefined; + #_searchTextMinimumCharacters = 1; constructor( changeDetector: ChangeDetectorRef, @@ -472,6 +485,11 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { this.#overlayService = overlayService; this.#inputBoxHostSvc = inputBoxHostSvc; + this.searchOrDefault = skyAutocompleteDefaultSearchFunction({ + propertiesToSearch: ['name'], + searchFilters: undefined, + }); + const id = ++uniqueId; this.resultsListId = `sky-autocomplete-list-${id}`; this.resultsWrapperId = `sky-autocomplete-wrapper-${id}`; @@ -714,7 +732,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { return searchAsyncArgs.result || of(undefined); } - const result = this.search(this.searchText, this.data, { + const result = this.searchOrDefault(this.searchText, this.data, { context: 'popover', }); @@ -971,4 +989,14 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { this.#setActiveDescendant(); } } + + #updateDefaultSearchOptions(): void { + // Reset default search if it is what is being used. + if (this.search !== this.searchOrDefault) { + this.searchOrDefault = skyAutocompleteDefaultSearchFunction({ + propertiesToSearch: this.propertiesToSearch, + searchFilters: this.searchFilters, + }); + } + } } diff --git a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete-reactive.component.fixture.ts b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete-reactive.component.fixture.ts index 567ec571cf..844a367a7f 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete-reactive.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete-reactive.component.fixture.ts @@ -31,7 +31,7 @@ export class SkyAutocompleteReactiveFixtureComponent implements OnInit { read: SkyAutocompleteComponent, static: true, }) - public autocomplete: SkyAutocompleteComponent | undefined; + public autocomplete!: SkyAutocompleteComponent; #formBuilder: UntypedFormBuilder; diff --git a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.ts b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.ts index ea07be00ce..246845a000 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.ts @@ -19,7 +19,9 @@ import { SkyAutocompleteSelectionChange } from '../types/autocomplete-selection- export class SkyAutocompleteFixtureComponent { public autocompleteAttribute: string | undefined; - public data: { objectid?: string; name?: string; text?: string }[] = [ + public data: + | { objectid?: string; name?: string; text?: string }[] + | undefined = [ { name: 'Red', objectid: 'abc' }, { name: 'Blue', objectid: 'def' }, { name: 'Green', objectid: 'ghi' }, @@ -34,7 +36,9 @@ export class SkyAutocompleteFixtureComponent { ]; public model: { - favoriteColor: { objectid?: string; name?: string; text?: string }; + favoriteColor: + | { objectid?: string; name?: string; text?: string } + | undefined; } = { favoriteColor: {}, }; @@ -42,8 +46,8 @@ export class SkyAutocompleteFixtureComponent { public customNoResultsMessage: string | undefined; public debounceTime: number | undefined; public descriptorProperty: string | undefined; - public disabled = false; - public enableShowMore = false; + public disabled: boolean | undefined = false; + public enableShowMore: boolean | undefined = false; public hideInput = false; public propertiesToSearch: string[] | undefined; public messageStream: Subject | undefined; @@ -53,7 +57,7 @@ export class SkyAutocompleteFixtureComponent { public searchResultTemplate: TemplateRef | undefined; public searchTextMinimumCharacters: number | undefined; public selectionFromChangeEvent: SkyAutocompleteSelectionChange | undefined; - public showAddButton = false; + public showAddButton: boolean | undefined = false; @ViewChild('asyncAutocomplete', { read: SkyAutocompleteComponent, @@ -97,10 +101,11 @@ export class SkyAutocompleteFixtureComponent { public searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { const searchText = (args.searchText || '').toLowerCase(); - const filteredData = this.data.filter((item) => { - const index = item?.name?.toLowerCase()?.indexOf(searchText); - return index !== undefined && index >= 0; - }); + const filteredData = + this.data?.filter((item) => { + const index = item?.name?.toLowerCase()?.indexOf(searchText); + return index !== undefined && index >= 0; + }) ?? []; args.result = of({ items: filteredData, diff --git a/libs/components/lookup/src/lib/modules/autocomplete/types/autocomplete-default-search-function-options.ts b/libs/components/lookup/src/lib/modules/autocomplete/types/autocomplete-default-search-function-options.ts index e011c8d099..6d5f71c2fa 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/types/autocomplete-default-search-function-options.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/types/autocomplete-default-search-function-options.ts @@ -4,7 +4,7 @@ import { SkyAutocompleteSearchFunctionFilter } from './autocomplete-search-funct * @internal */ export interface SkyAutocompleteDefaultSearchFunctionOptions { - propertiesToSearch?: string[]; + propertiesToSearch: string[]; searchFilters?: SkyAutocompleteSearchFunctionFilter[]; /** @deprecated */ searchResultsLimit?: number; diff --git a/libs/components/lookup/src/lib/modules/country-field/country-field.component.spec.ts b/libs/components/lookup/src/lib/modules/country-field/country-field.component.spec.ts index b1588c44e4..4516f6722e 100644 --- a/libs/components/lookup/src/lib/modules/country-field/country-field.component.spec.ts +++ b/libs/components/lookup/src/lib/modules/country-field/country-field.component.spec.ts @@ -97,7 +97,7 @@ describe('Country Field Component', () => { value: string, flag?: string ): void { - expect(nativeElement.querySelector('textarea').value).toBe(value); + expect(nativeElement.querySelector('textarea')?.value).toBe(value); const flagEl = nativeElement.querySelector('.sky-country-field-flag'); @@ -106,7 +106,7 @@ describe('Country Field Component', () => { } if (flag) { - const flagInnerEl = flagEl.querySelector('.iti__flag'); + const flagInnerEl = flagEl?.querySelector('.iti__flag'); expect(flagInnerEl).toHaveCssClass('iti__' + flag); } @@ -281,7 +281,7 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[0]).toHaveText('Cyprus (Κύπρος)'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -295,11 +295,11 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Australia'); + expect(results[1]).toHaveText('Australia'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__au'); @@ -318,10 +318,10 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[1]).toHaveText('Cyprus (Κύπρος)'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -375,11 +375,11 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect( - textAreaElement.attributes.getNamedItem('disabled') + textAreaElement?.attributes.getNamedItem('disabled') ).not.toBeNull(); SkyAppTestUtility.fireDomEvent(textAreaElement, 'mousedown'); SkyAppTestUtility.fireDomEvent(textAreaElement, 'focusin'); @@ -398,7 +398,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect( @@ -487,7 +487,19 @@ describe('Country Field Component', () => { iso2: 'au', }); - const searchResults = searchAndGetResults('Austr', fixture); + let searchResults = searchAndGetResults('Austr', fixture); + expect(searchResults[0].querySelector('.sky-deemphasized')).toBeNull(); + + component.countryFieldComponent.includePhoneInfo = undefined; + searchAndSelect('Austr', 0, fixture); + fixture.detectChanges(); + tick(); + expect(changeEventSpy).toHaveBeenCalledWith({ + name: 'Australia', + iso2: 'au', + }); + + searchResults = searchAndGetResults('Austr', fixture); expect(searchResults[0].querySelector('.sky-deemphasized')).toBeNull(); })); @@ -518,9 +530,9 @@ describe('Country Field Component', () => { }); const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-deemphasized').textContent.trim() - ).toBe('61'); + expect(searchResults[0].querySelector('.sky-deemphasized')).toHaveText( + '61' + ); })); it('should not hide the flag in the input box if the `hideSelectedCountryFlag` is not set', fakeAsync(() => { @@ -736,7 +748,7 @@ describe('Country Field Component', () => { tick(); fixture.detectChanges(); - expect(nativeElement.querySelector('textarea').value).toBe(''); + expect(nativeElement.querySelector('textarea')?.value).toBe(''); expect( nativeElement.querySelector('.sky-country-field-flag') ).toBeNull(); @@ -773,7 +785,7 @@ describe('Country Field Component', () => { validateSelectedCountry(nativeElement, 'United States', 'us'); - component.countryControl.setValue({ + component.countryControl?.setValue({ name: 'Australia', iso2: 'au', }); @@ -790,7 +802,7 @@ describe('Country Field Component', () => { tick(); fixture.detectChanges(); - component.countryControl.setValue({ + component.countryControl?.setValue({ name: 'United States', iso2: 'us', }); @@ -813,7 +825,7 @@ describe('Country Field Component', () => { validateSelectedCountry(nativeElement, 'United States', 'us'); - component.countryControl.setValue({ + component.countryControl?.setValue({ name: 'Test Name', iso2: 'au', }); @@ -836,7 +848,7 @@ describe('Country Field Component', () => { validateSelectedCountry(nativeElement, 'United States', 'us'); - component.countryControl.setValue({ + component.countryControl?.setValue({ name: 'Australia', iso2: 'au', }); @@ -856,7 +868,7 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[0]).toHaveText('Cyprus (Κύπρος)'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -870,11 +882,11 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Australia'); + expect(results[1]).toHaveText('Australia'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__au'); @@ -890,11 +902,11 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Australia'); + expect(results[1]).toHaveText('Australia'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__au'); @@ -938,10 +950,10 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[1]).toHaveText('Cyprus (Κύπρος)'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -962,8 +974,8 @@ describe('Country Field Component', () => { tick(); fixture.detectChanges(); - expect(component.countryControl.value).toBeUndefined(); - expect(nativeElement.querySelector('textarea').value).toBe(''); + expect(component.countryControl?.value).toBeUndefined(); + expect(nativeElement.querySelector('textarea')?.value).toBe(''); expect( nativeElement.querySelector('.sky-country-field-flag') ).toBeNull(); @@ -981,7 +993,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect(component.countryFieldComponent.disabled).toBeTruthy(); @@ -1004,7 +1016,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect(component.countryFieldComponent.disabled).toBeTruthy(); @@ -1030,13 +1042,13 @@ describe('Country Field Component', () => { it('should mark the form as touched when the form loses focus', fakeAsync(() => { fixture.detectChanges(); const textAreaElement = getInputElement(); - expect(component.countryForm.touched).toEqual(false); + expect(component.countryForm?.touched).toEqual(false); SkyAppTestUtility.fireDomEvent(textAreaElement, 'blur'); tick(); fixture.detectChanges(); - expect(component.countryForm.touched).toEqual(true); + expect(component.countryForm?.touched).toEqual(true); })); it('should emit the countryChange event correctly', fakeAsync(() => { @@ -1185,9 +1197,9 @@ describe('Country Field Component', () => { }); const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-deemphasized').textContent.trim() - ).toBe('61'); + expect(searchResults[0].querySelector('.sky-deemphasized')).toHaveText( + '61' + ); })); it('should not hide the flag in the input box if the `hideSelectedCountryFlag` is not set', fakeAsync(() => { @@ -1225,12 +1237,12 @@ describe('Country Field Component', () => { component.isRequired = true; fixture.detectChanges(); tick(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(false); + expect(component.countryForm?.valid).toEqual(false); })); it('should mark the form valid when it is set and required', fakeAsync(() => { @@ -1242,12 +1254,12 @@ describe('Country Field Component', () => { component.isRequired = true; fixture.detectChanges(); tick(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(true); + expect(component.countryForm?.valid).toEqual(true); })); it('should mark the form invalid when it is set to a non-real country', fakeAsync(() => { @@ -1256,12 +1268,12 @@ describe('Country Field Component', () => { iso2: 'xx', }; fixture.detectChanges(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(false); + expect(component.countryForm?.valid).toEqual(false); })); it('should mark the form valid when it is set to a real country', fakeAsync(() => { @@ -1270,12 +1282,12 @@ describe('Country Field Component', () => { iso2: 'us', }; fixture.detectChanges(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(true); + expect(component.countryForm?.valid).toEqual(true); })); it('should mark the form valid when it is set to a supported country', fakeAsync(() => { @@ -1285,12 +1297,12 @@ describe('Country Field Component', () => { }; component.supportedCountryISOs = ['au', 'de']; fixture.detectChanges(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(true); + expect(component.countryForm?.valid).toEqual(true); })); it('should mark the form invalid when it is set to a non-supported country', fakeAsync(() => { @@ -1300,12 +1312,12 @@ describe('Country Field Component', () => { }; component.supportedCountryISOs = ['au', 'de']; fixture.detectChanges(); - component.countryControl.updateValueAndValidity(); + component.countryControl?.updateValueAndValidity(); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(component.countryForm.valid).toEqual(false); + expect(component.countryForm?.valid).toEqual(false); })); }); @@ -1365,7 +1377,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(nativeElement.querySelector('textarea').value).toBe(''); + expect(nativeElement.querySelector('textarea')?.value).toBe(''); searchAndSelect('Austr', 0, fixture); @@ -1382,7 +1394,7 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[0]).toHaveText('Cyprus (Κύπρος)'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -1396,11 +1408,11 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('United States'); + expect(results[0]).toHaveText('United States'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__us'); - expect(results[1].innerText.trim()).toBe('Australia'); + expect(results[1]).toHaveText('Australia'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__au'); @@ -1421,10 +1433,10 @@ describe('Country Field Component', () => { const results = searchAndGetResults('us', fixture); - expect(results[0].innerText.trim()).toBe('Australia'); + expect(results[0]).toHaveText('Australia'); expect(results[0].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[0].querySelector('div')).toHaveCssClass('iti__au'); - expect(results[1].innerText.trim()).toBe('Cyprus (Κύπρος)'); + expect(results[1]).toHaveText('Cyprus (Κύπρος)'); expect(results[1].querySelector('div')).toHaveCssClass('iti__flag'); expect(results[1].querySelector('div')).toHaveCssClass('iti__cy'); })); @@ -1447,7 +1459,7 @@ describe('Country Field Component', () => { tick(); fixture.detectChanges(); - expect(nativeElement.querySelector('textarea').value).toBe(''); + expect(nativeElement.querySelector('textarea')?.value).toBe(''); expect( nativeElement.querySelector('.sky-country-field-flag') ).toBeNull(); @@ -1463,7 +1475,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect(component.countryFieldComponent.disabled).toBeTruthy(); @@ -1482,7 +1494,7 @@ describe('Country Field Component', () => { fixture.detectChanges(); tick(); - const textAreaElement: HTMLElement = + const textAreaElement: HTMLElement | null = nativeElement.querySelector('textarea'); expect(component.countryFieldComponent.disabled).toBeTruthy(); @@ -1567,9 +1579,9 @@ describe('Country Field Component', () => { }); const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-deemphasized').textContent.trim() - ).toBe('61'); + expect(searchResults[0].querySelector('.sky-deemphasized')).toHaveText( + '61' + ); })); it('should not hide the flag in the input box if the `hideSelectedCountryFlag` is not set', fakeAsync(() => { @@ -1585,6 +1597,19 @@ describe('Country Field Component', () => { expect( nativeElement.querySelector('.sky-country-field-flag') ).not.toBeNull(); + + component.countryFieldComponent.hideSelectedCountryFlag = undefined; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + searchAndSelect('Austr', 0, fixture); + fixture.detectChanges(); + tick(); + + expect( + nativeElement.querySelector('.sky-country-field-flag') + ).not.toBeNull(); })); it('should hide the flag in the input box if the `hideSelectedCountryFlag` is set', fakeAsync(() => { @@ -1690,10 +1715,10 @@ describe('Country Field Component', () => { const inputBoxEl = nativeElement.querySelector('sky-input-box'); - const inputGroupEl = inputBoxEl.querySelector( + const inputGroupEl = inputBoxEl?.querySelector( '.sky-input-box-input-group-inner' ); - const containerEl = inputGroupEl.children.item(1); + const containerEl = inputGroupEl?.children.item(1); expect(containerEl).toHaveCssClass('sky-country-field-container'); })); @@ -1703,14 +1728,16 @@ describe('Country Field Component', () => { tick(); const inputBoxEl = nativeElement.querySelector('sky-input-box'); - let inputBoxInsetIcon = inputBoxEl.querySelector( + let inputBoxInsetIcon = inputBoxEl?.querySelector( '.sky-input-box-icon-inset' ); expect(inputBoxInsetIcon).toBeNull(); setModernTheme(); - inputBoxInsetIcon = inputBoxEl.querySelector('.sky-input-box-icon-inset'); + inputBoxInsetIcon = inputBoxEl?.querySelector( + '.sky-input-box-icon-inset' + ); expect(inputBoxInsetIcon).not.toBeNull(); })); @@ -1719,12 +1746,14 @@ describe('Country Field Component', () => { tick(); const input = nativeElement.querySelector('.sky-form-control'); - expect(input.getAttribute('placeholder')).toEqual('Search for a country'); + expect(input?.getAttribute('placeholder')).toEqual( + 'Search for a country' + ); setModernTheme(); const modernInput = nativeElement.querySelector('.sky-form-control'); - expect(modernInput.getAttribute('placeholder')).toEqual(''); + expect(modernInput?.getAttribute('placeholder')).toEqual(''); })); }); }); diff --git a/libs/components/lookup/src/lib/modules/country-field/country-field.component.ts b/libs/components/lookup/src/lib/modules/country-field/country-field.component.ts index 9d9c27ff94..3044d1bc6b 100644 --- a/libs/components/lookup/src/lib/modules/country-field/country-field.component.ts +++ b/libs/components/lookup/src/lib/modules/country-field/country-field.component.ts @@ -61,7 +61,6 @@ export class SkyCountryFieldComponent { /** * Specifies the value for the `autocomplete` attribute on the form input. - * @default "off" */ @Input() public autocompleteAttribute: string | undefined; @@ -98,14 +97,14 @@ export class SkyCountryFieldComponent * @default false */ @Input() - public set disabled(isDisabled: boolean) { + public set disabled(isDisabled: boolean | undefined) { this.#removeEventListeners(); if (!isDisabled) { - this.addEventListeners(); + this.#addEventListeners(); } - this.#_disabled = isDisabled; + this.#_disabled = isDisabled ?? false; this.#changeDetector.markForCheck(); } @@ -116,23 +115,30 @@ export class SkyCountryFieldComponent /** * Indicates whether to hide the flag in the input element. + * @default false */ @Input() - public hideSelectedCountryFlag = false; + public set hideSelectedCountryFlag(value: boolean | undefined) { + this.#_hideSelectedCountryFlag = value ?? false; + } + + public get hideSelectedCountryFlag(): boolean { + return this.#_hideSelectedCountryFlag; + } /** * Indicates whether to include phone information in the selected country and country dropdown. * @default false */ @Input() - public set includePhoneInfo(value: boolean) { - this.#_includePhoneInfo = value; + public set includePhoneInfo(value: boolean | undefined) { + this.#_includePhoneInfo = value ?? false; this.#setupCountries(); } public get includePhoneInfo(): boolean { - return this.#_includePhoneInfo || false; + return this.#_includePhoneInfo; } /** @@ -234,13 +240,13 @@ export class SkyCountryFieldComponent read: TemplateRef, static: true, }) - private inputTemplateRef: TemplateRef | undefined; + public inputTemplateRef: TemplateRef | undefined; @ViewChild('searchIconTemplateRef', { read: TemplateRef, static: true, }) - private searchIconTemplateRef: TemplateRef | undefined; + public searchIconTemplateRef: TemplateRef | undefined; #changeDetector: ChangeDetectorRef; @@ -268,6 +274,8 @@ export class SkyCountryFieldComponent #_disabled = false; + #_hideSelectedCountryFlag = false; + #_includePhoneInfo = false; #_selectedCountry: SkyCountryFieldCountry | undefined; @@ -330,7 +338,7 @@ export class SkyCountryFieldComponent }); if (!this.disabled) { - this.addEventListeners(); + this.#addEventListeners(); } } @@ -424,7 +432,7 @@ export class SkyCountryFieldComponent this.#changeDetector.markForCheck(); } - private addEventListeners(): void { + #addEventListeners(): void { this.#removeEventListeners(); this.#idle = new Subject(); @@ -532,7 +540,7 @@ export class SkyCountryFieldComponent if ( ((this.#defaultCountryData && this.#countriesEqual(a, this.#defaultCountryData)) || - a.name! < b.name!) && + (a.name && b.name && a.name < b.name)) && (!this.#defaultCountryData || !this.#countriesEqual(this.#defaultCountryData, b)) ) { diff --git a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-reactive.component.fixture.ts b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-reactive.component.fixture.ts index 4c95cda441..cc92b85243 100644 --- a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-reactive.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-reactive.component.fixture.ts @@ -20,7 +20,7 @@ export class CountryFieldReactiveTestComponent implements OnInit { public countryForm: UntypedFormGroup | undefined; - public countryControl!: UntypedFormControl; + public countryControl: UntypedFormControl | undefined; public initialValue: SkyCountryFieldCountry | undefined; @@ -29,38 +29,38 @@ export class CountryFieldReactiveTestComponent implements OnInit { public supportedCountryISOs: string[] | undefined; public set isDisabled(value: boolean) { - this._isDisabled = value; + this.#_isDisabled = value; - if (this._isDisabled) { - this.countryControl.disable(); + if (this.#_isDisabled) { + this.countryControl?.disable(); } else { - this.countryControl.enable(); + this.countryControl?.enable(); } } public get isDisabled(): boolean { - return this._isDisabled; + return this.#_isDisabled; } public set isRequired(value: boolean) { - this._isRequired = value; + this.#_isRequired = value; - if (this._isRequired) { - this.countryControl.setValidators([Validators.required]); + if (this.#_isRequired) { + this.countryControl?.setValidators([Validators.required]); } else { - this.countryControl.setValidators([]); + this.countryControl?.setValidators([]); } } public get isRequired(): boolean { - return this._isRequired; + return this.#_isRequired; } public defaultCountry: string | undefined; - private _isDisabled = false; + #_isDisabled = false; - private _isRequired = false; + #_isRequired = false; public ngOnInit(): void { this.countryControl = new UntypedFormControl(); @@ -87,6 +87,6 @@ export class CountryFieldReactiveTestComponent implements OnInit { } public setValue(country: SkyCountryFieldCountry): void { - this.countryControl.setValue(country); + this.countryControl?.setValue(country); } } 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 f4c1c9aba1..51d3c586a5 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 @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormControl, @@ -11,7 +11,7 @@ import { SkyLookupComponent } from '../lookup.component'; selector: 'sky-test-cmp', templateUrl: './lookup-input-box.component.fixture.html', }) -export class SkyLookupInputBoxTestComponent implements OnInit { +export class SkyLookupInputBoxTestComponent { @ViewChild(SkyLookupComponent, { static: true, }) @@ -25,15 +25,9 @@ export class SkyLookupInputBoxTestComponent implements OnInit { public friends: any[] = []; - public form!: UntypedFormGroup; - - #formBuilder: UntypedFormBuilder; + public form: UntypedFormGroup; constructor(formBuilder: UntypedFormBuilder) { - this.#formBuilder = formBuilder; - } - - public ngOnInit(): void { this.data = [ { id: 1, name: 'Andy' }, { id: 2, name: 'Beth' }, @@ -54,11 +48,7 @@ export class SkyLookupInputBoxTestComponent implements OnInit { { id: 17, name: 'Zack' }, ]; - this.#createForm(); - } - - #createForm(): void { - this.form = this.#formBuilder.group({ + this.form = formBuilder.group({ friends: new UntypedFormControl(this.friends), }); } diff --git a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-template.component.fixture.ts b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-template.component.fixture.ts index c0477ebf8d..5b0b4a060e 100644 --- a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-template.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup-template.component.fixture.ts @@ -44,13 +44,13 @@ export class SkyLookupTemplateTestComponent implements OnInit { public descriptorProperty: string | undefined; - public disabled = false; + public disabled: boolean | undefined = false; public enabledSearchResultTemplate: TemplateRef | undefined; - public enableShowMore = false; + public enableShowMore: boolean | undefined = false; - public idProperty = 'name'; + public idProperty: string | undefined = 'name'; public ignoreAddDataUpdate = false; @@ -66,9 +66,9 @@ export class SkyLookupTemplateTestComponent implements OnInit { public selectMode: SkyLookupSelectModeType | undefined; - public showAddButton = false; + public showAddButton: boolean | undefined = false; - public showMoreConfig: SkyLookupShowMoreConfig = {}; + public showMoreConfig: SkyLookupShowMoreConfig | undefined = {}; public ngOnInit(): void { this.data = [ @@ -129,11 +129,13 @@ export class SkyLookupTemplateTestComponent implements OnInit { } public enableCustomPicker(): void { - this.showMoreConfig.customPicker = { - open: (context: SkyLookupShowMoreCustomPickerContext) => { - return; - }, - }; + if (this.showMoreConfig) { + this.showMoreConfig.customPicker = { + open: (context: SkyLookupShowMoreCustomPickerContext) => { + return; + }, + }; + } } public enableSearchResultTemplate(): void { @@ -181,7 +183,9 @@ export class SkyLookupTemplateTestComponent implements OnInit { public setShowMoreNativePickerConfig( config: SkyLookupShowMoreNativePickerConfig ): void { - this.showMoreConfig.nativePickerConfig = config; + if (this.showMoreConfig) { + this.showMoreConfig.nativePickerConfig = config; + } } public setSingleSelect(): void { diff --git a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup.component.fixture.ts b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup.component.fixture.ts index c9220539bb..a7316f8379 100644 --- a/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/lookup/fixtures/lookup.component.fixture.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { Component, TemplateRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormControl, @@ -24,7 +24,7 @@ import { SkyLookupShowMoreNativePickerConfig } from '../types/lookup-show-more-n selector: 'sky-test-cmp', templateUrl: './lookup.component.fixture.html', }) -export class SkyLookupTestComponent implements OnInit { +export class SkyLookupTestComponent { @ViewChild('asyncLookup', { read: SkyLookupComponent, static: true, @@ -47,7 +47,7 @@ export class SkyLookupTestComponent implements OnInit { public ariaLabelledBy: string | undefined; - public asyncForm!: UntypedFormGroup; + public asyncForm: UntypedFormGroup; public autocompleteAttribute: string | undefined; @@ -55,17 +55,17 @@ export class SkyLookupTestComponent implements OnInit { public customSearchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; - public data: any[] = []; + public data: any[] | undefined = []; public descriptorProperty: string | undefined; public enabledSearchResultTemplate: TemplateRef | undefined; - public enableShowMore = false; + public enableShowMore: boolean | undefined = false; - public form!: UntypedFormGroup; + public form: UntypedFormGroup; - public idProperty = 'name'; + public idProperty: string | undefined = 'name'; public ignoreAddDataUpdate = false; @@ -75,9 +75,9 @@ export class SkyLookupTestComponent implements OnInit { public selectMode: SkyLookupSelectModeType | undefined; - public showAddButton = false; + public showAddButton: boolean | undefined = false; - public showMoreConfig: SkyLookupShowMoreConfig = {}; + public showMoreConfig: SkyLookupShowMoreConfig | undefined = {}; public get friends(): any[] { return this.#_friends; @@ -94,15 +94,9 @@ export class SkyLookupTestComponent implements OnInit { } } - #formBuilder: UntypedFormBuilder; - #_friends: any[] = []; constructor(formBuilder: UntypedFormBuilder) { - this.#formBuilder = formBuilder; - } - - public ngOnInit(): void { this.data = [ { name: 'Andy', @@ -139,14 +133,19 @@ export class SkyLookupTestComponent implements OnInit { { name: 'Zack' }, ]; - this.#createForm(); + this.asyncForm = formBuilder.group({ + friends: new UntypedFormControl(this.friends), + }); + this.form = formBuilder.group({ + friends: new UntypedFormControl(this.friends), + }); } public addButtonClicked( addButtonClickArgs: SkyLookupAddClickEventArgs ): void { const newItem = { name: 'New item' }; - const newItems = [newItem].concat(this.data); + const newItems = [newItem].concat(this.data ?? []); const callbackArgs: SkyLookupAddCallbackArgs = { item: newItem, data: this.ignoreAddDataUpdate ? undefined : newItems, @@ -155,11 +154,13 @@ export class SkyLookupTestComponent implements OnInit { } public enableCustomPicker(): void { - this.showMoreConfig.customPicker = { - open: (context: SkyLookupShowMoreCustomPickerContext) => { - return; - }, - }; + if (this.showMoreConfig) { + this.showMoreConfig.customPicker = { + open: (context: SkyLookupShowMoreCustomPickerContext) => { + return; + }, + }; + } } public enableLookup(): void { @@ -219,7 +220,9 @@ export class SkyLookupTestComponent implements OnInit { public setShowMoreNativePickerConfig( config: SkyLookupShowMoreNativePickerConfig ): void { - this.showMoreConfig.nativePickerConfig = config; + if (this.showMoreConfig) { + this.showMoreConfig.nativePickerConfig = config; + } } public setSingleSelect(): void { @@ -227,21 +230,14 @@ export class SkyLookupTestComponent implements OnInit { } public setValue(index: number): void { - this.asyncForm.controls.friends.setValue([this.data[index]]); - this.form.controls.friends.setValue([this.data[index]]); + if (this.data) { + this.asyncForm.controls.friends.setValue([this.data[index]]); + this.form.controls.friends.setValue([this.data[index]]); + } } public removeRequired(): void { this.asyncForm.controls.friends.setValidators([]); this.form.controls.friends.setValidators([]); } - - #createForm(): void { - this.asyncForm = this.#formBuilder.group({ - friends: new UntypedFormControl(this.friends), - }); - this.form = this.#formBuilder.group({ - friends: new UntypedFormControl(this.friends), - }); - } } 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 eb8c2d1b64..2c1b5d103a 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 @@ -30,12 +30,12 @@ export class SkyLookupAutocompleteAdapter { * @default "name" */ @Input() - public set descriptorProperty(value: string) { - this.#_descriptorProperty = value; + public set descriptorProperty(value: string | undefined) { + this.#_descriptorProperty = value || 'name'; } public get descriptorProperty(): string { - return this.#_descriptorProperty || 'name'; + return this.#_descriptorProperty; } /** @@ -43,12 +43,14 @@ export class SkyLookupAutocompleteAdapter { * @default ["name"] */ @Input() - public set propertiesToSearch(value: string[]) { - this.#_propertiesToSearch = value; + public set propertiesToSearch(value: string[] | undefined) { + this.#_propertiesToSearch = value ?? ['name']; + + this.#updateDefaultSearchOptions(); } public get propertiesToSearch(): string[] { - return this.#_propertiesToSearch || ['name']; + return this.#_propertiesToSearch; } /** @@ -60,18 +62,18 @@ export class SkyLookupAutocompleteAdapter { * `search`. */ @Input() - public set search(value: SkyAutocompleteSearchFunction) { + public set search(value: SkyAutocompleteSearchFunction | undefined) { this.#_search = value; - } - - public get search(): SkyAutocompleteSearchFunction { - return ( - this.#_search || + this.searchOrDefault = + value || skyAutocompleteDefaultSearchFunction({ propertiesToSearch: this.propertiesToSearch, searchFilters: this.searchFilters, - }) - ); + }); + } + + public get search(): SkyAutocompleteSearchFunction | undefined { + return this.#_search; } /** @@ -99,7 +101,19 @@ export class SkyLookupAutocompleteAdapter { * `false` for each result to indicate whether to display it in the dropdown list. */ @Input() - public searchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; + public set searchFilters( + value: SkyAutocompleteSearchFunctionFilter[] | undefined + ) { + this.#_searchFilters = value; + + this.#updateDefaultSearchOptions(); + } + + public get searchFilters(): + | SkyAutocompleteSearchFunctionFilter[] + | undefined { + return this.#_searchFilters; + } /** * Specifies the maximum number of search results to display in the dropdown @@ -115,9 +129,26 @@ export class SkyLookupAutocompleteAdapter { @Output() public searchAsync = new EventEmitter(); - #_descriptorProperty: string | undefined; + public searchOrDefault = skyAutocompleteDefaultSearchFunction({ + propertiesToSearch: ['name'], + searchFilters: undefined, + }); + + #_descriptorProperty = 'name'; - #_propertiesToSearch: string[] | undefined; + #_propertiesToSearch = ['name']; #_search: SkyAutocompleteSearchFunction | undefined; + + #_searchFilters: SkyAutocompleteSearchFunctionFilter[] | undefined; + + #updateDefaultSearchOptions(): void { + // Reset default search if it is what is being used. + if (this.search !== this.searchOrDefault) { + this.searchOrDefault = skyAutocompleteDefaultSearchFunction({ + propertiesToSearch: this.propertiesToSearch, + searchFilters: this.searchFilters, + }); + } + } } diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup-show-more-modal.component.ts b/libs/components/lookup/src/lib/modules/lookup/lookup-show-more-modal.component.ts index 2c6877a558..9b914e04f0 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup-show-more-modal.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup-show-more-modal.component.ts @@ -129,7 +129,7 @@ export class SkyLookupShowMoreModalComponent if (isInitialValue || (initialIsArray && initialValueContainsItem)) { item.selected = true; - const itemIndex = this.items!.indexOf(item); + const itemIndex = this.items.indexOf(item); if ( selectedItems.findIndex( (selectedItem) => selectedItem.index === itemIndex @@ -181,7 +181,7 @@ export class SkyLookupShowMoreModalComponent } public onItemSelect(newSelectState: boolean, itemToSelect: any): void { - const items = this.items!; + const items = this.items; if (this.context.selectMode === 'single') { /* Sanity check - single select mode should only alow for a `true` select state */ @@ -270,7 +270,7 @@ export class SkyLookupShowMoreModalComponent } public selectAll(): void { - const items = this.items!; + const items = this.items; const selectedItems: { index: number; itemData: any }[] = this.selectedItems; @@ -301,11 +301,11 @@ export class SkyLookupShowMoreModalComponent } public updateDataState(): void { - const items = this.items!; + const items = this.items; const selectedItems: { index: number; itemData: any }[] = this.selectedItems; - items.forEach((item: any, index: number) => { + items?.forEach((item: any, index: number) => { item.selected = selectedItems.findIndex( (selectedItem) => selectedItem.index === index 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 0cbeefcc47..e2bc1737dc 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.html +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.html @@ -21,7 +21,7 @@ [enableShowMore]="enableShowMore" [messageStream]="autocompleteController" [propertiesToSearch]="propertiesToSearch" - [search]="search" + [search]="searchOrDefault" [searchAsyncDisabled]="searchAsync.observers.length === 0" [searchFilters]="searchFilters" [searchResultsLimit]="searchResultsLimit" diff --git a/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts b/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts index ccfafb4cc5..e1e7cb9dac 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.spec.ts @@ -198,19 +198,19 @@ describe('Lookup component', function () { ); } - function getDropdown(): HTMLElement { + function getDropdown(): HTMLElement | null { return document.querySelector('.sky-autocomplete-results-container'); } function getInputElement( lookupComponent: SkyLookupComponent ): HTMLInputElement { - return lookupComponent['lookupWrapperRef'].nativeElement.querySelector( + return lookupComponent['lookupWrapperRef']?.nativeElement.querySelector( '.sky-lookup-input' ); } - function getModalEl(): HTMLElement { + function getModalEl(): HTMLElement | null { return document.querySelector('.sky-lookup-show-more-modal'); } @@ -220,7 +220,7 @@ describe('Lookup component', function () { ) as HTMLElement; } - function getModalSearchInput(): HTMLInputElement { + function getModalSearchInput(): HTMLInputElement | null { return document.querySelector( '.sky-search-input-container .sky-form-control' ); @@ -228,7 +228,7 @@ describe('Lookup component', function () { function getModalSearchInputValue(): string { const modalSearchInput = getModalSearchInput(); - return modalSearchInput.value; + return modalSearchInput?.value ?? ''; } function getRepeaterItemCount(): number { @@ -283,17 +283,19 @@ describe('Lookup component', function () { function getShowMoreRepeaterItemContent(index: number): string { return ( - document.querySelectorAll('sky-modal sky-repeater-item-content')[ - index - ] as HTMLElement - ).textContent.trim(); + document + .querySelectorAll('sky-modal sky-repeater-item-content') + [index]?.textContent?.trim() ?? '' + ); } function getShowMoreModalTitle(): string { - return document.querySelector('sky-modal-header').textContent.trim(); + return ( + document.querySelector('sky-modal-header')?.textContent?.trim() ?? '' + ); } - function getShowMoreNoResultsElement(): HTMLElement { + function getShowMoreNoResultsElement(): HTMLElement | null { return document.querySelector('.sky-lookup-show-more-no-results'); } @@ -394,26 +396,28 @@ describe('Lookup component', function () { } function triggerClick( - element: Element, + element: Element | null, fixture: ComponentFixture, focusable = false ): void { - SkyAppTestUtility.fireDomEvent(element, 'mousedown'); - tick(); - fixture.detectChanges(); - tick(); + if (element) { + SkyAppTestUtility.fireDomEvent(element, 'mousedown'); + tick(); + fixture.detectChanges(); + tick(); + + if (focusable) { + (element as HTMLElement).focus(); + tick(); + fixture.detectChanges(); + tick(); + } - if (focusable) { - (element as HTMLElement).focus(); + SkyAppTestUtility.fireDomEvent(element, 'mouseup'); tick(); fixture.detectChanges(); tick(); } - - SkyAppTestUtility.fireDomEvent(element, 'mouseup'); - tick(); - fixture.detectChanges(); - tick(); } function triggerInputFocus( @@ -452,17 +456,19 @@ describe('Lookup component', function () { } function verifyPickerId() { - expect(getModalEl().id).toBeTruthy(); + expect(getModalEl()?.id).toBeTruthy(); } async function triggerModalScrollAsync( fixture: ComponentFixture ): Promise { const modalContent = document.querySelector('.sky-modal-content'); - modalContent.scrollTop = modalContent.scrollHeight; - SkyAppTestUtility.fireDomEvent(modalContent, 'scroll'); - fixture.detectChanges(); - return fixture.whenStable(); + if (modalContent) { + modalContent.scrollTop = modalContent.scrollHeight; + SkyAppTestUtility.fireDomEvent(modalContent, 'scroll'); + fixture.detectChanges(); + return fixture.whenStable(); + } } //#endregion @@ -579,8 +585,8 @@ describe('Lookup component', function () { ]; fixture.detectChanges(); - expect(lookupComponent.tokens.length).toBe(5); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(5); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -594,8 +600,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(6); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(6); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -798,12 +804,12 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(1); + expect(lookupComponent.tokens?.length).toBe(1); expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); component.resetForm(); - expect(lookupComponent.tokens.length).toBe(0); + expect(lookupComponent.tokens?.length).toBe(0); expect(lookupComponent.value).toEqual([]); })); @@ -813,16 +819,16 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Isaac', }); expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); component.setValue(0); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Andy', description: 'Mr. Andy', birthDate: '1/1/1995', @@ -840,8 +846,8 @@ describe('Lookup component', function () { fixture.detectChanges(); performSearch('s', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Isaac', }); expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -849,8 +855,8 @@ describe('Lookup component', function () { component.disableLookup(); component.setValue(0); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Andy', description: 'Mr. Andy', birthDate: '1/1/1995', @@ -1028,7 +1034,17 @@ describe('Lookup component', function () { // Type 'r' to activate the autocomplete dropdown, then click the first result. performSearch('r', fixture); - const addButton = getAddButton(); + let addButton = getAddButton(); + expect(addButton).toBeNull(); + expect(addButtonSpy).not.toHaveBeenCalled(); + + component.showAddButton = undefined; + fixture.detectChanges(); + + // Type 'r' to activate the autocomplete dropdown, then click the first result. + performSearch('r', fixture); + + addButton = getAddButton(); expect(addButton).toBeNull(); expect(addButtonSpy).not.toHaveBeenCalled(); })); @@ -1226,7 +1242,7 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -1348,7 +1364,7 @@ describe('Lookup component', function () { performSearch('s', fixture, true); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(asyncLookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -1466,7 +1482,7 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -1586,7 +1602,7 @@ describe('Lookup component', function () { performSearch('s', fixture, true); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(asyncLookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -2093,7 +2109,7 @@ describe('Lookup component', function () { it('should add items when scrolling ends with "Only show selected" active', async () => { component.enableShowMore = true; component.friends = [ - ...component.data.filter((item) => !item.description), + ...(component.data?.filter((item) => !item.description) ?? []), ]; fixture.detectChanges(); @@ -2156,8 +2172,8 @@ describe('Lookup component', function () { ]; fixture.detectChanges(); - expect(lookupComponent.tokens.length).toBe(5); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(5); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -2171,8 +2187,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: '6 items selected', }); expect(lookupComponent.value).toEqual([ @@ -2242,7 +2258,7 @@ describe('Lookup component', function () { let tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -2252,7 +2268,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); it('should not open the show more modal when disabled', fakeAsync(() => { @@ -2537,8 +2553,8 @@ describe('Lookup component', function () { ]; fixture.detectChanges(); - expect(asyncLookupComponent.tokens.length).toBe(5); - expect(asyncLookupComponent.tokens[0].value).toEqual({ + expect(asyncLookupComponent.tokens?.length).toBe(5); + expect(asyncLookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(asyncLookupComponent.value).toEqual([ @@ -2552,8 +2568,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture, true); selectSearchResult(0, fixture); - expect(asyncLookupComponent.tokens.length).toBe(1); - expect(asyncLookupComponent.tokens[0].value).toEqual({ + expect(asyncLookupComponent.tokens?.length).toBe(1); + expect(asyncLookupComponent.tokens![0].value).toEqual({ name: '6 items selected', }); expect(asyncLookupComponent.value).toEqual([ @@ -2623,7 +2639,7 @@ describe('Lookup component', function () { let tokenElements = getTokenElements(true); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -2633,7 +2649,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(true); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); it('should not open the show more modal when disabled', fakeAsync(() => { @@ -3047,7 +3063,16 @@ describe('Lookup component', function () { // Type 'r' to activate the autocomplete dropdown, then click the first result. performSearch('r', fixture); - const showMoreButton = getShowMoreButton(); + let showMoreButton = getShowMoreButton(); + expect(showMoreButton).toBeNull(); + + component.enableShowMore = undefined; + fixture.detectChanges(); + + // Type 'r' to activate the autocomplete dropdown, then click the first result. + performSearch('r', fixture); + + showMoreButton = getShowMoreButton(); expect(showMoreButton).toBeNull(); })); @@ -3113,7 +3138,7 @@ describe('Lookup component', function () { fixture.detectChanges(); const customPickerSpy = spyOn( - component.showMoreConfig.customPicker, + component.showMoreConfig!.customPicker!, 'open' ).and.callThrough(); @@ -3135,7 +3160,7 @@ describe('Lookup component', function () { fixture.detectChanges(); const customPickerSpy = spyOn( - component.showMoreConfig.customPicker, + component.showMoreConfig!.customPicker!, 'open' ).and.callThrough(); @@ -3148,7 +3173,7 @@ describe('Lookup component', function () { items: component.data, initialSearch: 'p', initialValue: [ - component.data.find((item) => item.name === 'Patty'), + component.data?.find((item) => item.name === 'Patty'), ], }); })); @@ -3173,7 +3198,7 @@ describe('Lookup component', function () { let tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -3183,7 +3208,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); }); }); @@ -3603,7 +3628,7 @@ describe('Lookup component', function () { // performing a search in the popover view should provide the custom search function with a 'popover' context expect(customSearchFunctionSpy).toHaveBeenCalledWith( 's', - jasmine.arrayWithExactContents(component.data), + jasmine.arrayWithExactContents(component.data ?? []), jasmine.objectContaining({ context: 'popover', }) @@ -3639,7 +3664,7 @@ describe('Lookup component', function () { // performing a search in the modal view should call the custom search function with a 'modal' context expect(customSearchFunctionSpy).toHaveBeenCalledWith( 's', - jasmine.arrayWithExactContents(component.data), + jasmine.arrayWithExactContents(component.data ?? []), jasmine.objectContaining({ context: 'modal', }) @@ -3689,6 +3714,19 @@ describe('Lookup component', function () { expect(typeof lookupComponent.searchResultsLimit).not.toBeUndefined(); }); + it('should handle disabled', () => { + fixture.detectChanges(); + + const inputElement = getInputElement(lookupComponent); + expect(inputElement.disabled).toBeFalse(); + component.disableLookup(); + fixture.detectChanges(); + expect(inputElement.disabled).toBeTrue(); + component.disabled = undefined; + fixture.detectChanges(); + expect(inputElement.disabled).toBeFalse(); + }); + describe('multi-select', () => { beforeEach(() => { component.setMultiSelect(); @@ -3730,8 +3768,8 @@ describe('Lookup component', function () { tick(); fixture.detectChanges(); - expect(lookupComponent.tokens.length).toBe(5); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(5); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -3745,8 +3783,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(6); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(6); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -4178,7 +4216,7 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -4300,7 +4338,7 @@ describe('Lookup component', function () { performSearch('s', fixture, true); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(asyncLookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -4418,7 +4456,7 @@ describe('Lookup component', function () { performSearch('s', fixture); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(lookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -4538,7 +4576,7 @@ describe('Lookup component', function () { performSearch('s', fixture, true); selectSearchResult(0, fixture); - const originalDataLength = component.data.length; + const originalDataLength = component.data?.length ?? 0; expect(asyncLookupComponent.value).toEqual([{ name: 'Isaac' }]); @@ -5055,7 +5093,7 @@ describe('Lookup component', function () { it('should add items when scrolling ends with "Only show selected" active', async () => { component.enableShowMore = true; component.selectedFriends = [ - ...component.data.filter((item) => !item.description), + ...(component.data?.filter((item) => !item.description) ?? []), ]; fixture.detectChanges(); @@ -5120,8 +5158,8 @@ describe('Lookup component', function () { tick(); fixture.detectChanges(); - expect(lookupComponent.tokens.length).toBe(5); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(5); + expect(lookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(lookupComponent.value).toEqual([ @@ -5135,8 +5173,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture); selectSearchResult(0, fixture); - expect(lookupComponent.tokens.length).toBe(1); - expect(lookupComponent.tokens[0].value).toEqual({ + expect(lookupComponent.tokens?.length).toBe(1); + expect(lookupComponent.tokens![0].value).toEqual({ name: '6 items selected', }); expect(lookupComponent.value).toEqual([ @@ -5203,14 +5241,14 @@ describe('Lookup component', function () { fixture.detectChanges(); tick(); - component.selectedFriends = [component.data[1]]; + component.selectedFriends = [component.data![1]]; fixture.detectChanges(); tick(); expect(lookupComponent.value).toEqual([{ name: 'Beth' }]); let tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -5220,7 +5258,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); }); describe('async', () => { @@ -5489,8 +5527,8 @@ describe('Lookup component', function () { tick(); fixture.detectChanges(); - expect(asyncLookupComponent.tokens.length).toBe(5); - expect(asyncLookupComponent.tokens[0].value).toEqual({ + expect(asyncLookupComponent.tokens?.length).toBe(5); + expect(asyncLookupComponent.tokens![0].value).toEqual({ name: 'Fred', }); expect(asyncLookupComponent.value).toEqual([ @@ -5504,8 +5542,8 @@ describe('Lookup component', function () { performSearch('Oli', fixture, true); selectSearchResult(0, fixture); - expect(asyncLookupComponent.tokens.length).toBe(1); - expect(asyncLookupComponent.tokens[0].value).toEqual({ + expect(asyncLookupComponent.tokens?.length).toBe(1); + expect(asyncLookupComponent.tokens![0].value).toEqual({ name: '6 items selected', }); expect(asyncLookupComponent.value).toEqual([ @@ -5575,14 +5613,14 @@ describe('Lookup component', function () { fixture.detectChanges(); tick(); - component.selectedFriendsAsync = [component.data[1]]; + component.selectedFriendsAsync = [component.data![1]]; fixture.detectChanges(); tick(); expect(asyncLookupComponent.value).toEqual([{ name: 'Beth' }]); let tokenElements = getTokenElements(true); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -5592,7 +5630,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(true); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); it('should not open the show more modal when disabled', fakeAsync(() => { @@ -6049,7 +6087,7 @@ describe('Lookup component', function () { fixture.detectChanges(); const customPickerSpy = spyOn( - component.showMoreConfig.customPicker, + component.showMoreConfig!.customPicker!, 'open' ).and.callThrough(); @@ -6069,7 +6107,7 @@ describe('Lookup component', function () { fixture.detectChanges(); const customPickerSpy = spyOn( - component.showMoreConfig.customPicker, + component.showMoreConfig!.customPicker!, 'open' ).and.callThrough(); @@ -6082,7 +6120,7 @@ describe('Lookup component', function () { items: component.data, initialSearch: 'p', initialValue: [ - component.data.find((item) => item.name === 'Patty'), + component.data?.find((item) => item.name === 'Patty'), ], }); })); @@ -6100,14 +6138,14 @@ describe('Lookup component', function () { fixture.detectChanges(); tick(); - component.selectedFriends = [component.data[1]]; + component.selectedFriends = [component.data![1]]; fixture.detectChanges(); tick(); expect(lookupComponent.value).toEqual([{ name: 'Beth' }]); let tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); saveShowMoreModal(fixture); fixture.detectChanges(); @@ -6117,7 +6155,7 @@ describe('Lookup component', function () { tokenElements = getTokenElements(); expect(tokenElements.length).toBe(1); - expect(tokenElements.item(0).textContent.trim()).toBe('Isaac'); + expect(tokenElements.item(0).textContent?.trim()).toBe('Isaac'); })); }); }); @@ -6440,10 +6478,10 @@ describe('Lookup component', function () { const inputBoxEl = nativeElement.querySelector('sky-input-box'); - const inputGroupEl = inputBoxEl.querySelector( + const inputGroupEl = inputBoxEl?.querySelector( '.sky-input-box-input-group-inner' ); - const containerEl = inputGroupEl.children.item(1); + const containerEl = inputGroupEl?.children.item(1); expect(containerEl).toHaveCssClass('sky-lookup'); })); @@ -6454,10 +6492,10 @@ describe('Lookup component', function () { const inputBoxEl = nativeElement.querySelector('sky-input-box'); - const inputGroupEl = inputBoxEl.querySelector( + const inputGroupEl = inputBoxEl?.querySelector( '.sky-input-box-input-group-inner' ); - const containerEl = inputGroupEl.children.item(1); + const containerEl = inputGroupEl?.children.item(1); expect(containerEl).toHaveCssClass('sky-lookup'); })); 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 6603721574..e6e73d3382 100644 --- a/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts +++ b/libs/components/lookup/src/lib/modules/lookup/lookup.component.ts @@ -79,7 +79,6 @@ export class SkyLookupComponent /** * Specifies the value for the `autocomplete` attribute on the form input. - * @default "off" */ @Input() public autocompleteAttribute: string | undefined; @@ -105,15 +104,29 @@ export class SkyLookupComponent /** * Indicates whether to disable the lookup field. + * @default false */ @Input() - public disabled = false; + public set disabled(value: boolean | undefined) { + this.#_disabled = value ?? false; + } + + public get disabled(): boolean { + return this.#_disabled; + } /** * Indicates whether to enable users to open a picker where they can view all options. + * @default false */ @Input() - public enableShowMore = false; + public set enableShowMore(value: boolean | undefined) { + this.#_enableShowMore = value ?? false; + } + + public get enableShowMore(): boolean { + return this.#_enableShowMore; + } /** * Specifies placeholder text to display in the lookup field. @@ -131,9 +144,16 @@ export class SkyLookupComponent /** * Indicates whether to display a button that lets users add options to the list. + * @default false */ @Input() - public showAddButton = false; + public set showAddButton(value: boolean | undefined) { + this.#_showAddButton = value ?? false; + } + + public get showAddButton(): boolean { + return this.#_showAddButton; + } /** * Specifies configuration options for the picker that displays all options. @@ -228,12 +248,16 @@ export class SkyLookupComponent read: SkyAutocompleteInputDirective, static: false, }) - private set autocompleteInputDirective(value: SkyAutocompleteInputDirective) { + public set autocompleteInputDirective( + value: SkyAutocompleteInputDirective | undefined + ) { this.#_autocompleteInputDirective = value; this.#updateForSelectMode(); } - private get autocompleteInputDirective(): SkyAutocompleteInputDirective { + public get autocompleteInputDirective(): + | SkyAutocompleteInputDirective + | undefined { return this.#_autocompleteInputDirective; } @@ -241,24 +265,24 @@ export class SkyLookupComponent read: TemplateRef, static: true, }) - private showMoreButtonTemplateRef!: TemplateRef; + public showMoreButtonTemplateRef: TemplateRef | undefined; @ViewChild('inputTemplateRef', { read: TemplateRef, static: true, }) - private inputTemplateRef!: TemplateRef; + public inputTemplateRef: TemplateRef | undefined; @ViewChild('lookupWrapper', { read: ElementRef, }) - private lookupWrapperRef!: ElementRef; + public lookupWrapperRef: ElementRef | undefined; @ViewChild('searchIconTemplateRef', { read: TemplateRef, static: true, }) - private searchIconTemplateRef!: TemplateRef; + public searchIconTemplateRef: TemplateRef | undefined; #adapter: SkyLookupAdapterService; @@ -280,12 +304,18 @@ export class SkyLookupComponent #windowRef: SkyAppWindowRef; - #_autocompleteInputDirective!: SkyAutocompleteInputDirective; + #_autocompleteInputDirective: SkyAutocompleteInputDirective | undefined; #_data: any[] | undefined; + #_disabled = false; + + #_enableShowMore = false; + #_selectMode: SkyLookupSelectModeType | undefined; + #_showAddButton = false; + #_tokens: SkyToken[] | undefined; #_value: any[] | undefined; @@ -316,7 +346,7 @@ export class SkyLookupComponent } public ngOnInit(): void { - if (this.inputBoxHostSvc) { + if (this.inputBoxHostSvc && this.inputTemplateRef) { this.inputBoxHostSvc.populate({ inputTemplate: this.inputTemplateRef, buttonsTemplate: this.enableShowMore @@ -460,8 +490,10 @@ export class SkyLookupComponent } public clearSearchText(): void { - this.autocompleteInputDirective.value = undefined; - this.autocompleteInputDirective.inputTextValue = ''; + if (this.autocompleteInputDirective) { + this.autocompleteInputDirective.value = undefined; + this.autocompleteInputDirective.inputTextValue = ''; + } } // Handles when to focus on the tokens. @@ -527,15 +559,19 @@ export class SkyLookupComponent let isValueInTextBox = false; if (this.selectMode === 'single') { isValueInTextBox = - this.autocompleteInputDirective.value && + this.autocompleteInputDirective?.value && this.autocompleteInputDirective.inputTextValue === this.autocompleteInputDirective.value[this.descriptorProperty]; } - this.openPicker( - isValueInTextBox ? '' : this.autocompleteInputDirective.inputTextValue - ); - this.autocompleteInputDirective.restoreInputTextValueToPreviousState(); + let searchValue = ''; + + if (!isValueInTextBox && this.autocompleteInputDirective) { + searchValue = this.autocompleteInputDirective.inputTextValue; + } + + this.openPicker(searchValue); + this.autocompleteInputDirective?.restoreInputTextValueToPreviousState(); } public onShowMoreClick(event: SkyAutocompleteShowMoreArgs): void { @@ -570,28 +606,35 @@ export class SkyLookupComponent contextProviderType = SkyLookupShowMoreNativePickerAsyncContext; modalComponentType = SkyLookupShowMoreAsyncModalComponent; - contextProviderValue = new SkyLookupShowMoreNativePickerAsyncContext(); - contextProviderValue.idProperty = this.idProperty!; - contextProviderValue.searchAsync = (args) => { - this.searchAsync.emit(args); - return args.result!; - }; + contextProviderValue = new SkyLookupShowMoreNativePickerAsyncContext( + this.descriptorProperty, + this.idProperty!, + initialSearch, + initialValue, + (args) => { + this.searchAsync.emit(args); + return args.result!; + }, + this.selectMode, + this.showAddButton, + modalConfig + ); } else { contextProviderType = SkyLookupShowMoreNativePickerContext; modalComponentType = SkyLookupShowMoreModalComponent; - contextProviderValue = new SkyLookupShowMoreNativePickerContext(); - contextProviderValue.items = this.data; - contextProviderValue.search = this.search; + contextProviderValue = new SkyLookupShowMoreNativePickerContext( + this.descriptorProperty, + initialSearch, + initialValue, + this.data, + this.searchOrDefault, + this.selectMode, + this.showAddButton, + modalConfig + ); } - contextProviderValue.descriptorProperty = this.descriptorProperty; - contextProviderValue.initialSearch = initialSearch; - contextProviderValue.initialValue = initialValue; - contextProviderValue.selectMode = this.selectMode; - contextProviderValue.showAddButton = this.showAddButton; - contextProviderValue.userConfig = modalConfig; - return this.#modalService.open(modalComponentType, { providers: [ { @@ -701,7 +744,7 @@ export class SkyLookupComponent #focusInputOnHostClick(): void { let hostElement = !this.inputBoxHostSvc ? this.#elementRef.nativeElement - : this.lookupWrapperRef.nativeElement; + : this.lookupWrapperRef?.nativeElement; const documentObj = this.#windowRef.nativeWindow.document; // Handles focusing the input when the host is clicked. @@ -713,7 +756,7 @@ export class SkyLookupComponent .subscribe((event) => { hostElement = !this.inputBoxHostSvc ? this.#elementRef.nativeElement - : this.lookupWrapperRef.nativeElement; + : this.lookupWrapperRef?.nativeElement; this.isInputFocused = hostElement.contains(event.target); this.#changeDetector.markForCheck(); @@ -724,7 +767,7 @@ export class SkyLookupComponent .subscribe((event) => { hostElement = !this.inputBoxHostSvc ? this.#elementRef.nativeElement - : this.lookupWrapperRef.nativeElement; + : this.lookupWrapperRef?.nativeElement; this.isInputFocused = hostElement.contains(event.target); this.#changeDetector.markForCheck(); @@ -741,7 +784,9 @@ export class SkyLookupComponent } #focusInput(): void { - this.#adapter.focusInput(this.lookupWrapperRef); + if (this.lookupWrapperRef) { + this.#adapter.focusInput(this.lookupWrapperRef); + } } #onAddButtonComplete(args: SkyLookupAddCallbackArgs) { diff --git a/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-async-context.ts b/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-async-context.ts index 329cf968a7..5155025213 100644 --- a/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-async-context.ts +++ b/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-async-context.ts @@ -8,19 +8,39 @@ import { SkyLookupShowMoreNativePickerConfig } from './lookup-show-more-native-p * Context for the show more native picker. These values are provided by the lookup component. */ export class SkyLookupShowMoreNativePickerAsyncContext { - public descriptorProperty!: string; + public descriptorProperty: string; - public idProperty!: string; + public idProperty: string; - public initialSearch!: string; + public initialSearch: string; - public initialValue!: any[]; + public initialValue: any[]; - public searchAsync!: SkyAutocompleteSearchAsyncFunction; + public searchAsync: SkyAutocompleteSearchAsyncFunction; - public selectMode!: SkyLookupSelectModeType; + public selectMode: SkyLookupSelectModeType; - public showAddButton!: boolean; + public showAddButton: boolean; - public userConfig!: SkyLookupShowMoreNativePickerConfig; + public userConfig: SkyLookupShowMoreNativePickerConfig; + + constructor( + descriptorProperty: string, + idProperty: string, + initialSearch: string, + initialValue: any[], + searchAsync: SkyAutocompleteSearchAsyncFunction, + selectMode: SkyLookupSelectModeType, + showAddButton: boolean, + userConfig: SkyLookupShowMoreNativePickerConfig + ) { + this.descriptorProperty = descriptorProperty; + this.idProperty = idProperty; + this.initialSearch = initialSearch; + this.initialValue = initialValue; + this.searchAsync = searchAsync; + this.selectMode = selectMode; + this.showAddButton = showAddButton; + this.userConfig = userConfig; + } } diff --git a/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-context.ts b/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-context.ts index 86edc462c1..f4eb8d0ed8 100644 --- a/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-context.ts +++ b/libs/components/lookup/src/lib/modules/lookup/types/lookup-show-more-native-picker-context.ts @@ -8,19 +8,39 @@ import { SkyLookupShowMoreNativePickerConfig } from './lookup-show-more-native-p * Context for the show more native picker. These values are provided by the lookup component. */ export class SkyLookupShowMoreNativePickerContext { - public descriptorProperty!: string; + public descriptorProperty: string; - public initialSearch!: string; + public initialSearch: string; - public initialValue!: any[]; + public initialValue: any[]; - public items!: any[]; + public items: any[]; - public search!: SkyAutocompleteSearchFunction; + public search: SkyAutocompleteSearchFunction; - public selectMode!: SkyLookupSelectModeType; + public selectMode: SkyLookupSelectModeType; - public showAddButton!: boolean; + public showAddButton: boolean; - public userConfig!: SkyLookupShowMoreNativePickerConfig; + public userConfig: SkyLookupShowMoreNativePickerConfig; + + constructor( + descriptorProperty: string, + initialSearch: string, + initialValue: any[], + items: any[], + search: SkyAutocompleteSearchFunction, + selectMode: SkyLookupSelectModeType, + showAddButton: boolean, + userConfig: SkyLookupShowMoreNativePickerConfig + ) { + this.descriptorProperty = descriptorProperty; + this.initialSearch = initialSearch; + this.initialValue = initialValue; + this.items = items; + this.search = search; + this.selectMode = selectMode; + this.showAddButton = showAddButton; + this.userConfig = userConfig; + } } diff --git a/libs/components/lookup/src/lib/modules/search/fixtures/search.component.fixture.html b/libs/components/lookup/src/lib/modules/search/fixtures/search.component.fixture.html index 3f28f2455b..f848327b6e 100644 --- a/libs/components/lookup/src/lib/modules/search/fixtures/search.component.fixture.html +++ b/libs/components/lookup/src/lib/modules/search/fixtures/search.component.fixture.html @@ -1,4 +1,6 @@ - + { fixture.destroy(); }); + function getInput(): DebugElement { + return element.query(By.css('input')); + } + function setInput(text: string) { const inputEvent = document.createEvent('Event'); const params = { @@ -82,7 +91,7 @@ describe('Search component', () => { const changeEvent = document.createEvent('Event'); changeEvent.initEvent('change', params.bubbles, params.cancelable); - const inputEl = element.query(By.css('input')); + const inputEl = getInput(); inputEl.nativeElement.value = text; inputEl.nativeElement.dispatchEvent(inputEvent); @@ -306,6 +315,25 @@ describe('Search component', () => { expect(component.searchComponent.searchClear.emit).toHaveBeenCalled(); }); + it('should disable the input correctly', async () => { + fixture.detectChanges(); + await fixture.whenRenderingDone(); + let input: HTMLInputElement = getInput().nativeElement; + expect(input.disabled).toBeFalse(); + + component.disabled = true; + fixture.detectChanges(); + await fixture.whenRenderingDone(); + input = getInput().nativeElement; + expect(input.disabled).toBeTrue(); + + component.disabled = undefined; + fixture.detectChanges(); + await fixture.whenRenderingDone(); + input = getInput().nativeElement; + expect(input.disabled).toBeFalse(); + }); + it('should update search text when applySearchText is called with new search text', () => { component.searchComponent.applySearchText('new search text'); fixture.detectChanges(); @@ -324,13 +352,30 @@ describe('Search component', () => { ).toBeVisible(); }); - it('should delay the search if debounce is used', async () => { + it('should delay the search if debounce is used', fakeAsync(() => { component.searchComponent.searchTextChanged('debounce this please'); fixture.detectChanges(); - await fixture.whenStable(); + tick(1); fixture.detectChanges(); - expect(component.searchComponent.searchText).toBe('debounce this please'); - }); + expect(component.lastSearchTextChanged).toBe('debounce this please'); + component.debounceTime = 10; + fixture.detectChanges(); + component.searchComponent.searchTextChanged('debounce this please 2'); + fixture.detectChanges(); + tick(1); + fixture.detectChanges(); + expect(component.lastSearchTextChanged).toBe('debounce this please'); + tick(10); + fixture.detectChanges(); + expect(component.lastSearchTextChanged).toBe('debounce this please 2'); + component.debounceTime = undefined; + fixture.detectChanges(); + component.searchComponent.searchTextChanged('debounce this please 3'); + fixture.detectChanges(); + tick(1); + fixture.detectChanges(); + expect(component.lastSearchTextChanged).toBe('debounce this please 3'); + })); describe('animations', () => { describe('should animate the mobile search input open', () => { diff --git a/libs/components/lookup/src/lib/modules/search/search.component.ts b/libs/components/lookup/src/lib/modules/search/search.component.ts index 2e37d133fc..a1f09a0f87 100644 --- a/libs/components/lookup/src/lib/modules/search/search.component.ts +++ b/libs/components/lookup/src/lib/modules/search/search.component.ts @@ -91,19 +91,40 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { * @default "responsive" */ @Input() - public expandMode = EXPAND_MODE_RESPONSIVE; + public set expandMode(value: string | undefined) { + this.#_expandMode = value ?? EXPAND_MODE_RESPONSIVE; + } + + public get expandMode(): string { + return this.#_expandMode; + } /** * Specifies how many milliseconds to wait before searching after users enter text in the search input. + * @default 0 */ @Input() - public debounceTime = 0; + public set debounceTime(value: number | undefined) { + this.#_debounceTime = value ?? 0; + this.#setupSearchChangedEvent(); + } + + public get debounceTime(): number { + return this.#_debounceTime; + } /** * Indicates whether to disable the filter button. + * @default false */ @Input() - public disabled = false; + public set disabled(value: boolean | undefined) { + this.#_disabled = value ?? false; + } + + public get disabled(): boolean { + return this.#_disabled; + } /** * Specifies placeholder text to display in the search input until users @@ -139,6 +160,14 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { #searchUpdated = new Subject(); + #searchUpdatedSub: Subscription | undefined; + + #_debounceTime = 0; + + #_disabled = false; + + #_expandMode = EXPAND_MODE_RESPONSIVE; + constructor( mediaQueryService: SkyMediaQueryService, elRef: ElementRef, @@ -161,12 +190,7 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { ); } - this.#searchUpdated - .asObservable() - .pipe(debounceTime(this.debounceTime), distinctUntilChanged()) - .subscribe((value) => { - this.searchChange.emit(value); - }); + this.#setupSearchChangedEvent(); } public ngOnChanges(changes: SimpleChanges) { @@ -284,6 +308,7 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { } this.#searchUpdated.complete(); + this.#searchUpdatedSub?.unsubscribe(); } #searchBindingChanged(changes: SimpleChanges) { return ( @@ -299,7 +324,7 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { ); } - #shouldOpenInput() { + #shouldOpenInput(): boolean { return ( this.searchText !== '' && this.#mediaQueryService.current === SkyMediaBreakpoints.xs && @@ -307,7 +332,7 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { ); } - #mediaQueryCallback(args: SkyMediaBreakpoints) { + #mediaQueryCallback(args: SkyMediaBreakpoints): void { if (this.#searchShouldCollapse()) { if (args === SkyMediaBreakpoints.xs) { this.inputAnimate = INPUT_HIDDEN_STATE; @@ -320,10 +345,20 @@ export class SkySearchComponent implements OnDestroy, OnInit, OnChanges { this.#changeRef.markForCheck(); } - #searchShouldCollapse() { + #searchShouldCollapse(): boolean { return ( (this.isCollapsible || this.isCollapsible === undefined) && this.isFullWidth !== true ); } + + #setupSearchChangedEvent(): void { + this.#searchUpdatedSub?.unsubscribe(); + + this.#searchUpdatedSub = this.#searchUpdated + .pipe(debounceTime(this.debounceTime), distinctUntilChanged()) + .subscribe((value) => { + this.searchChange.emit(value); + }); + } } diff --git a/libs/components/lookup/testing/src/autocomplete/autocomplete-harness.spec.ts b/libs/components/lookup/testing/src/autocomplete/autocomplete-harness.spec.ts index afd1951d56..8c08f57e2c 100644 --- a/libs/components/lookup/testing/src/autocomplete/autocomplete-harness.spec.ts +++ b/libs/components/lookup/testing/src/autocomplete/autocomplete-harness.spec.ts @@ -16,7 +16,7 @@ describe('Autocomplete harness', () => { const fixture = TestBed.createComponent(AutocompleteHarnessTestComponent); const loader = TestbedHarnessEnvironment.loader(fixture); - let autocompleteHarness: SkyAutocompleteHarness; + let autocompleteHarness: SkyAutocompleteHarness | undefined; if (options.dataSkyId) { autocompleteHarness = await loader.getHarness( SkyAutocompleteHarness.with({ dataSkyId: options.dataSkyId }) @@ -31,13 +31,13 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await expectAsync(autocompleteHarness.isFocused()).toBeResolvedTo(false); + await expectAsync(autocompleteHarness?.isFocused()).toBeResolvedTo(false); - await autocompleteHarness.focus(); - await expectAsync(autocompleteHarness.isFocused()).toBeResolvedTo(true); + await autocompleteHarness?.focus(); + await expectAsync(autocompleteHarness?.isFocused()).toBeResolvedTo(true); - await autocompleteHarness.blur(); - await expectAsync(autocompleteHarness.isFocused()).toBeResolvedTo(false); + await autocompleteHarness?.blur(); + await expectAsync(autocompleteHarness?.isFocused()).toBeResolvedTo(false); }); it('should check if autocomplete is disabled', async () => { @@ -45,11 +45,11 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await expectAsync(autocompleteHarness.isDisabled()).toBeResolvedTo(false); + await expectAsync(autocompleteHarness?.isDisabled()).toBeResolvedTo(false); fixture.componentInstance.disableForm(); - await expectAsync(autocompleteHarness.isDisabled()).toBeResolvedTo(true); + await expectAsync(autocompleteHarness?.isDisabled()).toBeResolvedTo(true); }); it('should check if autocomplete is open', async () => { @@ -57,9 +57,9 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('r'); + await autocompleteHarness?.enterText('r'); - await expectAsync(autocompleteHarness.isOpen()).toBeResolvedTo(true); + await expectAsync(autocompleteHarness?.isOpen()).toBeResolvedTo(true); }); it('should return search result harnesses', async () => { @@ -67,9 +67,9 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('d'); + await autocompleteHarness?.enterText('d'); - const results = await autocompleteHarness.getSearchResults(); + const results = (await autocompleteHarness?.getSearchResults()) ?? []; await expectAsync(results[0].getDescriptorValue()).toBeResolvedTo('Red'); await expectAsync(results[0].getText()).toBeResolvedTo('Red'); @@ -80,10 +80,10 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('r'); + await autocompleteHarness?.enterText('r'); await expectAsync( - autocompleteHarness.getSearchResultsText() + autocompleteHarness?.getSearchResultsText() ).toBeResolvedTo([ 'Red', 'Green', @@ -99,11 +99,11 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('r'); - const result = (await autocompleteHarness.getSearchResults())[0]; + await autocompleteHarness?.enterText('r'); + const result = ((await autocompleteHarness?.getSearchResults()) ?? [])[0]; await result.select(); - await expectAsync(autocompleteHarness.getValue()).toBeResolvedTo('Red'); + await expectAsync(autocompleteHarness?.getValue()).toBeResolvedTo('Red'); }); it('should select a search result using filters', async () => { @@ -111,12 +111,12 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('r'); - await autocompleteHarness.selectSearchResult({ + await autocompleteHarness?.enterText('r'); + await autocompleteHarness?.selectSearchResult({ text: 'Green', }); - await expectAsync(autocompleteHarness.getValue()).toBeResolvedTo('Green'); + await expectAsync(autocompleteHarness?.getValue()).toBeResolvedTo('Green'); }); it('should clear the input value', async () => { @@ -125,15 +125,15 @@ describe('Autocomplete harness', () => { }); // First, set a value on the autocomplete. - await autocompleteHarness.enterText('r'); - await autocompleteHarness.selectSearchResult({ + await autocompleteHarness?.enterText('r'); + await autocompleteHarness?.selectSearchResult({ text: 'Green', }); - await expectAsync(autocompleteHarness.getValue()).toBeResolvedTo('Green'); + await expectAsync(autocompleteHarness?.getValue()).toBeResolvedTo('Green'); // Now, clear the value. - await autocompleteHarness.clear(); - await expectAsync(autocompleteHarness.getValue()).toBeResolvedTo(''); + await autocompleteHarness?.clear(); + await expectAsync(autocompleteHarness?.getValue()).toBeResolvedTo(''); }); it('should throw error if getting search results when autocomplete not open', async () => { @@ -142,7 +142,7 @@ describe('Autocomplete harness', () => { }); await expectAsync( - autocompleteHarness.getSearchResults() + autocompleteHarness?.getSearchResults() ).toBeRejectedWithError( 'Unable to retrieve search results. The autocomplete is closed.' ); @@ -153,10 +153,10 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('r'); + await autocompleteHarness?.enterText('r'); await expectAsync( - autocompleteHarness.getSearchResults({ + autocompleteHarness?.getSearchResults({ text: /invalidsearchtext/, }) ).toBeRejectedWithError( @@ -169,9 +169,9 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-1', }); - await autocompleteHarness.enterText('invalidsearchtext'); + await autocompleteHarness?.enterText('invalidsearchtext'); - await expectAsync(autocompleteHarness.getSearchResults()).toBeResolvedTo( + await expectAsync(autocompleteHarness?.getSearchResults()).toBeResolvedTo( [] ); }); @@ -182,9 +182,9 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-2', }); - await autocompleteHarness.enterText('d'); + await autocompleteHarness?.enterText('d'); - const results = await autocompleteHarness.getSearchResults(); + const results = (await autocompleteHarness?.getSearchResults()) ?? []; await expectAsync(results[0].getDescriptorValue()).toBeResolvedTo('Red'); await expectAsync(results[0].getText()).toBeResolvedTo('Red ID: 1'); @@ -195,12 +195,12 @@ describe('Autocomplete harness', () => { dataSkyId: 'my-autocomplete-2', }); - await autocompleteHarness.enterText('d'); + await autocompleteHarness?.enterText('d'); - const results = await autocompleteHarness.getSearchResults(); + const results = (await autocompleteHarness?.getSearchResults()) ?? []; const harness = await results[0].queryHarness(ColorIdHarness); - await expectAsync((await harness.host()).text()).toBeResolvedTo('1'); + await expectAsync((await harness?.host())?.text()).toBeResolvedTo('1'); }); }); diff --git a/libs/components/lookup/testing/src/country-field/country-field-fixture.spec.ts b/libs/components/lookup/testing/src/country-field/country-field-fixture.spec.ts index 9b9be7025a..d5126221e7 100644 --- a/libs/components/lookup/testing/src/country-field/country-field-fixture.spec.ts +++ b/libs/components/lookup/testing/src/country-field/country-field-fixture.spec.ts @@ -7,7 +7,7 @@ import { SkyThemeService } from '@skyux/theme'; import { SkyCountryFieldFixture } from './country-field-fixture'; import { SkyCountryFieldTestingModule } from './country-field-testing.module'; -const COUNTRY: SkyCountryFieldCountry = { +const COUNTRY: Required> = { name: 'United States', iso2: 'us', }; @@ -29,9 +29,9 @@ const DATA_SKY_ID = 'test-country-field'; `, }) class CountryFieldTestComponent { - public autocompleteAttribute: string; - public disabled: boolean; - public hideSelectedCountryFlag: boolean; + public autocompleteAttribute: string | undefined; + public disabled: boolean | undefined; + public hideSelectedCountryFlag: boolean | undefined; public selectedCountryChange(query: string): void {} } diff --git a/libs/components/lookup/testing/src/country-field/country-field-fixture.ts b/libs/components/lookup/testing/src/country-field/country-field-fixture.ts index 6779f7c62b..a14a859cec 100644 --- a/libs/components/lookup/testing/src/country-field/country-field-fixture.ts +++ b/libs/components/lookup/testing/src/country-field/country-field-fixture.ts @@ -8,13 +8,13 @@ import { SkyAppTestUtility } from '@skyux-sdk/testing'; * @internal */ export class SkyCountryFieldFixture { - private debugEl: DebugElement; + #debugEl: DebugElement; /** * The value of the input field's autocomplete attribute. */ public get autocompleteAttribute(): string | null { - return this.getInputElement().getAttribute('autocomplete'); + return this.#getInputElement().getAttribute('autocomplete'); } /** @@ -23,7 +23,7 @@ export class SkyCountryFieldFixture { * and if the hideSelectedCountryFlag option is false. */ public get countryFlagIsVisible(): boolean { - const flag = this.getCountryFlag(); + const flag = this.#getCountryFlag(); return flag !== null; } @@ -31,18 +31,21 @@ export class SkyCountryFieldFixture { * A flag indicating whether or not the input has been disabled. */ public get disabled(): boolean { - return this.getInputElement().disabled; + return this.#getInputElement().disabled; } /** * The value of the input field. */ public get searchText(): string { - return this.getInputElement().value; + return this.#getInputElement().value; } - constructor(private fixture: ComponentFixture, skyTestId: string) { - this.debugEl = SkyAppTestUtility.getDebugElementByTestId( + #fixture: ComponentFixture; + + constructor(fixture: ComponentFixture, skyTestId: string) { + this.#fixture = fixture; + this.#debugEl = SkyAppTestUtility.getDebugElementByTestId( fixture, skyTestId, 'sky-country-field' @@ -55,9 +58,9 @@ export class SkyCountryFieldFixture { * @returns The list of country names matching the search text. */ public async search(searchText: string): Promise { - const resultNodes = await this.searchAndGetResults( + const resultNodes = await this.#searchAndGetResults( searchText, - this.fixture + this.#fixture ); const resultArray = Array.prototype.slice.call(resultNodes); const results = resultArray.map((result: HTMLElement) => { @@ -66,8 +69,8 @@ export class SkyCountryFieldFixture { return countryName; }); - this.fixture.detectChanges(); - await this.fixture.whenStable(); + this.#fixture.detectChanges(); + await this.#fixture.whenStable(); return results; } @@ -77,48 +80,48 @@ export class SkyCountryFieldFixture { * @param searchText The name of the country to select. */ public async searchAndSelectFirstResult(searchText: string): Promise { - await this.searchAndSelect(searchText, 0, this.fixture); + await this.#searchAndSelect(searchText, 0, this.#fixture); - this.fixture.detectChanges(); - return this.fixture.whenStable(); + this.#fixture.detectChanges(); + return this.#fixture.whenStable(); } /** * Clears the country selection and input field. */ public clear(): Promise { - this.enterSearch('', this.fixture); + this.#enterSearch('', this.#fixture); - this.fixture.detectChanges(); - return this.fixture.whenStable(); + this.#fixture.detectChanges(); + return this.#fixture.whenStable(); } //#region helpers - private getCountryFlag(): DebugElement { - return this.debugEl.query(By.css('.sky-country-field-flag')); + #getCountryFlag(): DebugElement { + return this.#debugEl.query(By.css('.sky-country-field-flag')); } - private getAutocompleteElement(): HTMLElement { + #getAutocompleteElement(): HTMLElement { return document.querySelector('.sky-autocomplete-results') as HTMLElement; } - private getInputElement(): HTMLTextAreaElement { - const debugEl = this.debugEl.query(By.css('textarea')); + #getInputElement(): HTMLTextAreaElement { + const debugEl = this.#debugEl.query(By.css('textarea')); return debugEl.nativeElement as HTMLTextAreaElement; } - private blurInput(fixture: ComponentFixture): Promise { - SkyAppTestUtility.fireDomEvent(this.getInputElement(), 'blur'); + #blurInput(fixture: ComponentFixture): Promise { + SkyAppTestUtility.fireDomEvent(this.#getInputElement(), 'blur'); fixture.detectChanges(); return fixture.whenStable(); } - private enterSearch( + #enterSearch( newValue: string, fixture: ComponentFixture ): Promise { - const inputElement = this.getInputElement(); + const inputElement = this.#getInputElement(); inputElement.value = newValue; SkyAppTestUtility.fireDomEvent(inputElement, 'keyup'); SkyAppTestUtility.fireDomEvent(inputElement, 'input'); @@ -126,25 +129,25 @@ export class SkyCountryFieldFixture { return fixture.whenStable(); } - private async searchAndGetResults( + async #searchAndGetResults( newValue: string, fixture: ComponentFixture ): Promise> { - await this.enterSearch(newValue, fixture); + await this.#enterSearch(newValue, fixture); fixture.detectChanges(); await fixture.whenStable(); - return this.getAutocompleteElement().querySelectorAll( + return this.#getAutocompleteElement().querySelectorAll( '.sky-autocomplete-result' ); } - private async searchAndSelect( + async #searchAndSelect( newValue: string, index: number, fixture: ComponentFixture ): Promise { - const inputElement = this.getInputElement(); - const searchResults = await this.searchAndGetResults(newValue, fixture); + const inputElement = this.#getInputElement(); + const searchResults = await this.#searchAndGetResults(newValue, fixture); if (searchResults.length < index + 1) { throw new Error('Index out of range for results'); @@ -153,7 +156,7 @@ export class SkyCountryFieldFixture { // Note: the ordering of these events is important! SkyAppTestUtility.fireDomEvent(inputElement, 'change'); SkyAppTestUtility.fireDomEvent(searchResults[index], 'mousedown'); - this.blurInput(fixture); + this.#blurInput(fixture); } //#endregion helpers diff --git a/libs/components/lookup/testing/src/lookup/fixtures/lookup-harness-test.component.ts b/libs/components/lookup/testing/src/lookup/fixtures/lookup-harness-test.component.ts index f1ebb39c8e..e3180f5467 100644 --- a/libs/components/lookup/testing/src/lookup/fixtures/lookup-harness-test.component.ts +++ b/libs/components/lookup/testing/src/lookup/fixtures/lookup-harness-test.component.ts @@ -135,8 +135,11 @@ export class LookupHarnessTestComponent implements AfterViewInit { } public ngAfterViewInit(): void { - this.showMoreConfig.nativePickerConfig!.itemTemplate = - this.showMoreSearchResultTemplate; + /* istanbul ignore else */ + if (this.showMoreConfig.nativePickerConfig) { + this.showMoreConfig.nativePickerConfig.itemTemplate = + this.showMoreSearchResultTemplate; + } } // Only show people in the search results that have not been chosen already. diff --git a/libs/components/lookup/testing/src/lookup/lookup-harness.spec.ts b/libs/components/lookup/testing/src/lookup/lookup-harness.spec.ts index d556d751d2..777059798e 100644 --- a/libs/components/lookup/testing/src/lookup/lookup-harness.spec.ts +++ b/libs/components/lookup/testing/src/lookup/lookup-harness.spec.ts @@ -24,7 +24,7 @@ async function setupTest(options: { dataSkyId: string }) { const inputBoxHarness = await loader.getHarness( SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }) ); - lookupHarness = await inputBoxHarness.queryHarness(SkyLookupHarness); + lookupHarness = (await inputBoxHarness.queryHarness(SkyLookupHarness))!; } return { fixture, lookupHarness }; @@ -41,7 +41,7 @@ function testSingleSelect(dataSkyId: string) { await lookupHarness.enterText('d'); - const results = await lookupHarness.getSearchResults(); + const results = (await lookupHarness.getSearchResults()) ?? []; await expectAsync(results[0].getDescriptorValue()).toBeResolvedTo('Abed'); await expectAsync(results[0].getText()).toBeResolvedTo('Abed'); @@ -84,9 +84,9 @@ function testSingleSelect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('rachel'); - await picker.selectSearchResult({ contentText: 'Rachel' }); - await picker.saveAndClose(); + await picker?.enterSearchText('rachel'); + await picker?.selectSearchResult({ contentText: 'Rachel' }); + await picker?.saveAndClose(); await expectAsync(lookupHarness.getValue()).toBeResolvedTo('Rachel'); }); @@ -100,7 +100,7 @@ function testSingleSelect(dataSkyId: string) { const picker = await lookupHarness.getShowMorePicker(); - await expectAsync(picker.selectAll()).toBeRejectedWithError( + await expectAsync(picker?.selectAll()).toBeRejectedWithError( 'Could not select all selections because the "Select all" button could not be found.' ); }); @@ -114,7 +114,7 @@ function testSingleSelect(dataSkyId: string) { const picker = await lookupHarness.getShowMorePicker(); - await expectAsync(picker.clearAll()).toBeRejectedWithError( + await expectAsync(picker?.clearAll()).toBeRejectedWithError( 'Could not clear all selections because the "Clear all" button could not be found.' ); }); @@ -133,13 +133,13 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('abed'); - await picker.selectSearchResult({ contentText: 'Abed' }); - await picker.saveAndClose(); + await picker?.enterSearchText('abed'); + await picker?.selectSearchResult({ contentText: 'Abed' }); + await picker?.saveAndClose(); - const selections = await lookupHarness.getSelections(); + const selections = (await lookupHarness.getSelections()) ?? []; - expect(selections.length).toBe(1); + expect(selections?.length).toBe(1); await expectAsync(selections[0].getText()).toBeResolvedTo('Abed'); }); @@ -153,9 +153,9 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('ra'); - await picker.selectSearchResult({ contentText: /Craig|Rachel/ }); - await picker.saveAndClose(); + await picker?.enterSearchText('ra'); + await picker?.selectSearchResult({ contentText: /Craig|Rachel/ }); + await picker?.saveAndClose(); await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ 'Craig', @@ -173,9 +173,9 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('ra'); - await picker.selectAll(); - await picker.saveAndClose(); + await picker?.enterSearchText('ra'); + await picker?.selectAll(); + await picker?.saveAndClose(); await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ 'Craig', @@ -195,9 +195,9 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.loadMore(); // <-- make sure existing selection is present in the search results. - await picker.clearAll(); - await picker.saveAndClose(); + await picker?.loadMore(); // <-- make sure existing selection is present in the search results. + await picker?.clearAll(); + await picker?.saveAndClose(); await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([]); }); @@ -210,15 +210,15 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('rachel'); - let searchResults = await picker.getSearchResults(); + await picker?.enterSearchText('rachel'); + let searchResults = await picker?.getSearchResults(); - expect(searchResults.length).toEqual(1); + expect(searchResults?.length).toEqual(1); - await picker.clearSearchText(); - searchResults = await picker.getSearchResults(); + await picker?.clearSearchText(); + searchResults = await picker?.getSearchResults(); - expect(searchResults.length).toEqual(10); + expect(searchResults?.length).toEqual(10); }); it('should cancel the "Show more" picker', async () => { @@ -233,9 +233,9 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('ra'); - await picker.selectAll(); - await picker.cancel(); + await picker?.enterSearchText('ra'); + await picker?.selectAll(); + await picker?.cancel(); await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ 'Shirley', @@ -254,9 +254,9 @@ function testMultiselect(dataSkyId: string) { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.loadMore(); - await picker.selectSearchResult({ contentText: 'Vicki' }); - await picker.saveAndClose(); + await picker?.loadMore(); + await picker?.selectSearchResult({ contentText: 'Vicki' }); + await picker?.saveAndClose(); await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ 'Vicki', @@ -272,7 +272,7 @@ function testMultiselect(dataSkyId: string) { const picker = await lookupHarness.getShowMorePicker(); await expectAsync( - picker.selectSearchResult({ contentText: 'Invalid search' }) + picker?.selectSearchResult({ contentText: 'Invalid search' }) ).toBeRejectedWithError( 'Could not find search results in the picker matching filter(s): {"contentText":"Invalid search"}' ); @@ -309,7 +309,7 @@ describe('Lookup harness', () => { await lookupHarness.enterText('d'); - const results = await lookupHarness.getSearchResults(); + const results = (await lookupHarness.getSearchResults()) ?? []; await expectAsync(results[0].getDescriptorValue()).toBeResolvedTo('Abed'); await expectAsync(results[0].getText()).toBeResolvedTo( @@ -325,9 +325,9 @@ describe('Lookup harness', () => { await lookupHarness.clickShowMoreButton(); const picker = await lookupHarness.getShowMorePicker(); - await picker.enterSearchText('d'); + await picker?.enterSearchText('d'); - const results = await picker.getSearchResults(); + const results = (await picker?.getSearchResults()) ?? []; await expectAsync(results[0].getContentText()).toBeResolvedTo( 'Abed (Mr. Nadir)' ); diff --git a/libs/components/lookup/testing/src/search/search-fixture.ts b/libs/components/lookup/testing/src/search/search-fixture.ts index 0c4dd5eca3..411f7e9cc3 100644 --- a/libs/components/lookup/testing/src/search/search-fixture.ts +++ b/libs/components/lookup/testing/src/search/search-fixture.ts @@ -13,20 +13,20 @@ export class SkySearchFixture { * Gets the search's current text. */ public get searchText(): string { - return this.getInputEl().nativeElement.value; + return this.#getInputEl().nativeElement.value; } /** * Gets the search's current placeholder text. */ public get placeholderText(): string { - return this.getInputEl().nativeElement.placeholder; + return this.#getInputEl().nativeElement.placeholder; } - private debugEl: DebugElement; + #debugEl: DebugElement; constructor(fixture: ComponentFixture, skyTestId: string) { - this.debugEl = SkyAppTestUtility.getDebugElementByTestId( + this.#debugEl = SkyAppTestUtility.getDebugElementByTestId( fixture, skyTestId, 'sky-search' @@ -41,12 +41,12 @@ export class SkySearchFixture { public apply(searchText?: string) { if (searchText) { SkyAppTestUtility.setInputValue( - this.getInputEl().nativeElement, + this.#getInputEl().nativeElement, searchText ); } - const btnEl = this.getApplyBtnEl(); + const btnEl = this.#getApplyBtnEl(); btnEl.triggerEventHandler('click', {}); } @@ -56,7 +56,7 @@ export class SkySearchFixture { * not currently applied, an error is thrown. */ public clear() { - const clearEl = this.debugEl.query(By.css('.sky-input-group-clear')); + const clearEl = this.#debugEl.query(By.css('.sky-input-group-clear')); if (!SkyAppTestUtility.isVisible(clearEl)) { throw new Error( @@ -65,20 +65,20 @@ export class SkySearchFixture { ); } - const btnEl = this.getClearBtnEl(); + const btnEl = this.#getClearBtnEl(); btnEl.triggerEventHandler('click', {}); } - private getApplyBtnEl(): DebugElement { - return this.debugEl.query(By.css('.sky-search-btn-apply')); + #getApplyBtnEl(): DebugElement { + return this.#debugEl.query(By.css('.sky-search-btn-apply')); } - private getClearBtnEl(): DebugElement { - return this.debugEl.query(By.css('.sky-search-btn-clear')); + #getClearBtnEl(): DebugElement { + return this.#debugEl.query(By.css('.sky-search-btn-clear')); } - private getInputEl(): DebugElement { - return this.debugEl.query(By.css('.sky-search-input')); + #getInputEl(): DebugElement { + return this.#debugEl.query(By.css('.sky-search-input')); } } diff --git a/libs/components/lookup/testing/src/search/search-harness.spec.ts b/libs/components/lookup/testing/src/search/search-harness.spec.ts index 56835c876e..ada9a31705 100644 --- a/libs/components/lookup/testing/src/search/search-harness.spec.ts +++ b/libs/components/lookup/testing/src/search/search-harness.spec.ts @@ -6,7 +6,7 @@ import { SearchHarnessTestModule } from './fixtures/search-harness-test.module'; import { SkySearchHarness } from './search-harness'; describe('Search harness', () => { - async function setupTest(options: { dataSkyId?: string } = {}) { + async function setupTest(options: { dataSkyId: string }) { await TestBed.configureTestingModule({ imports: [SearchHarnessTestModule], }).compileComponents(); @@ -14,12 +14,9 @@ describe('Search harness', () => { const fixture = TestBed.createComponent(SearchHarnessTestComponent); const loader = TestbedHarnessEnvironment.loader(fixture); - let searchHarness: SkySearchHarness; - if (options.dataSkyId) { - searchHarness = await loader.getHarness( - SkySearchHarness.with({ dataSkyId: options.dataSkyId }) - ); - } + const searchHarness = await loader.getHarness( + SkySearchHarness.with({ dataSkyId: options.dataSkyId }) + ); return { searchHarness, fixture, loader }; } @@ -29,13 +26,13 @@ describe('Search harness', () => { dataSkyId: 'my-search-1', }); - await expectAsync(searchHarness.isFocused()).toBeResolvedTo(false); + await expectAsync(searchHarness?.isFocused()).toBeResolvedTo(false); - await searchHarness.focus(); - await expectAsync(searchHarness.isFocused()).toBeResolvedTo(true); + await searchHarness?.focus(); + await expectAsync(searchHarness?.isFocused()).toBeResolvedTo(true); - await searchHarness.blur(); - await expectAsync(searchHarness.isFocused()).toBeResolvedTo(false); + await searchHarness?.blur(); + await expectAsync(searchHarness?.isFocused()).toBeResolvedTo(false); }); it('should check if search is disabled', async () => { @@ -43,11 +40,11 @@ describe('Search harness', () => { dataSkyId: 'my-search-1', }); - await expectAsync(searchHarness.isDisabled()).toBeResolvedTo(false); + await expectAsync(searchHarness?.isDisabled()).toBeResolvedTo(false); fixture.componentInstance.disabled = true; - await expectAsync(searchHarness.isDisabled()).toBeResolvedTo(true); + await expectAsync(searchHarness?.isDisabled()).toBeResolvedTo(true); }); it('should clear the input value', async () => { @@ -56,12 +53,12 @@ describe('Search harness', () => { }); // First, set a value on the search. - await searchHarness.enterText('green'); - await expectAsync(searchHarness.getValue()).toBeResolvedTo('green'); + await searchHarness?.enterText('green'); + await expectAsync(searchHarness?.getValue()).toBeResolvedTo('green'); // Now, clear the value. - await searchHarness.clear(); - await expectAsync(searchHarness.getValue()).toBeResolvedTo(''); + await searchHarness?.clear(); + await expectAsync(searchHarness?.getValue()).toBeResolvedTo(''); }); it('should get the placeholder value', async () => { @@ -71,7 +68,7 @@ describe('Search harness', () => { fixture.componentInstance.placeholderText = 'My placeholder text.'; - await expectAsync(searchHarness.getPlaceholderText()).toBeResolvedTo( + await expectAsync(searchHarness?.getPlaceholderText()).toBeResolvedTo( 'My placeholder text.' ); }); From 90811866e56e772f95422db308ed7caf801cfac0 Mon Sep 17 00:00:00 2001 From: Sandhya Raja Sabeson Date: Thu, 13 Oct 2022 16:42:39 -0400 Subject: [PATCH 20/30] feat(components/indicators): change alertType to SkyIndicatorIconType (#683) BREAKING CHANGE: This change removes support for `alertType` on the alert component being an unaccepted string. To address this change the `alertType` to an accepted `SkyIndicatorTypeIcon` or remove it to use the default alert. --- .../indicators/alert/alert-routing.module.ts | 26 +++++++++++++++++++ .../indicators/alert/alert.component.html | 18 +++++++++++++ .../indicators/alert/alert.component.ts | 9 +++++++ .../indicators/alert/alert.module.ts | 14 ++++++++++ .../indicators/indicators.module.ts | 5 ++++ .../src/lib/modules/alert/alert.component.ts | 6 ++--- 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 apps/playground/src/app/components/indicators/alert/alert-routing.module.ts create mode 100644 apps/playground/src/app/components/indicators/alert/alert.component.html create mode 100644 apps/playground/src/app/components/indicators/alert/alert.component.ts create mode 100644 apps/playground/src/app/components/indicators/alert/alert.module.ts diff --git a/apps/playground/src/app/components/indicators/alert/alert-routing.module.ts b/apps/playground/src/app/components/indicators/alert/alert-routing.module.ts new file mode 100644 index 0000000000..fb921fcf9a --- /dev/null +++ b/apps/playground/src/app/components/indicators/alert/alert-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 { AlertDemoComponent } from './alert.component'; + +const routes: ComponentRouteInfo[] = [ + { + path: '', + component: AlertDemoComponent, + data: { + name: 'Alert', + icon: 'warning', + library: 'indicators', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AlertRoutingModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/indicators/alert/alert.component.html b/apps/playground/src/app/components/indicators/alert/alert.component.html new file mode 100644 index 0000000000..56f6de8b8e --- /dev/null +++ b/apps/playground/src/app/components/indicators/alert/alert.component.html @@ -0,0 +1,18 @@ +
        + + Info alert + + + Success alert + + + Warning alert + + + Danger alert + + Default alert + + Invalid alert + +
        diff --git a/apps/playground/src/app/components/indicators/alert/alert.component.ts b/apps/playground/src/app/components/indicators/alert/alert.component.ts new file mode 100644 index 0000000000..138aa57966 --- /dev/null +++ b/apps/playground/src/app/components/indicators/alert/alert.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-alert', + templateUrl: './alert.component.html', +}) +export class AlertDemoComponent { + public alertCloseable = true; +} diff --git a/apps/playground/src/app/components/indicators/alert/alert.module.ts b/apps/playground/src/app/components/indicators/alert/alert.module.ts new file mode 100644 index 0000000000..4a67d4cc71 --- /dev/null +++ b/apps/playground/src/app/components/indicators/alert/alert.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; + +import { AlertRoutingModule } from './alert-routing.module'; +import { AlertDemoComponent } from './alert.component'; + +@NgModule({ + declarations: [AlertDemoComponent], + imports: [CommonModule, AlertRoutingModule, SkyAlertModule], +}) +export class AlertModule { + public static routes = AlertRoutingModule.routes; +} diff --git a/apps/playground/src/app/components/indicators/indicators.module.ts b/apps/playground/src/app/components/indicators/indicators.module.ts index 7e3d3fdbc7..8b588a734d 100644 --- a/apps/playground/src/app/components/indicators/indicators.module.ts +++ b/apps/playground/src/app/components/indicators/indicators.module.ts @@ -18,6 +18,11 @@ const routes: Routes = [ path: 'wait', loadChildren: () => import('./wait/wait.module').then((m) => m.WaitModule), }, + { + path: 'alert', + loadChildren: () => + import('./alert/alert.module').then((m) => m.AlertModule), + }, ]; @NgModule({ 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 3e6e3e9a41..21043c6cce 100644 --- a/libs/components/indicators/src/lib/modules/alert/alert.component.ts +++ b/libs/components/indicators/src/lib/modules/alert/alert.component.ts @@ -23,17 +23,15 @@ const ALERT_TYPE_DEFAULT = 'warning'; templateUrl: './alert.component.html', }) export class SkyAlertComponent implements OnInit, OnDestroy { - // TODO: Change alertType to SkyIndicatorIconType in a breaking change. /** * Specifies a style for the alert to determine the icon and background color. * The valid options are `danger`, `info`, `success`, and `warning`. * @default "warning" */ @Input() - public set alertType(value: string | undefined) { + public set alertType(value: SkyIndicatorIconType | undefined) { if (value !== this.alertTypeOrDefault) { - this.alertTypeOrDefault = - (value as SkyIndicatorIconType) || ALERT_TYPE_DEFAULT; + this.alertTypeOrDefault = value || ALERT_TYPE_DEFAULT; this.#updateAlertIcon(); } } From 06fea9b59a55dcb90f1be49990df4399f6c5f62b Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 13 Oct 2022 16:57:09 -0400 Subject: [PATCH 21/30] refactor(components/tabs): vertical tabs strict mode (#652) --- .../vertical-tabset.component.ts | 8 +- ...vertical-tabset-ngfor.component.fixture.ts | 2 +- ...ical-tabset-no-active.component.fixture.ts | 2 +- ...tical-tabset-no-group.component.fixture.ts | 2 +- .../vertical-tabset.component.fixture.html | 9 +- .../vertical-tabset.component.fixture.ts | 7 +- .../vertical-tab-media-query.service.spec.ts | 9 +- .../vertical-tab-media-query.service.ts | 22 +-- .../vertical-tab.component.html | 4 +- .../vertical-tabset/vertical-tab.component.ts | 133 +++++++++------- .../vertical-tabset-adapter.service.ts | 16 +- .../vertical-tabset-group.component.html | 6 +- .../vertical-tabset-group.component.ts | 60 ++++--- .../vertical-tabset.component.html | 2 +- .../vertical-tabset.component.spec.ts | 149 +++++++++--------- .../vertical-tabset.component.ts | 58 +++---- .../vertical-tabset.service.spec.ts | 108 ++++--------- .../vertical-tabset.service.ts | 83 +++++----- 18 files changed, 329 insertions(+), 351 deletions(-) diff --git a/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.ts b/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.ts index 94fc4c543d..9df481c52f 100644 --- a/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.ts +++ b/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.ts @@ -47,7 +47,11 @@ export class VerticalTabsetComponent { }, ]; - constructor(private modalService: SkyModalService) {} + #modalService: SkyModalService; + + constructor(modalService: SkyModalService) { + this.#modalService = modalService; + } public onAddTabClick(): void { this.tabs.push({ @@ -63,6 +67,6 @@ export class VerticalTabsetComponent { } public openVerticalTabsetModal() { - this.modalService.open(VerticalTabsetModalComponent); + this.#modalService.open(VerticalTabsetModalComponent); } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-ngfor.component.fixture.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-ngfor.component.fixture.ts index 46c3754576..f4921ecd2f 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-ngfor.component.fixture.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-ngfor.component.fixture.ts @@ -5,7 +5,7 @@ import { Component } from '@angular/core'; templateUrl: './vertical-tabset-ngfor.component.fixture.html', }) export class VerticalTabsetWithNgForTestComponent { - public activeIndex: number; + public activeIndex: number | undefined; public maintainTabContent = false; public tabs = [ diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-active.component.fixture.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-active.component.fixture.ts index 208b42e68d..36307ddfa7 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-active.component.fixture.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-active.component.fixture.ts @@ -10,5 +10,5 @@ export class VerticalTabsetNoActiveTestComponent { public maintainTabContent = false; @ViewChild(SkyVerticalTabsetComponent) - public tabset: SkyVerticalTabsetComponent; + public tabset: SkyVerticalTabsetComponent | undefined; } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-group.component.fixture.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-group.component.fixture.ts index f7bb7152af..2c8a55f818 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-group.component.fixture.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset-no-group.component.fixture.ts @@ -5,7 +5,7 @@ import { Component } from '@angular/core'; templateUrl: './vertical-tabset-no-group.component.fixture.html', }) export class VerticalTabsetNoGroupTestComponent { - public currentIndex: number = undefined; + public currentIndex: number | undefined; public maintainTabContent = false; public indexChanged(index: number) { diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.html b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.html index d3c08caf1d..7577814f64 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.html @@ -1,7 +1,7 @@
        Group 1 Tab 1 content
        diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.ts index 3c8b46c4da..174cbd0021 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/fixtures/vertical-tabset.component.fixture.ts @@ -25,11 +25,14 @@ export class VerticalTabsetTestComponent { public showScrollable = false; public tabDisabled = true; + public tab1AriaRole: string | undefined = 'tab'; + public tab1Id: string | undefined = 'some-tab'; public tab1Required = false; + public tabsetAriaRole: string | undefined = 'tablist'; @ViewChild(SkyVerticalTabsetComponent) - public tabset: SkyVerticalTabsetComponent; + public tabset: SkyVerticalTabsetComponent | undefined; @ViewChildren(SkyVerticalTabComponent) - public verticalTabs: QueryList; + public verticalTabs: QueryList | undefined; } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.spec.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.spec.ts index 59ec53abff..79bedcc2ca 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.spec.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.spec.ts @@ -14,7 +14,7 @@ describe('Vertical tab media query service', () => { it('should handle initialization properly', inject( [SkyVerticalTabMediaQueryService], (mediaQueryService: SkyVerticalTabMediaQueryService) => { - let result: SkyMediaBreakpoints; + let result: SkyMediaBreakpoints | undefined; const subscription = mediaQueryService.subscribe( (args: SkyMediaBreakpoints) => { @@ -22,7 +22,10 @@ describe('Vertical tab media query service', () => { } ); - expect(result).toEqual(SkyMediaBreakpoints.xs); + expect(result).toBeUndefined(); + mediaQueryService.setBreakpointForWidth(20); + + expect(result).toBe(SkyMediaBreakpoints.xs); subscription.unsubscribe(); mediaQueryService.destroy(); @@ -45,7 +48,7 @@ describe('Vertical tab media query service', () => { it('should update the breakpoint correctly when setBreakPoint is called', inject( [SkyVerticalTabMediaQueryService], (mediaQueryService: SkyVerticalTabMediaQueryService) => { - let result: SkyMediaBreakpoints; + let result: SkyMediaBreakpoints | undefined; const subscription = mediaQueryService.subscribe( (args: SkyMediaBreakpoints) => { diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.ts index 155216532d..5cd23f1d31 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab-media-query.service.ts @@ -1,29 +1,23 @@ import { Injectable } from '@angular/core'; import { SkyMediaBreakpoints, SkyMediaQueryListener } from '@skyux/core'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { ReplaySubject, Subscription } from 'rxjs'; /** * @internal */ @Injectable() export class SkyVerticalTabMediaQueryService { - public get current(): SkyMediaBreakpoints { - return this._current; - } - - private currentSubject = new BehaviorSubject( - this.current - ); + public current: SkyMediaBreakpoints | undefined; - private _current = SkyMediaBreakpoints.xs; + #currentSubject: ReplaySubject; constructor() { - this.currentSubject.next(this._current); + this.#currentSubject = new ReplaySubject(1); } public subscribe(listener: SkyMediaQueryListener): Subscription { - return this.currentSubject.subscribe({ + return this.#currentSubject.subscribe({ next: (breakpoints: SkyMediaBreakpoints) => { listener(breakpoints); }, @@ -43,8 +37,8 @@ export class SkyVerticalTabMediaQueryService { breakpoint = SkyMediaBreakpoints.lg; } - this._current = breakpoint; - this.currentSubject.next(this._current); + this.current = breakpoint; + this.#currentSubject.next(this.current); } public isWidthWithinBreakpiont( @@ -75,6 +69,6 @@ export class SkyVerticalTabMediaQueryService { } public destroy(): void { - this.currentSubject.complete(); + this.#currentSubject.complete(); } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html index eec0724915..a3b1eea597 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html @@ -3,7 +3,7 @@ [attr.aria-controls]="isMobile ? null : ariaControls || tabContentPane.id" [attr.aria-selected]="active" [attr.id]="tabId" - [attr.role]="ariaRole" + [attr.role]="isMobile ? undefined : ariaRole" [ngClass]="{ 'sky-vertical-tab-active': active, 'sky-vertical-tab-disabled': disabled, @@ -31,7 +31,7 @@ indicatorType="danger" > diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts index dd74bf3e66..45c76839e4 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts @@ -47,7 +47,7 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy { * @deprecated Now that the vertical tabs provide aria labels automatically, this input is no longer necessary. */ @Input() - public ariaControls: string; + public ariaControls: string | undefined; /** * Specifies an ARIA role for the vertical tab @@ -61,27 +61,24 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy { */ @Input() public get ariaRole(): string { - if (this.isMobile) { - return undefined; - } - return this._ariaRole || 'tab'; + return this.#_ariaRole; } - public set ariaRole(value: string) { - this._ariaRole = value; + public set ariaRole(value: string | undefined) { + this.#_ariaRole = value ?? 'tab'; } /** * Indicates whether to disable the tab. */ @Input() - public disabled = false; + public disabled: boolean | undefined = false; /** * Indicates whether to indicate that the tab has an error. * @internal This is used for sectioned forms and is not currently a supported design for pure vertical tabs. */ @Input() - public errorIndicator = false; + public errorIndicator: boolean | undefined = false; /** * Displays an item count alongside the tab header to indicate how many list items the tab contains. @@ -101,92 +98,105 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy { * @internal */ @Input() - public get showTabRightArrow() { - return this._showTabRightArrow && this.tabsetService.isMobile(); - } - - public set showTabRightArrow(value: boolean) { - this._showTabRightArrow = value; - } + public showTabRightArrow: boolean | undefined; /** * Specifies an ID for the tab. * @deprecated Now that the vertical tabs provide aria labels automatically, this input is no longer necessary. */ @Input() - public tabId = `sky-vertical-tab-${++nextId}`; + public set tabId(value: string | undefined) { + this.#tabIdOrDefault = value || this.#defaultTabId; + } + + public get tabId(): string { + return this.#tabIdOrDefault; + } public set contentRendered(value: boolean) { - this._contentRendered = value; + this.#_contentRendered = value; /* istanbul ignore else */ - if (this._contentRendered) { + if (this.#_contentRendered) { // NOTE: Wrapped in a setTimeout here to ensure that everything has completed rendering. setTimeout(() => { - this.updateBreakpointAndResponsiveClass( - this.adapterService.getWidth(this.tabContent) - ); + if (this.tabContent) { + this.#updateBreakpointAndResponsiveClass(); + } }); } } public get contentRendered(): boolean { - return this._contentRendered; + return this.#_contentRendered; } - public index: number; + public index: number | undefined; public isMobile = false; @ViewChild('tabContentWrapper') - public tabContent: ElementRef; + public tabContent: ElementRef | undefined; + + #_ariaRole = 'tab'; + + #_contentRendered = false; - private _ariaRole: string; + #tabIdOrDefault: string; - private _contentRendered = false; + #defaultTabId: string; - private _mobileSubscription = new Subject(); + #mobileSubscription = new Subject(); - private _ngUnsubscribe = new Subject(); + #ngUnsubscribe = new Subject(); - private _showTabRightArrow = false; + #adapterService: SkyVerticalTabsetAdapterService; + #changeRef: ChangeDetectorRef; + #tabsetService: SkyVerticalTabsetService; + #verticalTabMediaQueryService: SkyVerticalTabMediaQueryService; constructor( - private adapterService: SkyVerticalTabsetAdapterService, - private changeRef: ChangeDetectorRef, - private tabsetService: SkyVerticalTabsetService, - private verticalTabMediaQueryService: SkyVerticalTabMediaQueryService - ) {} + adapterService: SkyVerticalTabsetAdapterService, + changeRef: ChangeDetectorRef, + tabsetService: SkyVerticalTabsetService, + verticalTabMediaQueryService: SkyVerticalTabMediaQueryService + ) { + this.#adapterService = adapterService; + this.#changeRef = changeRef; + this.#tabsetService = tabsetService; + this.#verticalTabMediaQueryService = verticalTabMediaQueryService; + this.#tabIdOrDefault = this.#defaultTabId = `sky-vertical-tab-${++nextId}`; + } public ngOnInit(): void { - this.isMobile = this.tabsetService.isMobile(); - this.changeRef.markForCheck(); + this.isMobile = this.#tabsetService.isMobile(); + this.#changeRef.markForCheck(); - this.tabsetService.switchingMobile.subscribe((mobile: boolean) => { + this.#tabsetService.switchingMobile.subscribe((mobile: boolean) => { this.isMobile = mobile; - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); }); // Update the breakpoint and responsive class here just as a sanity check since we can not // watch for element resizing. - this.tabsetService.indexChanged - .pipe(takeUntil(this._ngUnsubscribe)) + this.#tabsetService.indexChanged + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((index) => { if (this.index === index && this.contentRendered) { - this.updateBreakpointAndResponsiveClass( - this.adapterService.getWidth(this.tabContent) - ); + if (this.tabContent) { + this.#updateBreakpointAndResponsiveClass(); + } } }); - this.tabsetService.addTab(this); + this.#tabsetService.addTab(this); } public ngOnDestroy(): void { - this._mobileSubscription.unsubscribe(); - this._ngUnsubscribe.next(); - this._ngUnsubscribe.complete(); - this.tabsetService.destroyTab(this); + this.#mobileSubscription.unsubscribe(); + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + this.#tabsetService.destroyTab(this); } public tabIndex(): number { @@ -200,9 +210,9 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy { public activateTab(): void { if (!this.disabled) { this.active = true; - this.tabsetService.activateTab(this); + this.#tabsetService.activateTab(this); - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); } } @@ -224,22 +234,25 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy { @HostListener('window:resize') public onWindowResize(): void { - this.updateBreakpointAndResponsiveClass( - this.adapterService.getWidth(this.tabContent) - ); + if (this.tabContent) { + this.#updateBreakpointAndResponsiveClass(); + } } public tabDeactivated(): void { - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); } - private updateBreakpointAndResponsiveClass(width: number): void { - this.verticalTabMediaQueryService.setBreakpointForWidth(width); + #updateBreakpointAndResponsiveClass(): void { + if (this.tabContent) { + const width = this.#adapterService.getWidth(this.tabContent); + this.#verticalTabMediaQueryService.setBreakpointForWidth(width); - const newBreakpiont = this.verticalTabMediaQueryService.current; + const newBreakpoint = this.#verticalTabMediaQueryService.current; - this.adapterService.setResponsiveClass(this.tabContent, newBreakpiont); + this.#adapterService.setResponsiveClass(this.tabContent, newBreakpoint); - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); + } } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts index 7a11e99fba..ab67e6e5c2 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts @@ -8,10 +8,10 @@ import { SkyMediaBreakpoints } from '@skyux/core'; @Injectable() export class SkyVerticalTabsetAdapterService { - private renderer: Renderer2; + #renderer: Renderer2; - constructor(private rendererFactory: RendererFactory2) { - this.renderer = this.rendererFactory.createRenderer(undefined, undefined); + constructor(rendererFactory: RendererFactory2) { + this.#renderer = rendererFactory.createRenderer(undefined, null); } public getWidth(elementRef: ElementRef): number { @@ -28,10 +28,10 @@ export class SkyVerticalTabsetAdapterService { ): void { const nativeEl: HTMLElement = element.nativeElement; - this.renderer.removeClass(nativeEl, 'sky-responsive-container-xs'); - this.renderer.removeClass(nativeEl, 'sky-responsive-container-sm'); - this.renderer.removeClass(nativeEl, 'sky-responsive-container-md'); - this.renderer.removeClass(nativeEl, 'sky-responsive-container-lg'); + this.#renderer.removeClass(nativeEl, 'sky-responsive-container-xs'); + this.#renderer.removeClass(nativeEl, 'sky-responsive-container-sm'); + this.#renderer.removeClass(nativeEl, 'sky-responsive-container-md'); + this.#renderer.removeClass(nativeEl, 'sky-responsive-container-lg'); let newClass: string; @@ -54,6 +54,6 @@ export class SkyVerticalTabsetAdapterService { } } - this.renderer.addClass(nativeEl, newClass); + this.#renderer.addClass(nativeEl, newClass); } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html index c4192dd778..8238ae45f0 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html @@ -16,14 +16,14 @@ type="button" skyId [attr.aria-controls]="groupContent.id" - [attr.aria-expanded]="open" + [attr.aria-expanded]="!disabled && open" (click)="toggleMenuOpen()" #groupHeadingButton > {{ groupHeading }} @@ -33,7 +33,7 @@ role="region" skyId [attr.labelledby]="groupHeadingButton.id" - [@skyAnimationSlide]="open ? 'down' : 'up'" + [@skyAnimationSlide]="!disabled && open ? 'down' : 'up'" #groupContent > diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.ts index 44950cbd73..448bb52bb8 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.ts @@ -29,58 +29,56 @@ export class SkyVerticalTabsetGroupComponent implements OnInit, OnDestroy { * @default false */ @Input() - public disabled: boolean; + public disabled: boolean | undefined; /** * Specifies the header for the collapsible group of tabs. */ @Input() - public groupHeading: string; + public groupHeading: string | undefined; /** * Indicates whether the collapsible group is expanded. * @default false */ @Input() - public set open(value: boolean) { - this._open = value; - } - - public get open(): boolean { - return !this.disabled && this._open; - } + public open: boolean | undefined; @ContentChildren(SkyVerticalTabComponent) - private tabs: QueryList; + public tabs: QueryList | undefined; - private ngUnsubscribe = new Subject(); + #ngUnsubscribe = new Subject(); - private openBeforeTabsHidden = false; + #openBeforeTabsHidden: boolean | undefined = false; - private _open = false; + #tabService: SkyVerticalTabsetService; + #changeRef: ChangeDetectorRef; constructor( - private tabService: SkyVerticalTabsetService, - private changeRef: ChangeDetectorRef - ) {} + tabService: SkyVerticalTabsetService, + changeRef: ChangeDetectorRef + ) { + this.#tabService = tabService; + this.#changeRef = changeRef; + } public ngOnInit(): void { - this.tabService.hidingTabs - .pipe(takeUntil(this.ngUnsubscribe)) + this.#tabService.hidingTabs + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe(this.tabsHidden); - this.tabService.showingTabs - .pipe(takeUntil(this.ngUnsubscribe)) + this.#tabService.showingTabs + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe(this.tabsShown); - this.tabService.tabClicked - .pipe(takeUntil(this.ngUnsubscribe)) + this.#tabService.tabClicked + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe(this.tabClicked); } public ngOnDestroy(): void { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); } public toggleMenuOpen(): void { @@ -88,26 +86,26 @@ export class SkyVerticalTabsetGroupComponent implements OnInit, OnDestroy { this.open = !this.open; } - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); } public subMenuOpen(): boolean { - return this.tabs && this.tabs.find((t) => t.active) !== undefined; + return !!this.tabs && this.tabs.find((t) => !!t.active) !== undefined; } public tabClicked = () => { - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); }; public tabsHidden = () => { // this fixes an animation bug with ngIf when the parent component goes from visible to hidden - this.openBeforeTabsHidden = this.open; + this.#openBeforeTabsHidden = this.open; this.open = false; - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); }; public tabsShown = () => { - this.open = this.openBeforeTabsHidden; - this.changeRef.markForCheck(); + this.open = this.#openBeforeTabsHidden; + this.#changeRef.markForCheck(); }; } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.html b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.html index 053d120590..4b422c7763 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.html @@ -6,7 +6,7 @@ [ngClass]="{ 'sky-vertical-tabset-hidden': !tabService.tabsVisible() }" [attr.aria-label]="ariaLabel" [attr.aria-labelledby]="ariaLabelledBy" - [attr.role]="ariaRole" + [attr.role]="isMobile ? undefined : ariaRole" [@tabGroupEnter]="tabService.animationTabsVisibleState" #groupContainerWrapper > diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.spec.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.spec.ts index 3a843aeaa7..e534af7ce2 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.spec.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.spec.ts @@ -62,7 +62,7 @@ function getOpenTabGroups(fixture: ComponentFixture): HTMLElement[] { } function clickGroupButton(fixture: ComponentFixture, index: number): void { - getTabGroups(fixture)[index].querySelector('button').click(); + getTabGroups(fixture)[index].querySelector('button')?.click(); fixture.detectChanges(); tick(); } @@ -119,7 +119,7 @@ function expectVisibleTabContentPane( ): void { const visibleTabContent = getVisibleTabContentPane(fixture); expect(visibleTabContent.length).toBe(1); - expect(visibleTabContent[0].textContent.trim()).toBe(innerText); + expect(visibleTabContent[0]).toHaveText(innerText); } // #endregion @@ -160,7 +160,7 @@ describe('Vertical tabset component', () => { // check open group tab content const content = getVisibleTabContentPane(fixture)[0]; - expect(content.textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(content).toHaveText('Group 1 Tab 1 content'); }); it('open second tab in second group', fakeAsync(() => { @@ -245,9 +245,32 @@ describe('Vertical tabset component', () => { // check open tab content const tab = el.querySelector('sky-vertical-tab a'); - expect(tab.id).toBe('some-tab'); - expect(tab.getAttribute('aria-controls')).toBe('some-div'); - expect(tab.getAttribute('role')).toBe('tab'); + const tabset = el.querySelector( + 'sky-vertical-tabset .sky-vertical-tabset-group-container' + ); + expect(tab?.id).toBe('some-tab'); + expect(tab?.getAttribute('aria-controls')).toBe('some-div'); + expect(tab?.getAttribute('id')).toBe('some-tab'); + expect(tab?.getAttribute('role')).toBe('tab'); + expect(tabset?.getAttribute('role')).toBe('tablist'); + + fixture.componentInstance.tab1Id = undefined; + fixture.componentInstance.tab1AriaRole = undefined; + fixture.componentInstance.tabsetAriaRole = undefined; + fixture.detectChanges(); + expect(tab?.getAttribute('id')).toEqual( + jasmine.stringMatching(/sky-vertical-tab-[0-9]/) + ); + expect(tab?.getAttribute('role')).toBe('tab'); + expect(tabset?.getAttribute('role')).toBe('tablist'); + + fixture.componentInstance.tab1Id = 'tab-changed'; + fixture.componentInstance.tab1AriaRole = 'custom'; + fixture.componentInstance.tabsetAriaRole = 'custom'; + fixture.detectChanges(); + expect(tab?.getAttribute('id')).toBe('tab-changed'); + expect(tab?.getAttribute('role')).toBe('custom'); + expect(tabset?.getAttribute('role')).toBe('custom'); }); it('check closing of group', fakeAsync(() => { @@ -310,7 +333,7 @@ describe('Vertical tabset component', () => { // check content is visible const visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 1 content'); // check tabs are not visible const tabs = getTabsContainer(fixture); @@ -331,7 +354,7 @@ describe('Vertical tabset component', () => { // check content is visible let visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 1 content'); // click show tabs const showTabsButton = el.querySelector( @@ -378,7 +401,7 @@ describe('Vertical tabset component', () => { // check content is visible const visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 2 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 2 content'); }); it('tabs should not have tab aria associations and roles in mobile view', () => { @@ -410,7 +433,7 @@ describe('Vertical tabset component', () => { // simulate screensize change switching to mobile mockQueryService.fire(SkyMediaBreakpoints.xs); - fixture.componentInstance.tabset.tabService.updateContent(); + fixture.componentInstance.tabset?.tabService.updateContent(); fixture.detectChanges(); // check tabs are not visible @@ -420,7 +443,7 @@ describe('Vertical tabset component', () => { // check content is visible const visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 1 content'); // check show tabs button is visible const showTabsButton = el.querySelector( @@ -438,7 +461,7 @@ describe('Vertical tabset component', () => { // simulate screensize change switching to widescreen mockQueryService.fire(SkyMediaBreakpoints.lg); - fixture.componentInstance.tabset.tabService.updateContent(); + fixture.componentInstance.tabset?.tabService.updateContent(); fixture.detectChanges(); // check tabs are visible @@ -448,7 +471,7 @@ describe('Vertical tabset component', () => { // check content is visible const visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 1 content'); // check show tabs button is not visible const showTabsButton = el.querySelectorAll( @@ -472,7 +495,7 @@ describe('Vertical tabset component', () => { // check open tab let visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 1 content'); // check open group expectOpenGroup(fixture, 'Group 1'); @@ -486,7 +509,7 @@ describe('Vertical tabset component', () => { // check open tab visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 1 Tab 2 content'); + expect(visibleTabs[0]).toHaveText('Group 1 Tab 2 content'); // check open group expectOpenGroup(fixture, 'Group 1'); @@ -503,7 +526,7 @@ describe('Vertical tabset component', () => { const activeTab = el.querySelectorAll('.sky-vertical-tab-active'); expect(activeTab.length).toBe(1); const headerCount = activeTab[0].querySelector('.sky-vertical-tab-count'); - expect(headerCount.textContent.trim()).toBe('(5)'); + expect(headerCount).toHaveText('(5)'); }); it('should not display tab header count when not defined', fakeAsync(() => { @@ -550,7 +573,7 @@ describe('Vertical tabset component', () => { // check content is displayed let visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 2 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 2 Tab 1 content'); // try clicking disabled third tab in second group tabs[4].click(); @@ -560,7 +583,7 @@ describe('Vertical tabset component', () => { // check content of second tab still displayed visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 2 Tab 1 content'); + expect(visibleTabs[0]).toHaveText('Group 2 Tab 1 content'); flush(); })); @@ -582,27 +605,13 @@ describe('Vertical tabset component', () => { // check order of vertical tab content const allTabContentElements = getTabContentPanes(fixture); expect(allTabContentElements.length).toBe(7); - expect(allTabContentElements[0].textContent.trim()).toBe( - 'Group 1 Tab 1 content' - ); - expect(allTabContentElements[1].textContent.trim()).toBe( - 'Group 1 Tab 2 content' - ); - expect(allTabContentElements[2].textContent.trim()).toBe( - 'Group 2 Tab 1 content' - ); - expect(allTabContentElements[3].textContent.trim()).toBe( - 'Group 2 Tab 2 content' - ); - expect(allTabContentElements[4].textContent.trim()).toBe( - 'Group 2 Tab 3 content' - ); - expect(allTabContentElements[5].textContent.trim()).toBe( - 'Group 3 Tab 1 content' - ); - expect(allTabContentElements[6].textContent.trim()).toBe( - 'Group 3 Tab 2 content' - ); + expect(allTabContentElements[0]).toHaveText('Group 1 Tab 1 content'); + expect(allTabContentElements[1]).toHaveText('Group 1 Tab 2 content'); + expect(allTabContentElements[2]).toHaveText('Group 2 Tab 1 content'); + expect(allTabContentElements[3]).toHaveText('Group 2 Tab 2 content'); + expect(allTabContentElements[4]).toHaveText('Group 2 Tab 3 content'); + expect(allTabContentElements[5]).toHaveText('Group 3 Tab 1 content'); + expect(allTabContentElements[6]).toHaveText('Group 3 Tab 2 content'); // open second group clickGroupButton(fixture, 1); @@ -619,7 +628,7 @@ describe('Vertical tabset component', () => { // check open tab const visibleTabs = getVisibleTabContentPane(fixture); expect(visibleTabs.length).toBe(1); - expect(visibleTabs[0].textContent.trim()).toBe('Group 2 Tab 2 content'); + expect(visibleTabs[0]).toHaveText('Group 2 Tab 2 content'); // check open group expectOpenGroup(fixture, 'Group 2'); @@ -629,27 +638,13 @@ describe('Vertical tabset component', () => { // check order of vertical tab content - order should not change const allTabContentElements2 = getTabContentPanes(fixture); expect(allTabContentElements2.length).toBe(7); - expect(allTabContentElements2[0].textContent.trim()).toBe( - 'Group 1 Tab 1 content' - ); - expect(allTabContentElements2[1].textContent.trim()).toBe( - 'Group 1 Tab 2 content' - ); - expect(allTabContentElements2[2].textContent.trim()).toBe( - 'Group 2 Tab 1 content' - ); - expect(allTabContentElements2[3].textContent.trim()).toBe( - 'Group 2 Tab 2 content' - ); - expect(allTabContentElements2[4].textContent.trim()).toBe( - 'Group 2 Tab 3 content' - ); - expect(allTabContentElements2[5].textContent.trim()).toBe( - 'Group 3 Tab 1 content' - ); - expect(allTabContentElements2[6].textContent.trim()).toBe( - 'Group 3 Tab 2 content' - ); + expect(allTabContentElements2[0]).toHaveText('Group 1 Tab 1 content'); + expect(allTabContentElements2[1]).toHaveText('Group 1 Tab 2 content'); + expect(allTabContentElements2[2]).toHaveText('Group 2 Tab 1 content'); + expect(allTabContentElements2[3]).toHaveText('Group 2 Tab 2 content'); + expect(allTabContentElements2[4]).toHaveText('Group 2 Tab 3 content'); + expect(allTabContentElements2[5]).toHaveText('Group 3 Tab 1 content'); + expect(allTabContentElements2[6]).toHaveText('Group 3 Tab 2 content'); })); it('should add the appropriate responsive container upon initialization', async () => { @@ -662,13 +657,13 @@ describe('Vertical tabset component', () => { const fixture = createTestComponent(); fixture.detectChanges(); - const activeTab = fixture.componentInstance.verticalTabs.find( - (tab) => tab.active + const activeTab = fixture.componentInstance.verticalTabs?.find( + (tab) => !!tab.active ); fixture.detectChanges(); await fixture.whenStable(); - const tabContentPane: HTMLElement = activeTab.tabContent.nativeElement; + const tabContentPane: HTMLElement = activeTab?.tabContent?.nativeElement; expect(mediaQuerySpy).toHaveBeenCalledWith(640); expect( tabContentPane.classList.contains('sky-responsive-container-xs') @@ -684,13 +679,13 @@ describe('Vertical tabset component', () => { ).and.returnValue(1500); fixture.detectChanges(); - const activeTab = fixture.componentInstance.verticalTabs.find( - (tab) => tab.active + const activeTab = fixture.componentInstance.verticalTabs?.find( + (tab) => !!tab.active ); fixture.detectChanges(); await fixture.whenStable(); - let tabContentPane: HTMLElement = activeTab.tabContent.nativeElement; + let tabContentPane: HTMLElement = activeTab?.tabContent?.nativeElement; expect( tabContentPane.classList.contains('sky-responsive-container-lg') @@ -704,7 +699,7 @@ describe('Vertical tabset component', () => { SkyAppTestUtility.fireDomEvent(window, 'resize'); fixture.detectChanges(); - tabContentPane = activeTab.tabContent.nativeElement; + tabContentPane = activeTab?.tabContent?.nativeElement; expect(mediaQuerySpy).toHaveBeenCalledWith(1100); expect( tabContentPane.classList.contains('sky-responsive-container-md') @@ -729,10 +724,10 @@ describe('Vertical tabset component', () => { tick(); fixture.detectChanges(); - const activeTab = fixture.componentInstance.verticalTabs.find( - (tab) => tab.active + const activeTab = fixture.componentInstance.verticalTabs?.find( + (tab) => !!tab.active ); - const tabContentPane: HTMLElement = activeTab.tabContent.nativeElement; + const tabContentPane: HTMLElement = activeTab?.tabContent?.nativeElement; expect(mediaQuerySpy).toHaveBeenCalledWith(800); expect( tabContentPane.classList.contains('sky-responsive-container-sm') @@ -752,7 +747,9 @@ describe('Vertical tabset component', () => { const contentPane = document.querySelector('.sky-vertical-tabset-content'); - contentPane.scrollTop = 200; + if (contentPane) { + contentPane.scrollTop = 200; + } fixture.detectChanges(); @@ -762,7 +759,7 @@ describe('Vertical tabset component', () => { fixture.detectChanges(); - expect(contentPane.scrollTop).toBe(0); + expect(contentPane?.scrollTop).toBe(0); }); it('should be accessible when content pane is scrollable', async () => { @@ -846,7 +843,7 @@ describe('Vertical tabset component - with ngFor', () => { let tabContent = visibleTabContent[0]; expect(tabContent).not.toBeUndefined(); expect(fixture.componentInstance.activeIndex).toEqual(0); - expect(tabContent.textContent.trim()).toBe('Tab 2 content'); + expect(tabContent).toHaveText('Tab 2 content'); // Now, remove last (second) tab from array. component.tabs.splice(1, 1); @@ -858,7 +855,7 @@ describe('Vertical tabset component - with ngFor', () => { tabContent = visibleTabContent[0]; expect(tabContent).not.toBeUndefined(); expect(fixture.componentInstance.activeIndex).toEqual(0); - expect(tabContent.textContent.trim()).toBe('Tab 2 content'); + expect(tabContent).toHaveText('Tab 2 content'); }); }); @@ -929,7 +926,7 @@ describe('Vertical tabset no active tabs', () => { it('should not fail when trying to move active content when no tabs are active', () => { // move content should not fail - fixture.componentInstance.tabset.tabService.updateContent(); + fixture.componentInstance.tabset?.tabService.updateContent(); }); it('should be accessible', async () => { diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.ts index 4224f16037..daae985e53 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.component.ts @@ -52,7 +52,7 @@ export class SkyVerticalTabsetComponent * Specifies the text to display on the show tabs button on mobile devices. */ @Input() - public showTabsText: string; + public showTabsText: string | undefined; /** * Specifies an ARIA label for the tabset. This sets the tabset's `aria-label` attribute @@ -60,7 +60,7 @@ export class SkyVerticalTabsetComponent * If the tabset includes a visible label, use `ariaLabelledBy` instead. */ @Input() - public ariaLabel: string; + public ariaLabel: string | undefined; /** * Specifies the HTML element ID (without the leading `#`) of the element that labels @@ -69,7 +69,7 @@ export class SkyVerticalTabsetComponent * If the tabset does not include a visible label, use `ariaLabel` instead. */ @Input() - public ariaLabelledBy: string; + public ariaLabelledBy: string | undefined; /** * Specifies an ARIA role for the vertical tabset @@ -83,13 +83,11 @@ export class SkyVerticalTabsetComponent */ @Input() public get ariaRole(): string { - if (this.isMobile) { - return undefined; - } - return this._ariaRole || 'tablist'; + return this.#_ariaRole; } - public set ariaRole(value: string) { - this._ariaRole = value; + + public set ariaRole(value: string | undefined) { + this.#_ariaRole = value ?? 'tablist'; } /** @@ -98,7 +96,7 @@ export class SkyVerticalTabsetComponent * @default false */ @Input() - public maintainTabContent = false; + public maintainTabContent: boolean | undefined = false; /** * Fires when the active tab changes. Emits the index of the active tab. The @@ -108,52 +106,58 @@ export class SkyVerticalTabsetComponent public activeChange = new EventEmitter(); @ViewChild('groupContainerWrapper') - public tabGroups: ElementRef; + public tabGroups: ElementRef | undefined; @ViewChild('skySideContent') - public content: ElementRef; + public content: ElementRef | undefined; @ViewChild('contentContainerWrapper') - private contentWrapper: ElementRef; + public contentWrapper: ElementRef | undefined; - private isMobile = false; - private _ngUnsubscribe = new Subject(); - private _ariaRole: string; + public isMobile = false; + #ngUnsubscribe = new Subject(); + #_ariaRole = 'tablist'; + + #resources: SkyLibResourcesService; + #changeRef: ChangeDetectorRef; constructor( public adapterService: SkyVerticalTabsetAdapterService, public tabService: SkyVerticalTabsetService, - private resources: SkyLibResourcesService, - private changeRef: ChangeDetectorRef - ) {} + resources: SkyLibResourcesService, + changeRef: ChangeDetectorRef + ) { + this.#resources = resources; + this.#changeRef = changeRef; + } public ngOnInit() { this.tabService.maintainTabContent = this.maintainTabContent; this.tabService.indexChanged - .pipe(takeUntil(this._ngUnsubscribe)) + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((index: any) => { this.activeChange.emit(index); if (this.contentWrapper) { this.adapterService.scrollToContentTop(this.contentWrapper); } - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); }); this.tabService.switchingMobile - .pipe(takeUntil(this._ngUnsubscribe)) + .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((mobile: boolean) => { this.isMobile = mobile; - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); }); if (this.tabService.isMobile()) { this.isMobile = true; this.tabService.animationContentVisibleState = VISIBLE_STATE; - this.changeRef.markForCheck(); + this.#changeRef.markForCheck(); } if (!this.showTabsText) { - this.resources + this.#resources .getString('skyux_vertical_tabs_show_tabs_text') .pipe(take(1)) .subscribe((resource) => { @@ -172,7 +176,7 @@ export class SkyVerticalTabsetComponent } public ngOnDestroy() { - this._ngUnsubscribe.next(); - this._ngUnsubscribe.complete(); + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.spec.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.spec.ts index f14f7a5454..cc8f48dc7d 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.spec.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.spec.ts @@ -1,16 +1,9 @@ import { MockSkyMediaQueryService } from '@skyux/core/testing'; -import { SkyVerticalTabComponent } from './vertical-tab.component'; import { SkyVerticalTabsetService } from './vertical-tabset.service'; -class MockChangeDetector { - public detectChanges() {} - public markForCheck() {} -} - describe('Vertical tabset service', () => { let service: SkyVerticalTabsetService; - const mockDetectChanges: any = new MockChangeDetector(); const mockQueryService = new MockSkyMediaQueryService(); beforeEach(() => { @@ -18,24 +11,18 @@ describe('Vertical tabset service', () => { }); it('should add two non active tabs', () => { - const tab1 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab1 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab1.tabHeading = 'tab 1'; - const tab2 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab2 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab2.tabHeading = 'tab 2'; service.tabClicked.subscribe((clicked) => { - if (service.activeIndex >= 0) { + if (service.activeIndex && service.activeIndex >= 0) { fail( `tab should not have been clicked with index =${service.activeIndex}` ); @@ -54,22 +41,16 @@ describe('Vertical tabset service', () => { }); it('should add active tab', () => { - const tab1 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); - const tab2 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab1 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); + const tab2 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab2.active = true; service.tabClicked.subscribe((clicked) => { - if (service.activeIndex >= 0) { + if (service.activeIndex && service.activeIndex >= 0) { expect(service.activeIndex).toBe(1); } }); @@ -81,19 +62,13 @@ describe('Vertical tabset service', () => { }); it('should deactive old active tab', () => { - const tab1 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab1 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab1.active = true; - const tab2 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab2 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); service.addTab(tab1); service.addTab(tab2); @@ -109,18 +84,12 @@ describe('Vertical tabset service', () => { }); it('content should return undefined when no active tabs', () => { - const tab1 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); - const tab2 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab1 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); + const tab2 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); service.addTab(tab1); service.addTab(tab2); @@ -129,20 +98,14 @@ describe('Vertical tabset service', () => { }); it('destroy tab removes it from the service', () => { - const tab1 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab1 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab1.tabHeading = 'tab 1'; - const tab2 = new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ); + const tab2 = jasmine.createSpyObj('SkyVerticalTabComponent', [ + 'tabDeactivated', + ]); tab2.tabHeading = 'tab 2'; service.addTab(tab1); @@ -152,12 +115,7 @@ describe('Vertical tabset service', () => { // attempt to destroy tab not existing in service service.destroyTab( - new SkyVerticalTabComponent( - undefined, - mockDetectChanges, - undefined, - undefined - ) + jasmine.createSpyObj('SkyVerticalTabComponent', ['tabDeactivated']) ); expect(service.tabs.length).toBe(2); diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.ts index c8f5f20b49..5a7e6b3fff 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset.service.ts @@ -1,7 +1,7 @@ import { ElementRef, Injectable } from '@angular/core'; import { SkyMediaBreakpoints, SkyMediaQueryService } from '@skyux/core'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs'; import { SkyVerticalTabComponent } from './vertical-tab.component'; @@ -13,17 +13,17 @@ export const HIDDEN_STATE = 'void'; */ @Injectable() export class SkyVerticalTabsetService { - public activeIndex: number = undefined; + public activeIndex: number | undefined = undefined; - public animationContentVisibleState: string; + public animationContentVisibleState: string | undefined; - public animationTabsVisibleState: string; + public animationTabsVisibleState: string | undefined; public content: ElementRef | undefined; public hidingTabs = new BehaviorSubject(false); - public indexChanged: BehaviorSubject = new BehaviorSubject(undefined); + public indexChanged: ReplaySubject = new ReplaySubject(1); public maintainTabContent: boolean | undefined = false; @@ -35,35 +35,38 @@ export class SkyVerticalTabsetService { public tabAdded: Subject = new Subject(); - public tabClicked: BehaviorSubject = new BehaviorSubject(undefined); + public tabClicked: ReplaySubject = new ReplaySubject(1); - private _contentAdded = false; + #contentAdded = false; - private _tabsVisible = false; + #tabsVisible = false; - private _isMobile = false; + #isMobile = false; - public constructor(private mediaQueryService: SkyMediaQueryService) { - this.mediaQueryService.subscribe((breakpoint) => { + #mediaQueryService: SkyMediaQueryService; + + public constructor(mediaQueryService: SkyMediaQueryService) { + this.#mediaQueryService = mediaQueryService; + this.#mediaQueryService.subscribe((breakpoint) => { const nowMobile = breakpoint === SkyMediaBreakpoints.xs; - if (nowMobile && !this._isMobile) { + if (nowMobile && !this.#isMobile) { // switching to mobile this.switchingMobile.next(true); - if (!this._tabsVisible) { + if (!this.#tabsVisible) { this.hidingTabs.next(true); } - } else if (!nowMobile && this._isMobile) { + } else if (!nowMobile && this.#isMobile) { // switching to widescreen this.switchingMobile.next(false); - if (!this._tabsVisible) { + if (!this.#tabsVisible) { this.showingTabs.next(true); } } - this._isMobile = nowMobile; + this.#isMobile = nowMobile; }); } @@ -90,9 +93,9 @@ export class SkyVerticalTabsetService { this.maintainTabContent && tab.contentRendered && /* istanbul ignore next */ - this.content?.nativeElement.contains(tab.tabContent.nativeElement) + this.content?.nativeElement.contains(tab.tabContent?.nativeElement) ) { - this.content?.nativeElement.removeChild(tab.tabContent.nativeElement); + this.content?.nativeElement.removeChild(tab.tabContent?.nativeElement); } this.tabs.splice(tabIndex, 1); @@ -101,7 +104,7 @@ export class SkyVerticalTabsetService { if (tab.active) { if (!this.maintainTabContent) { - this.destroyContent(); + this.#destroyContent(); } // Try selecting the next tab first, and if there's no next tab then // try selecting the previous one. @@ -123,30 +126,32 @@ export class SkyVerticalTabsetService { this.activeIndex = tab.index; this.tabClicked.next(true); - this.updateTabClicked(); + this.#updateTabClicked(); } - public activeTab(): SkyVerticalTabComponent { + public activeTab(): SkyVerticalTabComponent | undefined { return this.tabs.find((t) => t.index === this.activeIndex); } public isMobile() { - return this._isMobile; + return this.#isMobile; } public updateContent() { if (!this.maintainTabContent) { - if (!this._contentAdded && this.contentVisible()) { + if (!this.#contentAdded && this.contentVisible()) { // content needs to be moved - this.moveContent(); - } else if (this._contentAdded && !this.contentVisible()) { + this.#moveContent(); + } else if (this.#contentAdded && !this.contentVisible()) { // content hidden - this._contentAdded = false; + this.#contentAdded = false; } } else { this.tabs.forEach((tab) => { if (!tab.contentRendered) { - this.content?.nativeElement.appendChild(tab.tabContent.nativeElement); + this.content?.nativeElement.appendChild( + tab.tabContent?.nativeElement + ); tab.contentRendered = true; } }); @@ -154,47 +159,47 @@ export class SkyVerticalTabsetService { } public tabsVisible() { - return !this.isMobile() || this._tabsVisible; + return !this.isMobile() || this.#tabsVisible; } public contentVisible() { - return !this.isMobile() || !this._tabsVisible; + return !this.isMobile() || !this.#tabsVisible; } public showTabs() { - this._tabsVisible = true; - this._contentAdded = false; + this.#tabsVisible = true; + this.#contentAdded = false; this.animationTabsVisibleState = VISIBLE_STATE; this.animationContentVisibleState = HIDDEN_STATE; this.showingTabs.next(true); } - private destroyContent(): void { + #destroyContent(): void { if (this.content) { this.content.nativeElement.innerHTML = ''; } this.content = undefined; } - private moveContent() { + #moveContent() { /* istanbul ignore else */ - if (this.content && !this._contentAdded) { + if (this.content && !this.#contentAdded) { const activeTab = this.activeTab(); const activeContent = activeTab ? activeTab.tabContent : undefined; - if (activeContent && activeContent.nativeElement) { + if (activeContent && activeTab && activeContent.nativeElement) { this.content.nativeElement.appendChild(activeContent.nativeElement); activeTab.contentRendered = true; - this._contentAdded = true; + this.#contentAdded = true; } } } - private updateTabClicked() { - this._contentAdded = false; + #updateTabClicked() { + this.#contentAdded = false; if (this.isMobile()) { - this._tabsVisible = false; + this.#tabsVisible = false; this.animationContentVisibleState = VISIBLE_STATE; this.animationTabsVisibleState = HIDDEN_STATE; this.hidingTabs.next(true); From 6966a09ad7cf7bbc229a39e33be12c2d818d9b93 Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Fri, 14 Oct 2022 09:41:24 -0400 Subject: [PATCH 22/30] docs(components/indicators): key info code example styling and default value (#682) (#684) --- .../indicators/key-info/basic/key-info-demo.component.html | 4 +++- .../indicators/key-info/basic/key-info-demo.component.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.html b/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.html index 8b01bf973d..f21c9ea0bb 100644 --- a/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.html +++ b/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.html @@ -1,4 +1,6 @@ - {{ value }} + + {{ value }} + New members diff --git a/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.ts b/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.ts index 93dd7d08eb..e506e5f3e5 100644 --- a/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.ts +++ b/apps/code-examples/src/app/code-examples/indicators/key-info/basic/key-info-demo.component.ts @@ -7,7 +7,7 @@ import { SkyKeyInfoLayoutType } from '@skyux/indicators'; }) export class KeyInfoDemoComponent { @Input() - public set value(value: number) { + public set value(value: number | undefined) { this.#_value = value; this.layout = this.#_value >= 100 ? 'vertical' : 'horizontal'; } @@ -18,5 +18,5 @@ export class KeyInfoDemoComponent { public layout: SkyKeyInfoLayoutType = 'vertical'; - #_value: number | undefined; + #_value: number | undefined = 575; } From ca1b45dc48fb01e576a7f8bc6fc5263544fed40f Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Fri, 14 Oct 2022 15:46:31 -0400 Subject: [PATCH 23/30] chore: release 6.25.0 (#672) (#690) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc88e65105..9cf89cd851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [6.25.0](https://github.com/blackbaud/skyux/compare/6.24.0...6.25.0) (2022-10-13) + + +### Features + +* **components/forms:** support status indicator errors for input box ([#633](https://github.com/blackbaud/skyux/issues/633)) ([7648638](https://github.com/blackbaud/skyux/commit/764863802c3e4d18212dbd86fe390e14c3df0fb2)) + + +### Bug Fixes + +* **components/lookup:** modern search clickbox takes up entire input box ([#677](https://github.com/blackbaud/skyux/issues/677)) ([85330ed](https://github.com/blackbaud/skyux/commit/85330ed879054cd8967d9a075589ea601775509f)) +* **components/text-editor:** toolbars are hidden when no items exist within the toolbars ([#676](https://github.com/blackbaud/skyux/issues/676)) ([b2ba8de](https://github.com/blackbaud/skyux/commit/b2ba8de9952306c576bd04b066b70626cb756eee)) + ## [6.24.0](https://github.com/blackbaud/skyux/compare/6.23.3...6.24.0) (2022-10-10) From 69421fd21531178ce438a6a77049f778e75b2d49 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Fri, 14 Oct 2022 16:57:45 -0400 Subject: [PATCH 24/30] chore: release 7.0.0-beta.3 (#657) --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf89cd851..973732b606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [7.0.0-beta.3](https://github.com/blackbaud/skyux/compare/7.0.0-beta.2...7.0.0-beta.3) (2022-10-14) + + +### ⚠ BREAKING CHANGES + +* **components/indicators:** This change removes support for `alertType` on the alert component being an unaccepted string. To address this change, change the `alertType` to an accepted `SkyIndicatorTypeIcon` or remove it to use the default `alertType` of `'warning'`. +* **components/forms:** The `SkyFileDrop` and `SkyFileAttachment` components' `validateFn` input type was updated to receive a `SkyFileType` parameter and return a string or undefined. To address this, ensure all `validateFn` inputs have the correct parameter and return types. +* **components/config:** The config params `get` function was updated to accurately reflect that it may return undefined. To address this change, account for a possible undefined value wherever you are using the `get` function. +* **components/modals:** `SkyModalConfigurationInterface.providers` accepts an array of `StaticProvider`s instead of any value. +* **components/modals:** `SkyConfirmButton`'s `styleType` will only accept predefined strings of type `SkyConfirmButtonStyleType`. To address this, ensure `styleType` is only being set to a supported value. +* **components/modals:** The `SkyConfirmButton` component is intended for internal use only and is removed from the exported API. To address this, remove any usages of the `SkyConfirmButton` component. + +### Features + +* **components/angular-tree-component:** add inline help support for angular tree component ([#659](https://github.com/blackbaud/skyux/issues/659)) ([3fbabf2](https://github.com/blackbaud/skyux/commit/3fbabf28cb406a220aa4d7dbfe282b8a81e6365a)) +* **components/config:** add more specific typing to config params function return types ([#668](https://github.com/blackbaud/skyux/issues/668)) ([102cd0a](https://github.com/blackbaud/skyux/commit/102cd0a97a5b64c78e469b462fe1f59601e44557)) +* **components/forms:** update file attachment validateFn inputs to more specific type ([#669](https://github.com/blackbaud/skyux/issues/669)) ([95b7ab5](https://github.com/blackbaud/skyux/commit/95b7ab59f6352a591dcff17da5d76c3e9c4d3325)) +* **components/indicators:** change `alertType` to `SkyIndicatorIconType` ([#683](https://github.com/blackbaud/skyux/issues/683)) ([9081186](https://github.com/blackbaud/skyux/commit/90811866e56e772f95422db308ed7caf801cfac0)) +* **components/indicators:** remove bottom margin from alert component ([#648](https://github.com/blackbaud/skyux/issues/648)) ([5bd8762](https://github.com/blackbaud/skyux/commit/5bd87621ba412cebb38285b6e9ece256e07bbe6b)) +* **components/lookup:** deprecate search inputs ([#647](https://github.com/blackbaud/skyux/issues/647)) ([74396bb](https://github.com/blackbaud/skyux/commit/74396bb18906e82e86fa920276c8f709bd5b0143)) +* **components/modals:** improve `SkyModalConfigurationInterface.providers` type ([#665](https://github.com/blackbaud/skyux/issues/665)) ([a65dae0](https://github.com/blackbaud/skyux/commit/a65dae0642b45764fed92d9671e2830e0f1cc24e)) +* **components/modals:** remove 'string' from `SkyConfirmButton`'s `styleType` type ([#664](https://github.com/blackbaud/skyux/issues/664)) ([8fda84e](https://github.com/blackbaud/skyux/commit/8fda84ebf9afa68e0c436578dbb6177f6cc7bfdd)) +* **components/modals:** remove public export of confirm button ([#656](https://github.com/blackbaud/skyux/issues/656)) ([f465207](https://github.com/blackbaud/skyux/commit/f46520739ebf874d759efa372a809d19cee3afb6)) +* **components/tabs:** add descriptive aria label to tab buttons ([#586](https://github.com/blackbaud/skyux/issues/586)) ([#660](https://github.com/blackbaud/skyux/issues/660)) ([9a01d54](https://github.com/blackbaud/skyux/commit/9a01d549d498a9616d16aae4e3334b878372da3e)) +* **components/toast:** improve toast service `openComponent` `component` param type ([#667](https://github.com/blackbaud/skyux/issues/667)) ([8ffa182](https://github.com/blackbaud/skyux/commit/8ffa182538269488b561fda377dc677927f0e227)) + + +### Bug Fixes + +* **components/lookup:** modern search clickbox takes up entire input box ([#677](https://github.com/blackbaud/skyux/issues/677)) ([#679](https://github.com/blackbaud/skyux/issues/679)) ([2b70b38](https://github.com/blackbaud/skyux/commit/2b70b383a69bbe0c7028e0fdfaeb129d0c6fb1fa)) +* **components/text-editor:** toolbars are hidden when no items exist within the toolbars ([#676](https://github.com/blackbaud/skyux/issues/676)) ([#678](https://github.com/blackbaud/skyux/issues/678)) ([9711a84](https://github.com/blackbaud/skyux/commit/9711a842e8c3a5c6887adfdfceab6719001a4a1e)) + ## [6.25.0](https://github.com/blackbaud/skyux/compare/6.24.0...6.25.0) (2022-10-13) diff --git a/package-lock.json b/package-lock.json index 0ec7219723..012f34bd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "7.0.0-beta.2", + "version": "7.0.0-beta.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1330fffc82..a93121f39f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "7.0.0-beta.2", + "version": "7.0.0-beta.3", "license": "MIT", "scripts": { "ng": "nx", From 9f14409eef2b1b430882a1b5c7b22d4cc6c24f59 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Fri, 14 Oct 2022 17:27:12 -0400 Subject: [PATCH 25/30] chore: update release please 'release-as' to 7.0.0-beta.4 (#692) --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 7ec177ff85..00a74339d8 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -17,7 +17,7 @@ jobs: package-name: 'skyux' pull-request-title-pattern: 'chore: release ${version}' labels: 'autorelease ${{ github.ref_name }}: pending' - release-as: 7.0.0-beta.3 + release-as: 7.0.0-beta.4 release-labels: 'autorelease ${{ github.ref_name }}: tagged' prerelease: true draft-pull-request: true From 192506cf46f777bca6eb0e6e0931dd6ee0b141f0 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Fri, 14 Oct 2022 17:39:58 -0400 Subject: [PATCH 26/30] refactor(components/core): replace calls to ComponentFactoryResolver (#685) * refactor(components/core): replace calls to ComponentFactoryResolver * Simplify changes. --- .../core/src/lib/modules/dock/dock.component.ts | 15 +++------------ .../dynamic-component.service.ts | 15 +++++---------- .../src/lib/modules/overlay/overlay.component.ts | 15 +++------------ 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/libs/components/core/src/lib/modules/dock/dock.component.ts b/libs/components/core/src/lib/modules/dock/dock.component.ts index b69c6c40ac..bbc917856f 100644 --- a/libs/components/core/src/lib/modules/dock/dock.component.ts +++ b/libs/components/core/src/lib/modules/dock/dock.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ComponentFactoryResolver, ElementRef, Injector, Type, @@ -46,18 +45,13 @@ export class SkyDockComponent { #options: SkyDockOptions | undefined; - #resolver: ComponentFactoryResolver; - - // TODO: Replace deprecated `ComponentFactoryResolver`. constructor( changeDetector: ChangeDetectorRef, - resolver: ComponentFactoryResolver, elementRef: ElementRef, injector: Injector, domAdapter: SkyDockDomAdapterService ) { this.#changeDetector = changeDetector; - this.#resolver = resolver; this.#elementRef = elementRef; this.#injector = injector; this.#domAdapter = domAdapter; @@ -74,17 +68,14 @@ export class SkyDockComponent { ); } - const factory = this.#resolver.resolveComponentFactory(component); const injector = Injector.create({ providers: config.providers || [], parent: this.#injector, }); - const componentRef = this.target.createComponent( - factory, - undefined, - injector - ); + const componentRef = this.target.createComponent(component, { + injector, + }); const stackOrder = config.stackOrder !== null && config.stackOrder !== undefined ? config.stackOrder diff --git a/libs/components/core/src/lib/modules/dynamic-component/dynamic-component.service.ts b/libs/components/core/src/lib/modules/dynamic-component/dynamic-component.service.ts index 52c2b583c9..55a63c25ac 100644 --- a/libs/components/core/src/lib/modules/dynamic-component/dynamic-component.service.ts +++ b/libs/components/core/src/lib/modules/dynamic-component/dynamic-component.service.ts @@ -1,6 +1,5 @@ import { ApplicationRef, - ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injectable, @@ -8,6 +7,7 @@ import { Renderer2, RendererFactory2, Type, + createComponent, } from '@angular/core'; import { SkyAppWindowRef } from '../window/window-ref'; @@ -27,23 +27,18 @@ import { SkyDynamicComponentOptions } from './dynamic-component-options'; export class SkyDynamicComponentService { #applicationRef: ApplicationRef; - #componentFactoryResolver: ComponentFactoryResolver; - #injector: Injector; #renderer: Renderer2; #windowRef: SkyAppWindowRef; - // TODO: Replace deprecated `ComponentFactoryResolver`. constructor( - componentFactoryResolver: ComponentFactoryResolver, applicationRef: ApplicationRef, injector: Injector, windowRef: SkyAppWindowRef, rendererFactory: RendererFactory2 ) { - this.#componentFactoryResolver = componentFactoryResolver; this.#applicationRef = applicationRef; this.#injector = injector; this.#windowRef = windowRef; @@ -58,7 +53,6 @@ export class SkyDynamicComponentService { /** * Creates an instance of the specified component and adds it to the specified location * on the page. - * @param options Options for creating the dynamic component. */ public createComponent( componentType: Type, @@ -73,9 +67,10 @@ export class SkyDynamicComponentService { parent: this.#injector, }); - const componentRef = this.#componentFactoryResolver - .resolveComponentFactory(componentType) - .create(injector); + const componentRef = createComponent(componentType, { + environmentInjector: this.#applicationRef.injector, + elementInjector: injector, + }); this.#applicationRef.attachView(componentRef.hostView); diff --git a/libs/components/core/src/lib/modules/overlay/overlay.component.ts b/libs/components/core/src/lib/modules/overlay/overlay.component.ts index 9caddfbe26..8a9739ad6a 100644 --- a/libs/components/core/src/lib/modules/overlay/overlay.component.ts +++ b/libs/components/core/src/lib/modules/overlay/overlay.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ComponentFactoryResolver, ComponentRef, ElementRef, EmbeddedViewRef, @@ -108,16 +107,12 @@ export class SkyOverlayComponent implements OnInit, OnDestroy { #ngUnsubscribe = new Subject(); - #resolver: ComponentFactoryResolver; - #router: Router | undefined; #routerSubscription: Subscription | undefined; - // TODO: Replace deprecated `ComponentFactoryResolver`. constructor( changeDetector: ChangeDetectorRef, - resolver: ComponentFactoryResolver, injector: Injector, coreAdapter: SkyCoreAdapterService, context: SkyOverlayContext, @@ -125,7 +120,6 @@ export class SkyOverlayComponent implements OnInit, OnDestroy { @Optional() router?: Router ) { this.#changeDetector = changeDetector; - this.#resolver = resolver; this.#injector = injector; this.#coreAdapter = coreAdapter; this.#context = context; @@ -176,17 +170,14 @@ export class SkyOverlayComponent implements OnInit, OnDestroy { this.targetRef.clear(); - const factory = this.#resolver.resolveComponentFactory(component); const injector = Injector.create({ providers, parent: this.#injector, }); - const componentRef = this.targetRef.createComponent( - factory, - undefined, - injector - ); + const componentRef = this.targetRef.createComponent(component, { + injector, + }); // Run an initial change detection cycle after the component has been created. componentRef.changeDetectorRef.detectChanges(); From ed1b5bba5a37f006bc25a09bd92f003501f848ea Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Mon, 17 Oct 2022 12:02:55 -0400 Subject: [PATCH 27/30] fix(sdk/testing): use default `axe-core` rules when running the `toBeAccessible` matcher (#681) --- .../toggle-switch/toggle-switch.component.spec.ts | 9 --------- libs/sdk/testing/src/lib/a11y/a11y-analyzer.ts | 7 ++----- 2 files changed, 2 insertions(+), 14 deletions(-) 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 25bb84a3c5..df8ecfc79d 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 @@ -172,15 +172,6 @@ describe('Toggle switch component', () => { fixture.detectChanges(); await expectAsync(fixture.nativeElement).toBeAccessible(); }); - - it('should fail accessibility with mismatched `ariaLabel` and label element content', async () => { - // https://dequeuniversity.com/rules/axe/html/3.5/label-content-name-mismatch?application=axeAP - testComponent.ariaLabel = 'My aria label'; - testComponent.buttonLabel = 'Text that does not match aria label'; - - fixture.detectChanges(); - await expectAsync(fixture.nativeElement).not.toBeAccessible(); - }); }); describe('with change event and no initial value', () => { diff --git a/libs/sdk/testing/src/lib/a11y/a11y-analyzer.ts b/libs/sdk/testing/src/lib/a11y/a11y-analyzer.ts index 51674fbab8..2ebdd07303 100644 --- a/libs/sdk/testing/src/lib/a11y/a11y-analyzer.ts +++ b/libs/sdk/testing/src/lib/a11y/a11y-analyzer.ts @@ -40,15 +40,12 @@ export abstract class SkyA11yAnalyzer { throw new Error('No element was specified for accessibility checking.'); } + SkyA11yAnalyzer.analyzer.reset(); + const defaults: SkyA11yAnalyzerConfig = { rules: {}, }; - // Enable all rules by default. - axe.getRules().forEach((rule) => { - defaults.rules[rule.ruleId] = { enabled: true }; - }); - // Disable autocomplete-valid // Chrome browsers ignore autocomplete="off", which forces us to use non-standard values // to disable the browser's native autofill. From c7c60f273c8bb988bcd7908282ba623723e861e0 Mon Sep 17 00:00:00 2001 From: Erika McVey <50454925+Blackbaud-ErikaMcVey@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:16:40 -0400 Subject: [PATCH 28/30] feat(components/modals): make `dynamicComponentService` required in `SkyModalService` constructor (#674) BREAKING CHANGE: `dynamicComponentService` is now a required parameter of `SkyModalService`. To address this change, provide the `dynamicComponentService` wherever you are constructing the `SkyModalService` or any mocks extending it for unit testing. --- .../src/lib/modules/error/error-modal.service.spec.ts | 5 +++-- .../modals/src/lib/modules/modal/modal.service.ts | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/components/errors/src/lib/modules/error/error-modal.service.spec.ts b/libs/components/errors/src/lib/modules/error/error-modal.service.spec.ts index a8f7d01dc2..0f75e7d447 100644 --- a/libs/components/errors/src/lib/modules/error/error-modal.service.spec.ts +++ b/libs/components/errors/src/lib/modules/error/error-modal.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { SkyLogService } from '@skyux/core'; +import { SkyDynamicComponentService, SkyLogService } from '@skyux/core'; import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; import { ErrorModalConfig } from './error-modal-config'; @@ -9,7 +9,8 @@ import { MockModalService } from './fixtures/mocks'; describe('Error modal service', () => { function createMockModalService() { - return new MockModalService(); + const dynamicComponentService = TestBed.inject(SkyDynamicComponentService); + return new MockModalService(dynamicComponentService); } it('should open with correct parameters (log service undefined)', () => { diff --git a/libs/components/modals/src/lib/modules/modal/modal.service.ts b/libs/components/modals/src/lib/modules/modal/modal.service.ts index 2743c48c30..476742d7a2 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.service.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.service.ts @@ -21,10 +21,8 @@ export class SkyModalService { #dynamicComponentService: SkyDynamicComponentService; - // TODO: Make `dynamicComponentService` required. It is optional today to maintain binary compatibility for consumers when they construct - // the service for unit testing. - constructor(dynamicComponentService?: SkyDynamicComponentService) { - this.#dynamicComponentService = dynamicComponentService!; + constructor(dynamicComponentService: SkyDynamicComponentService) { + this.#dynamicComponentService = dynamicComponentService; } /** From f2f2039c9da142d01c5b0f3444616209cb17a15c Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Mon, 17 Oct 2022 12:24:44 -0400 Subject: [PATCH 29/30] fix(components/forms): use a label instead of a button as the wrapper (#687) --- .vscode/settings.json | 2 +- apps/code-examples/src/app/app.component.html | 8 +++++++ .../selection-box-demo.component.html | 2 +- .../checkbox/selection-box-demo.component.ts | 20 +++++++++++----- .../src/app/features/forms.module.ts | 14 +++++++++++ .../selection-box-grid.component.spec.ts | 6 ++--- .../selection-box.component.html | 7 ++---- .../selection-box.component.spec.ts | 11 +++++---- .../selection-box/selection-box.component.ts | 23 +++++-------------- 9 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0eccb35b55..7013c4b21f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,5 @@ "editor.formatOnSave": true, "prettier.requireConfig": true, "coverage-gutters.showLineCoverage": true, - "cSpell.words": ["skyux", "blackbaud", "tabset", "tablist"] + "cSpell.words": ["blackbaud", "skyux", "tabindex", "tablist", "tabset"] } diff --git a/apps/code-examples/src/app/app.component.html b/apps/code-examples/src/app/app.component.html index e08e0ef42e..e820ea06a7 100644 --- a/apps/code-examples/src/app/app.component.html +++ b/apps/code-examples/src/app/app.component.html @@ -143,6 +143,14 @@
      • Input box
      • +
      • + Selection box (checkbox) +
      • +
      • + Selection box (radio) +
      diff --git a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.html b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.html index 0390eb6311..78e2d6d35f 100644 --- a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.html +++ b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.html @@ -1,7 +1,7 @@ diff --git a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.ts b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.ts index 75b8fa2d1b..05280159cb 100644 --- a/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/selection-box/checkbox/selection-box-demo.component.ts @@ -1,14 +1,19 @@ import { Component, OnInit } from '@angular/core'; -import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; @Component({ selector: 'app-selection-box-demo', templateUrl: './selection-box-demo.component.html', }) export class SelectionBoxDemoComponent implements OnInit { - public checkboxArray: FormArray | undefined; - - public selectionBoxes: any[] = [ + public checkboxControls: FormControl[] | undefined; + + public selectionBoxes: { + name: string; + icon: string; + description: string; + selected?: boolean; + }[] = [ { name: 'Save time and effort', icon: 'clock', @@ -37,9 +42,12 @@ export class SelectionBoxDemoComponent implements OnInit { } public ngOnInit(): void { - this.checkboxArray = this.#buildCheckboxes(); + const checkboxArray = this.#buildCheckboxes(); + + this.checkboxControls = checkboxArray.controls as FormControl[]; + this.myForm = this.#formBuilder.group({ - checkboxes: this.checkboxArray, + checkboxes: checkboxArray, }); this.myForm.valueChanges.subscribe((value) => console.log(value)); diff --git a/apps/code-examples/src/app/features/forms.module.ts b/apps/code-examples/src/app/features/forms.module.ts index 8e21e55d48..05712ded52 100644 --- a/apps/code-examples/src/app/features/forms.module.ts +++ b/apps/code-examples/src/app/features/forms.module.ts @@ -9,6 +9,10 @@ import { InputBoxDemoComponent } from '../code-examples/forms/input-box/basic/in import { InputBoxDemoModule } from '../code-examples/forms/input-box/basic/input-box-demo.module'; import { RadioDemoComponent as InlineHelpRadioDemoComponent } from '../code-examples/forms/radio/inline-help/radio-demo.component'; import { RadioDemoModule as InlineHelpRadioDemoModule } from '../code-examples/forms/radio/inline-help/radio-demo.module'; +import { SelectionBoxDemoComponent as SelectionBoxCheckboxDemoComponent } from '../code-examples/forms/selection-box/checkbox/selection-box-demo.component'; +import { SkySelectionBoxDemoModule as SelectionBoxCheckboxDemoModule } from '../code-examples/forms/selection-box/checkbox/selection-box-demo.module'; +import { SelectionBoxDemoComponent as SelectionBoxRadioDemoComponent } from '../code-examples/forms/selection-box/radio/selection-box-demo.component'; +import { SkySelectionBoxDemoModule as SelectionBoxRadioDemoModule } from '../code-examples/forms/selection-box/radio/selection-box-demo.module'; import { SingleFileAttachmentDemoComponent as BasicSingleFileAttachmentDemoComponent } from '../code-examples/forms/single-file-attachment/basic/single-file-attachment-demo.component'; import { SkySingleFileAttachmentDemoModule as BasicSkySingleFileAttachmentDemoModule } from '../code-examples/forms/single-file-attachment/basic/single-file-attachment-demo.module'; import { SingleFileAttachmentDemoComponent as InlineHelpSingleFileAttachmentDemoComponent } from '../code-examples/forms/single-file-attachment/inline-help/single-file-attachment-demo.component'; @@ -45,6 +49,14 @@ const routes: Routes = [ path: 'input-box/basic', component: InputBoxDemoComponent, }, + { + path: 'selection-box/checkbox', + component: SelectionBoxCheckboxDemoComponent, + }, + { + path: 'selection-box/radio', + component: SelectionBoxRadioDemoComponent, + }, ]; @NgModule({ @@ -63,6 +75,8 @@ export class FormsRoutingModule {} InlineHelpRadioDemoModule, InlineHelpToggleSwitchModule, InputBoxDemoModule, + SelectionBoxCheckboxDemoModule, + SelectionBoxRadioDemoModule, ], }) export class FormsModule {} diff --git a/libs/components/forms/src/lib/modules/selection-box/selection-box-grid.component.spec.ts b/libs/components/forms/src/lib/modules/selection-box/selection-box-grid.component.spec.ts index d43544c3e4..4063c7f0c0 100644 --- a/libs/components/forms/src/lib/modules/selection-box/selection-box-grid.component.spec.ts +++ b/libs/components/forms/src/lib/modules/selection-box/selection-box-grid.component.spec.ts @@ -29,7 +29,7 @@ describe('Selection box grid component', () => { // Wait for the next change detection cycle. This avoids having nested setTimeout() calls // and using the Jasmine done() function. - function waitForMutationObserver() { + function waitForMutationObserver(): Promise { return new Promise((resolve) => { setTimeout(() => resolve()); }); @@ -75,7 +75,7 @@ describe('Selection box grid component', () => { ); }); - it('should set proper CSS classess when alignItems is set to center', () => { + it('should set proper CSS classes when alignItems is set to center', () => { component.alignItems = 'center'; fixture.detectChanges(); @@ -87,7 +87,7 @@ describe('Selection box grid component', () => { ); }); - it('should set proper CSS classess when alignItems is set to left', () => { + it('should set proper CSS classes when alignItems is set to left', () => { component.alignItems = 'left'; fixture.detectChanges(); diff --git a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.html b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.html index ec15f9c2dc..23b86ed356 100644 --- a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.html +++ b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.html @@ -1,7 +1,5 @@ -
      @@ -50,4 +47,4 @@
      - + diff --git a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts index e3c5c9544f..3ae1cba327 100644 --- a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts +++ b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.spec.ts @@ -91,7 +91,9 @@ describe('Selection box component', () => { it('should enable and disable AfterViewInit', async () => { const outermostDiv = debugElement.query( - By.css('div#checkboxSelectionBoxes > form > sky-selection-box > div') + By.css( + 'div#checkboxSelectionBoxes > form > sky-selection-box > .sky-selection-box' + ) ).nativeElement; fixture.detectChanges(); @@ -218,10 +220,9 @@ describe('Selection box component', () => { expect(selectionBoxes[2]).not.toHaveCssClass('sky-selection-box-selected'); }); - it('should have a role of button', () => { - const role: string | null = - getRadioSelectionBoxes()[0]?.getAttribute('role'); - expect(role).toBe('button'); + it('should be wrapped in a label element', () => { + const tagName: string | null = getRadioSelectionBoxes()[0]?.tagName; + expect(tagName).toBe('LABEL'); }); it('should have a tabindex of 0', () => { diff --git a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.ts b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.ts index 5e9d3e8844..8ff70386dd 100644 --- a/libs/components/forms/src/lib/modules/selection-box/selection-box.component.ts +++ b/libs/components/forms/src/lib/modules/selection-box/selection-box.component.ts @@ -92,7 +92,12 @@ export class SkySelectionBoxComponent implements OnDestroy { this.disabled ? -1 : 0 ); - this.#selectionBoxAdapterService.setChildrenTabIndex(value, -1); + // Wait for child elements to render before overriding tabIndex values. + // TODO: This logic is brittle since the checkbox/radio can set its own tab index + // value at any time. We need a way to enforce the tab index for the entire lifespan of the component. + setTimeout(() => { + this.#selectionBoxAdapterService.setChildrenTabIndex(value, -1); + }); } } @@ -126,22 +131,6 @@ export class SkySelectionBoxComponent implements OnDestroy { this.#ngUnsubscribe.complete(); } - /** - * Since we are programatically firing a click on the control, - * make sure user is not clicking on the control before firing click logic. - */ - public onClick(event: any): void { - const isControlClick = - this.controlEl && - this.#selectionBoxAdapterService.isDescendant( - this.controlEl, - event.target - ); - if (!isControlClick) { - this.#selectControl(); - } - } - public onKeydown(event: KeyboardEvent): void { /* istanbul ignore else */ if (event.key === ' ') { From e38a2da26c644bff20423036e736943d8915efc4 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Mon, 17 Oct 2022 12:58:06 -0400 Subject: [PATCH 30/30] refactor(components/ag-grid): convert SCSS `@import` to `@use` (#693) --- .../cell-editor-autocomplete.component.scss | 3 --- .../cell-editor-datepicker.component.scss | 14 +++++------ .../src/lib/styles/ag-grid-styles.scss | 25 +++++++++---------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.scss b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.scss index a8544c86f5..e185dff270 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.scss +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.scss @@ -1,6 +1,3 @@ -@import 'libs/components/theme/src/lib/styles/mixins'; -@import 'libs/components/theme/src/lib/styles/_compat/mixins'; - sky-autocomplete { input { border: none; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.scss b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.scss index 39c5ce9661..8aa591d846 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.scss +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.scss @@ -1,6 +1,5 @@ -@import 'libs/components/theme/src/lib/styles/mixins'; -@import 'libs/components/theme/src/lib/styles/_compat/mixins'; -@import 'libs/components/theme/src/lib/styles/themes/modern/_compat/variables'; +@use 'libs/components/theme/src/lib/styles/mixins' as defaultMixins; +@use 'libs/components/theme/src/lib/styles/variables' as defaultVars; .sky-ag-grid-cell-editor-datepicker { position: relative; @@ -12,8 +11,8 @@ box-shadow: none; border-width: 1px; border-style: solid; - border-color: transparent $sky-border-color-neutral-medium transparent - transparent; + border-color: transparent defaultVars.$sky-border-color-neutral-medium + transparent transparent; } .sky-input-group-datepicker-btn { @@ -22,7 +21,7 @@ } } -@include sky-theme-modern { +@include defaultMixins.sky-theme-modern { .sky-ag-grid-cell-editor-datepicker { position: static; top: initial; @@ -31,7 +30,8 @@ sky-datepicker { input.sky-form-control { border-radius: 0; - border: 1px solid $sky-theme-modern-border-color-primary-dark; + border: 1px solid + defaultVars.$sky-theme-modern-border-color-primary-dark; &:focus { border-width: 2px !important; diff --git a/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss b/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss index 7ae2d4045f..80ab8f3ba5 100644 --- a/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss +++ b/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss @@ -1,9 +1,8 @@ -@import 'libs/components/theme/src/lib/styles/mixins'; -@import 'libs/components/theme/src/lib/styles/_compat/mixins'; -@import 'libs/components/theme/src/lib/styles/variables'; -@import 'libs/components/theme/src/lib/styles/_compat/variables'; -@import 'node_modules/ag-grid-community/src/styles/ag-grid'; -@import 'node_modules/ag-grid-community/src/styles/ag-theme-base/sass/ag-theme-base-mixin'; +@use 'libs/components/theme/src/lib/styles/mixins' as defaultMixins; +@use 'libs/components/theme/src/lib/styles/variables' as *; +@use 'node_modules/ag-grid-community/src/styles/ag-grid'; +@use 'node_modules/ag-grid-community/src/styles/ag-theme-base/sass/ag-theme-base-mixin' + as agGridBaseMixin; $sky-standard-border: 1px solid $sky-border-color-neutral-medium; $sky-standard-border-theme-modern: 1px solid @@ -46,7 +45,7 @@ $sky-cell-wrap-text-line-height-modern: $sky-theme-modern-font-paragraph-line-he } ag-grid-angular:not(.sky-ag-grid-editable) { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( background-color: $sky-color-gray-01, border-color: $sky-border-color-neutral-medium, @@ -107,7 +106,7 @@ ag-grid-angular:not(.sky-ag-grid-editable) { } ag-grid-angular.sky-ag-grid-editable { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( border-color: $sky-border-color-neutral-medium, borders: true, @@ -230,7 +229,7 @@ ag-grid-angular.sky-ag-grid-editable { .ag-cell-focus.ag-cell-inline-editing, .ag-cell-focus.ag-cell-popup-editing:not(.sky-ag-grid-cell-lookup) { height: ag-param(row-height) - 1; - @include sky-field-status(active); + @include defaultMixins.sky-field-status(active); } } } @@ -238,7 +237,7 @@ ag-grid-angular.sky-ag-grid-editable { // ag-grid only allows one theme include per root selector; adding this .sky-theme-modern:not(.the-editing-theme) ag-grid-angular:not(.sky-ag-grid-editable) { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( border-color: $sky-theme-modern-border-color-neutral-medium, borders: true, @@ -301,7 +300,7 @@ ag-grid-angular.sky-ag-grid-editable { } .sky-theme-modern ag-grid-angular.sky-ag-grid-editable { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( border-color: $sky-theme-modern-border-color-neutral-medium, borders: true, @@ -425,7 +424,7 @@ ag-grid-angular.sky-ag-grid-editable { // ag-grid only allows one theme include per root selector; adding this .sky-theme-modern.sky-theme-mode-dark:not(.the-editing-theme) ag-grid-angular:not(.sky-ag-grid-editable) { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( background-color: $sky-theme-modern-mode-dark-background-color-page-default, @@ -498,7 +497,7 @@ ag-grid-angular.sky-ag-grid-editable { } .sky-theme-modern.sky-theme-mode-dark ag-grid-angular.sky-ag-grid-editable { - @include ag-theme-base( + @include agGridBaseMixin.ag-theme-base( ( background-color: $sky-theme-modern-mode-dark-background-color-page-default,