From 9283bbfb57fd187d4d5b1484255db28edff98d1b Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Mon, 3 Mar 2025 14:03:57 -0500 Subject: [PATCH] feat(components/lookup): country field displays hint text within the dropdown before a search is executed (#3179) [AB#3213325](https://dev.azure.com/blackbaud/f565481a-7bc9-4083-95d5-4f953da6d499/_workitems/edit/3213325) --- .../src/e2e/country-field.component.cy.ts | 22 - .../country-field.component.html | 5 +- .../country-field.component.stories.ts | 5 - .../country-field/country-field.component.ts | 3 - .../country-field.component.html | 2 +- .../src/assets/locales/resources_en_US.json | 4 + .../autocomplete-adapter.service.ts | 21 +- .../autocomplete/autocomplete.component.html | 10 +- .../autocomplete/autocomplete.component.scss | 18 + .../autocomplete.component.spec.ts | 85 ++++ .../autocomplete/autocomplete.component.ts | 15 +- .../autocomplete.component.fixture.html | 2 + .../autocomplete.component.fixture.ts | 1 + .../country-field.component.html | 20 +- .../country-field.component.spec.ts | 392 +++++++++--------- .../country-field/country-field.component.ts | 54 +-- ...try-field-input-box.component.fixture.html | 8 +- ...untry-field-input-box.component.fixture.ts | 4 + .../types/country-field-context.ts | 2 +- .../modules/country-field/types/country.ts | 3 +- .../shared/sky-lookup-resources.module.ts | 3 + .../phone-field/phone-field.component.html | 1 - .../phone-field/phone-field.component.spec.ts | 118 ------ .../phone-field/phone-field.component.ts | 2 +- 24 files changed, 373 insertions(+), 427 deletions(-) diff --git a/apps/e2e/lookup-storybook-e2e/src/e2e/country-field.component.cy.ts b/apps/e2e/lookup-storybook-e2e/src/e2e/country-field.component.cy.ts index 4474b3d11f..5a77772a08 100644 --- a/apps/e2e/lookup-storybook-e2e/src/e2e/country-field.component.cy.ts +++ b/apps/e2e/lookup-storybook-e2e/src/e2e/country-field.component.cy.ts @@ -3,28 +3,6 @@ import { E2eVariations } from '@skyux-sdk/e2e-schematics'; describe('lookup-storybook', () => { E2eVariations.forEachTheme((theme) => { describe(`in ${theme} theme`, () => { - describe(`in country field with phone info`, () => { - it(`should render the input dropdown`, () => { - cy.visit( - `/iframe.html?globals=theme:${theme}&id=countryfieldcomponent-countryfield--phone-info-country-field`, - ) - .get('#ready') - .should('exist') - .end(); - cy.get('app-country-field').should('exist').should('be.visible'); - cy.get('textarea').should('exist').should('be.visible').type('ba'); - - cy.get('app-country-field').screenshot( - `countryfieldcomponent-countryfield--country-field-dropdown-with-phone-info-${theme}`, - ); - cy.get('app-country-field').percySnapshot( - `countryfieldcomponent-countryfield--country-field-dropdown-with-phone-info-${theme}`, - { - widths: E2eVariations.DISPLAY_WIDTHS, - }, - ); - }); - }); ['empty', 'disabled', 'prepopulated', 'disabled-prepopulated'].forEach( (mode) => { describe(`in ${mode} country field`, () => { diff --git a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.html b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.html index 3a0c60a19d..d1da6c8435 100644 --- a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.html +++ b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.html @@ -1,9 +1,6 @@
- +
diff --git a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.stories.ts b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.stories.ts index 0961044619..eda0812ffe 100644 --- a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.stories.ts +++ b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.stories.ts @@ -19,11 +19,6 @@ type Story = StoryObj; export const EmptyCountryField: Story = {}; EmptyCountryField.args = {}; -export const PhoneInfoCountryField: Story = {}; -PhoneInfoCountryField.args = { - phoneInfoFlag: true, -}; - export const DisabledCountryField: Story = {}; DisabledCountryField.args = { disabledFlag: true, diff --git a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.ts b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.ts index 1e25824810..24c167da1d 100644 --- a/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.ts +++ b/apps/e2e/lookup-storybook/src/app/country-field/country-field.component.ts @@ -13,9 +13,6 @@ import { FontLoadingService } from '@skyux/storybook/font-loading'; standalone: false, }) export class CountryFieldComponent { - @Input() - public phoneInfoFlag = false; - @Input() public set prePopulatedFlag(value: boolean) { if (value) { diff --git a/apps/playground/src/app/components/lookup/country-field/country-field.component.html b/apps/playground/src/app/components/lookup/country-field/country-field.component.html index 652ebb5707..eb0aae5e64 100644 --- a/apps/playground/src/app/components/lookup/country-field/country-field.component.html +++ b/apps/playground/src/app/components/lookup/country-field/country-field.component.html @@ -65,9 +65,9 @@

Input box

- { + const parentElement = isInputBox + ? elementRef.nativeElement.closest('.sky-input-box') + : elementRef.nativeElement; + if (parentElement) { + const width = parentElement.getBoundingClientRect().width; + this.#renderer.setStyle( + dropdownRef.nativeElement, + 'width', + `${width}px`, + ); + } + }); } /** 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 556523967a..9d42f982f8 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.html @@ -4,11 +4,13 @@ class="sky-autocomplete" role="combobox" [attr.aria-expanded]=" - isOpen && (showActionsArea || (isResultsVisible | async)) + isOpen && + (showActionsArea || dropdownHintText || (isResultsVisible | async)) " [attr.aria-labelledby]="ariaLabelledBy" [attr.aria-controls]=" - isOpen && (showActionsArea || (isResultsVisible | async)) + isOpen && + (showActionsArea || dropdownHintText || (isResultsVisible | async)) ? resultsListId : null " @@ -76,6 +78,10 @@ } + } @else if (dropdownHintText) { +
+ {{ dropdownHintText }} +
} @if (showActionsArea) {
diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.scss b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.scss index 8454121ba4..ea09e0106b 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.scss +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.scss @@ -10,6 +10,8 @@ --sky-override-autocomplete-actions-no-results-padding: 7px 13px; --sky-override-autocomplete-actions-top-border: 1px solid #{$sky-border-color-neutral-medium}; + --sky-override-autocomplete-dropdown-hint-text-line-height: normal; + --sky-override-autocomplete-dropdown-hint-text-padding: 7px 13px; --sky-override-autocomplete-no-results-font-size: 15px; --sky-override-autocomplete-no-results-padding: #{$sky-padding-squish-default}; --sky-override-autocomplete-result-background-color: transparent; @@ -33,6 +35,8 @@ --sky-override-autocomplete-actions-no-results-padding: 7px 13px; --sky-override-autocomplete-actions-top-border: 1px solid #{$sky-border-color-neutral-medium}; + --sky-override-autocomplete-dropdown-hint-text-line-height: normal; + --sky-override-autocomplete-dropdown-hint-text-padding: 7px 13px; --sky-override-autocomplete-no-results-font-size: 15px; --sky-override-autocomplete-no-results-padding: #{$sky-padding-squish-default}; --sky-override-autocomplete-result-margin-bottom: 0; @@ -144,6 +148,20 @@ } } +.sky-autocomplete-dropdown-hint-text { + display: inline-block; + padding: var( + --sky-override-autocomplete-dropdown-hint-text-padding, + var(--sky-space-inset-pillarbox-1_2-top-m) 0 + var(--sky-space-inset-pillarbox-1_2-bottom-m) + var(--sky-space-inset-pillarbox-1_2-left-m) + ); + line-height: var( + --sky-override-autocomplete-dropdown-hint-text-line-height, + var(--sky-font-line_height-body-m) + ); +} + .sky-autocomplete-action-add { float: right; diff --git a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts index f8ed1f2059..183bbc1c9e 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.spec.ts @@ -43,6 +43,14 @@ describe('Autocomplete component', () => { return document.querySelector('sky-autocomplete') as HTMLElement; } + function getDisplayedHintText(): string { + return ( + document + .querySelector('.sky-autocomplete-dropdown-hint-text') + ?.textContent.trim() || '' + ); + } + function getInputElement(async = false): HTMLInputElement { if (async) { return document.getElementById( @@ -452,6 +460,83 @@ describe('Autocomplete component', () => { expect(actionsContainer).toBeNull(); })); + it('should show a dropdown hint message', fakeAsync(() => { + const expectedMessage = 'Type to search for a person'; + component.dropdownHintText = expectedMessage; + fixture.detectChanges(); + + const inputElement = getInputElement(); + + SkyAppTestUtility.fireDomEvent(inputElement, 'focus'); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const hintText = getDisplayedHintText(); + expect(hintText).toBe(expectedMessage); + + const actionsContainer = getActionsContainer(); + expect(actionsContainer).toBeNull(); + })); + + it('should not show a dropdown hint message when no results are found', fakeAsync(() => { + const expectedMessage = 'Type to search for a person'; + component.dropdownHintText = expectedMessage; + fixture.detectChanges(); + + const inputElement = getInputElement(); + + SkyAppTestUtility.fireDomEvent(inputElement, 'focus'); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + let hintText = getDisplayedHintText(); + expect(hintText).toBe(expectedMessage); + + let actionsContainer = getActionsContainer(); + expect(actionsContainer).toBeNull(); + + enterSearch('abcdefgh', fixture); + + const container = getSearchResultsSection(); + expect(container?.textContent?.trim()).toBe('No matches found'); + + actionsContainer = getActionsContainer(); + expect(actionsContainer).toBeNull(); + + hintText = getDisplayedHintText(); + expect(hintText).toBe(''); + })); + + it('should not show a dropdown hint message when results are found', fakeAsync(() => { + const expectedMessage = 'Type to search for a person'; + component.dropdownHintText = expectedMessage; + fixture.detectChanges(); + + const inputElement = getInputElement(); + + SkyAppTestUtility.fireDomEvent(inputElement, 'focus'); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + let hintText = getDisplayedHintText(); + expect(hintText).toBe(expectedMessage); + + const actionsContainer = getActionsContainer(); + expect(actionsContainer).toBeNull(); + + const spy = spyOn(autocomplete, 'searchOrDefault').and.callThrough(); + + enterSearch('r', fixture); + + expect(spy.calls.argsFor(0)[0]).toEqual('r'); + + hintText = getDisplayedHintText(); + expect(hintText).toBe(''); + })); + it('should show a no results found message in the actions area if the add button is shown', fakeAsync(() => { component.showAddButton = true; // NOTE: The "New" is from the "New" button 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 901d76da0f..2afa28bf64 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts +++ b/libs/components/lookup/src/lib/modules/autocomplete/autocomplete.component.ts @@ -133,6 +133,13 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { return this.#_descriptorProperty; } + /** + * Hint text to show in the dropdown + * @internal + */ + @Input() + public dropdownHintText: string | undefined; + /** * @internal * Whether to display a button in the dropdown that opens a picker where users can view all options. @@ -415,7 +422,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { .pipe(takeUntil(this.#inputDirectiveUnsubscribe)) .subscribe(() => { this.#hasFocus = true; - if (this.showActionsArea) { + if (this.showActionsArea || this.dropdownHintText) { this.#openDropdown(); } }); @@ -598,7 +605,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { if (targetIsSearchResult) { this.#selectSearchResultById(activeElementId); - if (!this.showActionsArea) { + if (!this.showActionsArea && !this.dropdownHintText) { this.#closeDropdown(); } else { this.#resetSearch(); @@ -701,7 +708,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { public onResultClick(id: string, event: MouseEvent): void { this.#selectSearchResultById(id); - if (!this.showActionsArea) { + if (!this.showActionsArea && !this.dropdownHintText) { this.#closeDropdown(); } else { this.#resetSearch(); @@ -740,7 +747,7 @@ export class SkyAutocompleteComponent implements OnDestroy, AfterViewInit { }); } - if (!this.showActionsArea) { + if (!this.showActionsArea && !this.dropdownHintText) { this.#closeDropdown(); } else { this.#resetSearch(); diff --git a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.html b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.html index 5aca9e25e4..d87c558150 100644 --- a/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.html +++ b/libs/components/lookup/src/lib/modules/autocomplete/fixtures/autocomplete.component.fixture.html @@ -11,6 +11,7 @@ [data]="data" [debounceTime]="debounceTime" [descriptorProperty]="descriptorProperty" + [dropdownHintText]="dropdownHintText" [enableShowMore]="enableShowMore" [messageStream]="messageStream" [propertiesToSearch]="propertiesToSearch" @@ -49,6 +50,7 @@ [data]="data" [debounceTime]="debounceTime" [descriptorProperty]="descriptorProperty" + [dropdownHintText]="dropdownHintText" [enableShowMore]="enableShowMore" [messageStream]="messageStream" [propertiesToSearch]="propertiesToSearch" 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 c10bef0da7..d59ccf136c 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 @@ -48,6 +48,7 @@ export class SkyAutocompleteFixtureComponent { public debounceTime: number | undefined; public descriptorProperty: string | undefined; public disabled: boolean | undefined = false; + public dropdownHintText: string | undefined; public enableShowMore: boolean | undefined = false; public hideInput = false; public propertiesToSearch: string[] | undefined; diff --git a/libs/components/lookup/src/lib/modules/country-field/country-field.component.html b/libs/components/lookup/src/lib/modules/country-field/country-field.component.html index 675ffef59a..7c2b67015d 100644 --- a/libs/components/lookup/src/lib/modules/country-field/country-field.component.html +++ b/libs/components/lookup/src/lib/modules/country-field/country-field.component.html @@ -10,28 +10,34 @@ }" >
+
@@ -45,7 +51,7 @@
{{ item.name }} - @if (includePhoneInfo) { + @if (context?.inPhoneField) { 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 4d3b3b09f9..d6b7a3d957 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 @@ -76,6 +76,14 @@ describe('Country Field Component', () => { return document.querySelector('.sky-autocomplete-results') as HTMLElement; } + function getDisplayedHintText(): string { + return ( + document + .querySelector('.sky-autocomplete-dropdown-hint-text') + ?.textContent.trim() || '' + ); + } + function getInputElement(): HTMLTextAreaElement { return document.querySelector('textarea') as HTMLTextAreaElement; } @@ -190,6 +198,18 @@ describe('Country Field Component', () => { validateSelectedCountry(nativeElement, ''); })); + + it('should show hint text in dropdown before searching', fakeAsync(() => { + fixture.detectChanges(); + + SkyAppTestUtility.fireDomEvent(getInputElement(), 'focus'); + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + expect(getDisplayedHintText()).toBe('Type to search for a country'); + })); }); describe('usage', () => { @@ -473,80 +493,6 @@ describe('Country Field Component', () => { fixture.detectChanges(); expect(changeEventSpy).not.toHaveBeenCalled(); })); - - it('should not include dial code information when the `includePhoneInfo` input is not set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'countryChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = false; - component.modelValue = { - name: 'United States', - iso2: 'us', - }; - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - expect(changeEventSpy).not.toHaveBeenCalled(); - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - }); - - let searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-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-font-deemphasized'), - ).toBeNull(); - })); - - it('should include dial code information when the `includePhoneInfo` input is set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'countryChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = true; - component.modelValue = { - name: 'United States', - iso2: 'us', - }; - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - expect(changeEventSpy).not.toHaveBeenCalled(); - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - dialCode: '61', - priority: 0, - }); - - const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-deemphasized'), - ).toHaveText('61'); - })); }); describe('validation', () => { @@ -653,6 +599,9 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); it('should be accessible (populated)', async () => { @@ -664,6 +613,9 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); }); }); @@ -733,6 +685,18 @@ describe('Country Field Component', () => { expect(nativeElement.querySelector('textarea')?.value).toBe(''); })); + + it('should show hint text in dropdown before searching', fakeAsync(() => { + fixture.detectChanges(); + + SkyAppTestUtility.fireDomEvent(getInputElement(), 'focus'); + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + expect(getDisplayedHintText()).toBe('Type to search for a country'); + })); }); describe('usage', () => { @@ -1092,64 +1056,6 @@ describe('Country Field Component', () => { iso2: 'au', }); })); - - it('should not include dial code information when the `includePhoneInfo` input is not set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'formValueChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = false; - component.initialValue = { - name: 'United States', - iso2: 'us', - }; - fixture.detectChanges(); - tick(); - - expect(changeEventSpy).not.toHaveBeenCalled(); - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - }); - - const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-deemphasized'), - ).toBeNull(); - })); - - it('should include dial code information when the `includePhoneInfo` input is set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'formValueChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = true; - component.initialValue = { - name: 'United States', - iso2: 'us', - }; - fixture.detectChanges(); - tick(); - - expect(changeEventSpy).not.toHaveBeenCalled(); - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - dialCode: '61', - priority: 0, - }); - - const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-deemphasized'), - ).toHaveText('61'); - })); }); describe('validation', () => { @@ -1256,6 +1162,9 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); it('should be accessible (populated)', async () => { @@ -1267,6 +1176,9 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); }); }); @@ -1420,56 +1332,6 @@ describe('Country Field Component', () => { iso2: 'au', }); })); - - it('should not include dial code information when the `includePhoneInfo` input is not set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'countryChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = false; - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - }); - - const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-deemphasized'), - ).toBeNull(); - })); - - it('should include dial code information when the `includePhoneInfo` input is set', fakeAsync(() => { - const changeEventSpy = spyOn( - component, - 'countryChanged', - ).and.callThrough(); - component.countryFieldComponent.includePhoneInfo = true; - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - searchAndSelect('Austr', 0, fixture); - fixture.detectChanges(); - tick(); - expect(changeEventSpy).toHaveBeenCalledWith({ - name: 'Australia', - iso2: 'au', - dialCode: '61', - priority: 0, - }); - - const searchResults = searchAndGetResults('Austr', fixture); - expect( - searchResults[0].querySelector('.sky-font-deemphasized'), - ).toHaveText('61'); - })); }); describe('a11y', () => { @@ -1486,6 +1348,9 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); it('should be accessible (populated)', async () => { @@ -1516,14 +1381,26 @@ describe('Country Field Component', () => { await fixture.whenStable(); fixture.detectChanges(); await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); }); }); }); describe('inside input box', () => { let fixture: ComponentFixture; + let component: CountryFieldInputBoxTestComponent; let nativeElement: HTMLElement; + const axeConfig = { + rules: { + region: { + enabled: false, + }, + }, + }; + //#region helpers function setModernTheme(): void { const modernTheme = new SkyThemeSettings( @@ -1554,6 +1431,7 @@ describe('Country Field Component', () => { }); fixture = TestBed.createComponent(CountryFieldInputBoxTestComponent); + component = fixture.componentInstance; nativeElement = fixture.nativeElement as HTMLElement; }); @@ -1588,19 +1466,28 @@ describe('Country Field Component', () => { expect(inputBoxInsetIcon).not.toBeNull(); })); - it('should remove placeholder in modern theme', fakeAsync(() => { + it('should not include dial code information', fakeAsync(() => { + const changeEventSpy = spyOn( + component, + 'countryChanged', + ).and.callThrough(); fixture.detectChanges(); tick(); - const input = nativeElement.querySelector('.sky-form-control'); - expect(input?.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); + expect(changeEventSpy).not.toHaveBeenCalled(); - setModernTheme(); + searchAndSelect('Austr', 0, fixture); + fixture.detectChanges(); + tick(); + expect(changeEventSpy).toHaveBeenCalledWith({ + name: 'Australia', + iso2: 'au', + }); - const modernInput = nativeElement.querySelector('.sky-form-control'); - expect(modernInput?.getAttribute('placeholder')).toEqual(''); + const searchResults = searchAndGetResults('Austr', fixture); + expect( + searchResults[0].querySelector('.sky-font-deemphasized'), + ).toBeNull(); })); it('should set aria-describedby when hint text is specified', () => { @@ -1624,7 +1511,7 @@ describe('Country Field Component', () => { }); }); - describe('with country field context', () => { + describe('with country field context setting in a phone field', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [CountryFieldInputBoxTestComponent], @@ -1635,31 +1522,134 @@ describe('Country Field Component', () => { }, { provide: SKY_COUNTRY_FIELD_CONTEXT, - useValue: { showPlaceholderText: true }, + useValue: { inPhoneField: true }, }, ], }); fixture = TestBed.createComponent(CountryFieldInputBoxTestComponent); + component = fixture.componentInstance; nativeElement = fixture.nativeElement as HTMLElement; }); - it('should include placeholder in modern theme when the country field context calls for it', fakeAsync(() => { + it('should include dial code information ', fakeAsync(() => { + const changeEventSpy = spyOn( + component, + 'countryChanged', + ).and.callThrough(); fixture.detectChanges(); tick(); - const input = nativeElement.querySelector('.sky-form-control'); - expect(input?.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); + expect(changeEventSpy).not.toHaveBeenCalled(); + searchAndSelect('Austr', 0, fixture); + fixture.detectChanges(); + tick(); + expect(changeEventSpy).toHaveBeenCalledWith({ + name: 'Australia', + iso2: 'au', + dialCode: '61', + priority: 0, + }); - setModernTheme(); + const searchResults = searchAndGetResults('Austr', fixture); + expect( + searchResults[0].querySelector('.sky-font-deemphasized'), + ).toHaveText('61'); + })); + + it('should be accessible (empty)', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', + ); + }); - const modernInput = nativeElement.querySelector('.sky-form-control'); - expect(modernInput?.getAttribute('placeholder')).toEqual( - 'Search for a country', + it('should be accessible (populated)', async () => { + component.modelValue = { + name: 'United States', + iso2: 'us', + }; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBe( + 'Type to search for a country', ); + }); + }); + + describe('with country field context setting not in a phone field', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CountryFieldInputBoxTestComponent], + providers: [ + { + provide: SkyThemeService, + useValue: mockThemeSvc, + }, + { + provide: SKY_COUNTRY_FIELD_CONTEXT, + useValue: { inPhoneField: false }, + }, + ], + }); + + fixture = TestBed.createComponent(CountryFieldInputBoxTestComponent); + component = fixture.componentInstance; + nativeElement = fixture.nativeElement as HTMLElement; + }); + + it('should not include dial code information', fakeAsync(() => { + const changeEventSpy = spyOn( + component, + 'countryChanged', + ).and.callThrough(); + fixture.detectChanges(); + tick(); + + expect(changeEventSpy).not.toHaveBeenCalled(); + + searchAndSelect('Austr', 0, fixture); + fixture.detectChanges(); + tick(); + expect(changeEventSpy).toHaveBeenCalledWith({ + name: 'Australia', + iso2: 'au', + }); + + const searchResults = searchAndGetResults('Austr', fixture); + expect( + searchResults[0].querySelector('.sky-font-deemphasized'), + ).toBeNull(); })); + + it('should be accessible (empty)', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBeNull(); + }); + + it('should be accessible (populated)', async () => { + component.modelValue = { + name: 'United States', + iso2: 'us', + }; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await expectAsync(document.body).toBeAccessible(axeConfig); + expect(getInputElement().getAttribute('aria-label')).toBeNull(); + }); }); }); }); 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 334296e852..4a3f88a84f 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 @@ -3,7 +3,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Injector, Input, @@ -122,22 +121,6 @@ export class SkyCountryFieldComponent return this.#_disabled; } - /** - * Whether to include phone information in the selected country and country dropdown. - * @default false - * @internal - */ - @Input() - public set includePhoneInfo(value: boolean | undefined) { - this.#_includePhoneInfo = value ?? false; - - this.#setupCountries(); - } - - public get includePhoneInfo(): boolean { - return this.#_includePhoneInfo; - } - /** * The [International Organization for Standardization Alpha 2](https://www.nationsonline.org/oneworld/country_code_list.htm) * country codes for the countries that users can select. By default, all countries are available. @@ -177,7 +160,7 @@ export class SkyCountryFieldComponent public set selectedCountry(newCountry: SkyCountryFieldCountry | undefined) { if (!this.#countriesAreEqual(this.selectedCountry, newCountry)) { - const newCountryIso = newCountry && newCountry.iso2; + const newCountryIso = newCountry?.iso2; if (newCountryIso) { const isoCountry = this.countries.find( (country) => country.iso2 === newCountryIso, @@ -200,26 +183,21 @@ export class SkyCountryFieldComponent this.onTouched(); this.selectedCountryChange.emit(newCountry); - } - - // Do not mark the field as "dirty" - // if the field has been initialized with a value. - if (this.#isInitialChange && this.#ngControl && this.#ngControl.control) { + } else if (this.#ngControl?.control) { + // Do not mark the field as "dirty" + // if the field has been initialized with a value. this.#ngControl.control.markAsPristine(); } this.#isInitialChange = false; /** - * The second portion of this if statement is complex. The control type check ensures that + * The if statement is complex. The control type check ensures that * we only watch for the initial time through this function on reactive forms. However, * template forms will send through `null` and then `undefined` on empty initialization * so we have to check for when the non-null pass through happens. */ - } else if ( - this.#isInitialChange && - (!(this.#ngControl instanceof NgModel) || newCountry !== null) - ) { + } else if (!(this.#ngControl instanceof NgModel) || newCountry !== null) { this.#isInitialChange = false; } } @@ -256,8 +234,6 @@ export class SkyCountryFieldComponent #defaultCountryData: SkyCountryFieldCountry | undefined; - #elRef: ElementRef; - #injector: Injector; #internalFormChange = false; @@ -274,21 +250,17 @@ export class SkyCountryFieldComponent #_disabled = false; - #_includePhoneInfo = false; - #_selectedCountry: SkyCountryFieldCountry | undefined; #_supportedCountryISOs: string[] | undefined; constructor( changeDetector: ChangeDetectorRef, - elRef: ElementRef, injector: Injector, @Optional() public inputBoxHostSvc?: SkyInputBoxHostService, @Optional() themeSvc?: SkyThemeService, ) { this.#changeDetector = changeDetector; - this.#elRef = elRef; this.#injector = injector; this.#themeSvc = themeSvc; @@ -447,20 +419,8 @@ export class SkyCountryFieldComponent #setupCountries(): void { this.countries = cloneCountryData(intlTelInput.getCountryData()); - // Ignoring coverage here as this will be removed in the next release. - // istanbul ignore next - if (!this.includePhoneInfo) { - if ( - ( - this.#elRef.nativeElement.parentElement as HTMLElement - )?.classList?.contains('sky-phone-field-country-search') - ) { - this.includePhoneInfo = true; - } - } - /* istanbul ignore else */ - if (!this.includePhoneInfo) { + if (!this.context?.inPhoneField) { /** * The library we get the country data from includes extra phone properties. * We want to remove these unless we are in a phone field diff --git a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.html b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.html index 28973b6eb0..ab9fc512d0 100644 --- a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.html +++ b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.html @@ -1,7 +1,11 @@
- - + +
diff --git a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.ts b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.ts index 6d0fa32eb7..427dc14392 100644 --- a/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.ts +++ b/libs/components/lookup/src/lib/modules/country-field/fixtures/country-field-input-box.component.fixture.ts @@ -13,4 +13,8 @@ import { SkyCountryFieldCountry } from '../types/country'; export class CountryFieldInputBoxTestComponent { public hintText: string | undefined; public modelValue: SkyCountryFieldCountry | undefined; + + public countryChanged(country: SkyCountryFieldCountry): void { + return; + } } diff --git a/libs/components/lookup/src/lib/modules/country-field/types/country-field-context.ts b/libs/components/lookup/src/lib/modules/country-field/types/country-field-context.ts index 516de3bdcd..64dc85bbc0 100644 --- a/libs/components/lookup/src/lib/modules/country-field/types/country-field-context.ts +++ b/libs/components/lookup/src/lib/modules/country-field/types/country-field-context.ts @@ -2,5 +2,5 @@ * @internal */ export interface SkyCountryFieldContext { - showPlaceholderText: boolean; + inPhoneField: boolean; } diff --git a/libs/components/lookup/src/lib/modules/country-field/types/country.ts b/libs/components/lookup/src/lib/modules/country-field/types/country.ts index f62c73bd39..8de639a4da 100644 --- a/libs/components/lookup/src/lib/modules/country-field/types/country.ts +++ b/libs/components/lookup/src/lib/modules/country-field/types/country.ts @@ -16,7 +16,8 @@ export interface SkyCountryFieldCountry { /** * The country's international dial code. - * This property will only be set if the `includePhoneInfo` input is set. + * This property is only set when the country field is within a phone field. + * @internal */ dialCode?: string; diff --git a/libs/components/lookup/src/lib/modules/shared/sky-lookup-resources.module.ts b/libs/components/lookup/src/lib/modules/shared/sky-lookup-resources.module.ts index 7a69626139..c64b77d0c7 100644 --- a/libs/components/lookup/src/lib/modules/shared/sky-lookup-resources.module.ts +++ b/libs/components/lookup/src/lib/modules/shared/sky-lookup-resources.module.ts @@ -21,6 +21,9 @@ const RESOURCES: Record = { skyux_autocomplete_show_all: { message: 'Show all' }, skyux_autocomplete_show_all_count: { message: 'Show all {0}' }, skyux_autocomplete_show_matches_count: { message: 'Show matches ({0})' }, + skyux_country_field_dropdown_hint_text: { + message: 'Type to search for a country', + }, skyux_country_field_search_placeholder: { message: 'Search for a country' }, skyux_lookup_search_button_show_more: { message: 'Show all search results', diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html index 76e42ea90c..e067e40c10 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.html @@ -69,7 +69,6 @@ diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts index 1c697ef37a..025325edaf 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.spec.ts @@ -8,23 +8,12 @@ import { NgModel, UntypedFormControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing'; -import { - SkyTheme, - SkyThemeMode, - SkyThemeService, - SkyThemeSettings, - SkyThemeSettingsChange, -} from '@skyux/theme'; - -import { BehaviorSubject } from 'rxjs'; import { PhoneFieldInputBoxTestComponent } from './fixtures/phone-field-input-box.component.fixture'; import { PhoneFieldReactiveTestComponent } from './fixtures/phone-field-reactive.component.fixture'; import { PhoneFieldTestComponent } from './fixtures/phone-field.component.fixture'; describe('Phone Field Component', () => { - let mockThemeSvc: Partial; - function checkCountrySearchToggleButtonFlag( iso: string, fixture: ComponentFixture< @@ -240,27 +229,6 @@ describe('Phone Field Component', () => { expect(model?.touched).toBe(isTouched); } - - function setModernTheme( - fixture: ComponentFixture< - | PhoneFieldTestComponent - | PhoneFieldReactiveTestComponent - | PhoneFieldInputBoxTestComponent - >, - ): void { - const modernTheme = new SkyThemeSettings( - SkyTheme.presets.modern, - SkyThemeMode.presets.light, - ); - ( - mockThemeSvc.settingsChange as BehaviorSubject - ).next({ - currentSettings: modernTheme, - previousSettings: undefined, - }); - fixture.detectChanges(); - tick(); - } // #endregion describe('template form', () => { @@ -269,24 +237,8 @@ describe('Phone Field Component', () => { let nativeElement: HTMLElement; beforeEach(() => { - mockThemeSvc = { - settingsChange: new BehaviorSubject({ - currentSettings: new SkyThemeSettings( - SkyTheme.presets.default, - SkyThemeMode.presets.light, - ), - previousSettings: undefined, - }), - }; - TestBed.configureTestingModule({ imports: [PhoneFieldTestComponent, NoopAnimationsModule], - providers: [ - { - provide: SkyThemeService, - useValue: mockThemeSvc, - }, - ], }); fixture = TestBed.createComponent(PhoneFieldTestComponent); @@ -770,25 +722,6 @@ describe('Phone Field Component', () => { ).toBeTruthy(); })); - it('should show placeholder text in both themes', fakeAsync(() => { - fixture.detectChanges(); - const countryInput = getCountrySearchToggleButton(fixture); - countryInput.click(); - detectChangesAndTick(fixture); - - const inputEl = getCountrySearchInput(fixture); - - expect(inputEl.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); - - setModernTheme(fixture); - - expect(inputEl.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); - })); - it('should be accessible when country search is shown', async () => { fixture.detectChanges(); const countryInput = getCountrySearchToggleButton(fixture); @@ -1364,24 +1297,8 @@ describe('Phone Field Component', () => { let nativeElement: HTMLElement; beforeEach(() => { - mockThemeSvc = { - settingsChange: new BehaviorSubject({ - currentSettings: new SkyThemeSettings( - SkyTheme.presets.default, - SkyThemeMode.presets.light, - ), - previousSettings: undefined, - }), - }; - TestBed.configureTestingModule({ imports: [PhoneFieldReactiveTestComponent, NoopAnimationsModule], - providers: [ - { - provide: SkyThemeService, - useValue: mockThemeSvc, - }, - ], }); fixture = TestBed.createComponent(PhoneFieldReactiveTestComponent); @@ -1920,25 +1837,6 @@ describe('Phone Field Component', () => { expect(document.activeElement === phoneInput).toBeTruthy(); })); - it('should show placeholder text in both themes', fakeAsync(() => { - fixture.detectChanges(); - const countryInput = getCountrySearchToggleButton(fixture); - countryInput.click(); - detectChangesAndTick(fixture); - - const inputEl = getCountrySearchInput(fixture); - - expect(inputEl.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); - - setModernTheme(fixture); - - expect(inputEl.getAttribute('placeholder')).toEqual( - 'Search for a country', - ); - })); - it('should be accessible when country search is shown', async () => { fixture.detectChanges(); const countryInput = getCountrySearchToggleButton(fixture); @@ -2267,24 +2165,8 @@ describe('Phone Field Component', () => { let nativeElement: HTMLElement; beforeEach(() => { - mockThemeSvc = { - settingsChange: new BehaviorSubject({ - currentSettings: new SkyThemeSettings( - SkyTheme.presets.default, - SkyThemeMode.presets.light, - ), - previousSettings: undefined, - }), - }; - TestBed.configureTestingModule({ imports: [PhoneFieldInputBoxTestComponent, NoopAnimationsModule], - providers: [ - { - provide: SkyThemeService, - useValue: mockThemeSvc, - }, - ], }); fixture = TestBed.createComponent(PhoneFieldInputBoxTestComponent); diff --git a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts index 8d17e048ae..138e95b372 100644 --- a/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts +++ b/libs/components/phone-field/src/lib/modules/phone-field/phone-field.component.ts @@ -84,7 +84,7 @@ const DEFAULT_COUNTRY_CODE = 'us'; }, { provide: SKY_COUNTRY_FIELD_CONTEXT, - useValue: { showPlaceholderText: true }, + useValue: { inPhoneField: true }, }, ], animations: [