diff --git a/src/index.ts b/src/index.ts index 83822debe0..555a8ce4d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import '@angular/localize/init'; + import { NgModule } from '@angular/core'; import { NgbAccordionModule } from './accordion/accordion.module'; diff --git a/src/typeahead/typeahead.spec.ts b/src/typeahead/typeahead.spec.ts index 53ceed9866..b25fca3a5a 100644 --- a/src/typeahead/typeahead.spec.ts +++ b/src/typeahead/typeahead.spec.ts @@ -8,7 +8,7 @@ import { debounceTime, filter, map } from 'rxjs/operators'; import { createGenericTestComponent, triggerEvent } from '../test/common'; import { expectResults, getWindowLinks } from '../test/typeahead/common'; -import { ARIA_LIVE_DELAY } from '../util/accessibility/live'; +import { ARIA_LIVE_DELAY, Live } from '../util/accessibility/live'; import { NgbTypeahead } from './typeahead'; import { NgbTypeaheadConfig } from './typeahead-config'; import { NgbHighlight } from './highlight'; @@ -63,9 +63,18 @@ function expectWindowResults(element, expectedResults: string[]) { } describe('ngb-typeahead', () => { + let mockLiveService: Partial; + beforeEach(() => { + mockLiveService = { + say: createSpy('say'), + }; + TestBed.configureTestingModule({ - providers: [{ provide: ARIA_LIVE_DELAY, useValue: null }], + providers: [ + { provide: ARIA_LIVE_DELAY, useValue: null }, + { provide: Live, useValue: mockLiveService }, + ], }); }); @@ -838,6 +847,41 @@ describe('ngb-typeahead', () => { expect(input.getAttribute('aria-owns')).toBeNull(); expect(input.getAttribute('aria-activedescendant')).toBeNull(); })); + + describe('live', () => { + let fixture: ComponentFixture; + let compiled: any; + + beforeEach(() => { + fixture = createTestComponent(``); + compiled = fixture.nativeElement; + }); + + it('should call the method with the correct message when there is more than one result', fakeAsync(() => { + tick(); + changeInput(compiled, 'o'); + fixture.detectChanges(); + + expectWindowResults(compiled, ['+one', 'one more']); + expect(mockLiveService.say).toHaveBeenCalledWith('2 results available'); + })); + + it('should call the method with the correct message when there is not any result available', fakeAsync(() => { + tick(); + changeInput(compiled, 'a'); + fixture.detectChanges(); + + expect(mockLiveService.say).toHaveBeenCalledWith('No results available'); + })); + + it('should call the method with the correct message when there is one single result available', fakeAsync(() => { + tick(); + changeInput(compiled, 'one more'); + fixture.detectChanges(); + + expect(mockLiveService.say).toHaveBeenCalledWith('1 result available'); + })); + }); }); describe('hint', () => { diff --git a/src/typeahead/typeahead.ts b/src/typeahead/typeahead.ts index 38fb23a4fb..ae08c597c8 100644 --- a/src/typeahead/typeahead.ts +++ b/src/typeahead/typeahead.ts @@ -399,6 +399,18 @@ export class NgbTypeahead implements ControlValueAccessor, OnInit, OnChanges, On this._nativeElement.value = toString(value); } + private _getAnnounceLocalizedMessage(count: number): string { + if (count === 0) { + return $localize`:@@ngb.typeahead.no-results:No results available`; + } + + if (count === 1) { + return $localize`:@@ngb.typeahead.one-result:1 result available`; + } + + return $localize`:@@ngb.typeahead.many-results:${count}:count: results available`; + } + private _subscribeToUserInput(): void { const results$ = this._valueChanges$.pipe( tap((value) => { @@ -445,7 +457,7 @@ export class NgbTypeahead implements ControlValueAccessor, OnInit, OnChanges, On // live announcer const count = results ? results.length : 0; - this._live.say(count === 0 ? 'No results available' : `${count} result${count === 1 ? '' : 's'} available`); + this._live.say(this._getAnnounceLocalizedMessage(count)); }); } diff --git a/src/util/accessibility/live.ts b/src/util/accessibility/live.ts index b1c7a222a7..10becdac47 100644 --- a/src/util/accessibility/live.ts +++ b/src/util/accessibility/live.ts @@ -19,6 +19,8 @@ function getLiveElement(document: any, container: HTMLElement, lazyCreate = fals element.setAttribute('aria-live', 'polite'); element.setAttribute('aria-atomic', 'true'); + element.classList.add('visually-hidden'); + container.appendChild(element); } @@ -34,7 +36,7 @@ export class Live implements OnDestroy { ngOnDestroy() { const element = getLiveElement(this._document, this._container); if (element) { - // if exists, it will always be attached to the + // if exists, it will always be attached to either the or the overriding container provided through the LIVE_CONTAINER DI token element.parentElement!.removeChild(element); } }