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..4337397250 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 }, + ], }); }); @@ -312,7 +321,7 @@ describe('ngb-typeahead', () => { it('should use provided result formatter function', () => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; @@ -403,7 +412,7 @@ describe('ngb-typeahead', () => { it('should not select the result on TAB, close window and not write to the input when focusFirst is false', () => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; @@ -432,7 +441,7 @@ describe('ngb-typeahead', () => { it('should apply additional class when specified', () => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; @@ -446,7 +455,7 @@ describe('ngb-typeahead', () => { it('should apply additional classes when specified', () => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; @@ -723,7 +732,7 @@ describe('ngb-typeahead', () => { it('should not propagate model when preventDefault() is called on selectEvent', () => { const fixture = createTestComponent( - '', + '' ); // clicking selected @@ -752,7 +761,7 @@ describe('ngb-typeahead', () => { const fixture = createTestComponent( `@if (show) { - }`, + }` ); changeInput(fixture.nativeElement, 'one'); @@ -781,7 +790,7 @@ describe('ngb-typeahead', () => { it('should have configurable autocomplete attribute', () => { const fixture = createTestComponent( - '', + '' ); const input = getNativeInput(fixture.nativeElement); @@ -838,12 +847,47 @@ 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', () => { it('should show hint when an item starts with user input', fakeAsync(() => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; const inputEl = getNativeInput(compiled); @@ -867,7 +911,7 @@ describe('ngb-typeahead', () => { it('should show hint with no selection when an item does not starts with user input', fakeAsync(() => { const fixture = createTestComponent( - ``, + `` ); const compiled = fixture.nativeElement; const inputEl = getNativeInput(compiled); @@ -912,7 +956,7 @@ describe('ngb-typeahead', () => { it('should not show hint when there is no result selected', fakeAsync(() => { const fixture = createTestComponent( - ``, + `` ); fixture.detectChanges(); const compiled = fixture.nativeElement; @@ -999,7 +1043,7 @@ describe('ngb-typeahead', () => { let inputEl; beforeEach(() => { fixture = createTestComponent( - ``, + `` ); compiled = fixture.nativeElement; inputEl = getNativeInput(compiled); @@ -1033,7 +1077,7 @@ describe('ngb-typeahead', () => { [ngbTypeahead]='findObjectsFormatter' [selectOnExact]='true' [inputFormatter]='formatter' - [resultFormatter]='formatter'/>`, + [resultFormatter]='formatter'/>` ); compiled = fixture.nativeElement; inputEl = getNativeInput(compiled); @@ -1066,7 +1110,7 @@ describe('ngb-typeahead', () => { `
-
`, + ` ); compiled = fixture.nativeElement; inputEl = getNativeInput(compiled); @@ -1124,7 +1168,7 @@ class TestComponent { find = (text$: Observable) => { const clicks$ = this.click$.pipe(filter(() => !this.typeahead.isPopupOpen())); this.findOutput$ = merge(text$, this.focus$, clicks$).pipe( - map((text) => this._strings.filter((v) => v.startsWith(text))), + map((text) => this._strings.filter((v) => v.startsWith(text))) ); return this.findOutput$; }; @@ -1132,7 +1176,7 @@ class TestComponent { findFilter = (text$: Observable) => { return text$.pipe( filter((term) => term.length > 1), - map((text) => this._strings.filter((v) => v.indexOf(text) > -1)), + map((text) => this._strings.filter((v) => v.indexOf(text) > -1)) ); }; @@ -1184,7 +1228,7 @@ class TestOnPushComponent { find = (text$: Observable) => { return text$.pipe( debounceTime(200), - map((text) => this._strings.filter((v) => v.startsWith(text))), + map((text) => this._strings.filter((v) => v.startsWith(text))) ); }; } @@ -1196,7 +1240,7 @@ class TestAsyncComponent { find = (text$: Observable) => { return text$.pipe( debounceTime(200), - map((text) => this._strings.filter((v) => v.startsWith(text))), + map((text) => this._strings.filter((v) => v.startsWith(text))) ); }; } 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); } }