Skip to content

Commit 75cef79

Browse files
YusefOudaAlex Umanskiy
andauthored
fix(typeahead): accessibility fixes (#6791)
* fix(typeahead): accessibility fixes - Adds role=combobox and sets aria-controls instead of aria-owns on typeahead. - Ensure `activeChangeEvent` is fired when active item changes by using setter Fixes #6468, #6647 * fix(typeahead): emit empty string instead of undefined for activeChangeEvent --------- Co-authored-by: Alex Umanskiy <aleksey.umanskiy@valor-software.com>
1 parent da7fe4b commit 75cef79

3 files changed

Lines changed: 27 additions & 18 deletions

File tree

src/typeahead/testing/typeahead.directive.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ describe('Directive: Typeahead', () => {
210210
expect(() => directive.onBlur()).not.toThrow();
211211
});
212212
}));
213+
214+
it('should have appropriate aria attributes', () => {
215+
expect(inputElement.getAttribute('role')).toEqual('combobox');
216+
expect(inputElement.getAttribute('aria-expanded')).toEqual('false');
217+
expect(inputElement.getAttribute('aria-autocomplete')).toEqual('list');
218+
});
213219
});
214220

215221
describe('onFocus', () => {

src/typeahead/typeahead-container.component.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,13 @@ export class TypeaheadContainerComponent implements OnDestroy {
145145
if (this.typeaheadIsFirstItemActive && this._matches.length > 0) {
146146
this.setActive(this._matches[0]);
147147

148-
if (this._active?.isHeader()) {
148+
if (this.active?.isHeader()) {
149149
this.nextActiveMatch();
150150
}
151151
}
152152

153-
if (this._active && !this.typeaheadIsFirstItemActive) {
154-
const concurrency = this._matches.find(match => match.value === this._active?.value);
153+
if (this.active && !this.typeaheadIsFirstItemActive) {
154+
const concurrency = this._matches.find(match => match.value === this.active?.value);
155155

156156
if (concurrency) {
157157
this.selectActive(concurrency);
@@ -203,34 +203,35 @@ export class TypeaheadContainerComponent implements OnDestroy {
203203
}
204204

205205
selectActiveMatch(isActiveItemChanged?: boolean): void {
206-
if (this._active && this.parent?.typeaheadSelectFirstItem()) {
207-
this.selectMatch(this._active);
206+
if (this.active && this.parent?.typeaheadSelectFirstItem()) {
207+
this.selectMatch(this.active);
208208
}
209209

210210
if (!this.parent?.typeaheadSelectFirstItem() && isActiveItemChanged) {
211-
this.selectMatch(this._active);
211+
this.selectMatch(this.active);
212212
}
213213
}
214214

215215
activeChanged(): void {
216-
if (!this._active) {
216+
if (!this.active) {
217+
this.activeChangeEvent.emit('');
217218
return;
218219
}
219-
const index = this.matches.indexOf(this._active);
220+
const index = this.matches.indexOf(this.active);
220221
this.activeChangeEvent.emit(`${this.popupId}-${index}`);
221222
}
222223

223224
prevActiveMatch(): void {
224-
if (!this._active) {
225+
if (!this.active) {
225226
return;
226227
}
227228

228-
const index = this.matches.indexOf(this._active);
229+
const index = this.matches.indexOf(this.active);
229230
this.setActive(this.matches[
230231
index - 1 < 0 ? this.matches.length - 1 : index - 1
231232
]);
232233

233-
if (this._active.isHeader()) {
234+
if (this.active.isHeader()) {
234235
this.prevActiveMatch();
235236
}
236237

@@ -240,12 +241,12 @@ export class TypeaheadContainerComponent implements OnDestroy {
240241
}
241242

242243
nextActiveMatch(): void {
243-
const index = this._active ? this.matches.indexOf(this._active) : -1;
244+
const index = this.active ? this.matches.indexOf(this.active) : -1;
244245
this.setActive(this.matches[
245246
index + 1 > this.matches.length - 1 ? 0 : index + 1
246247
]);
247248

248-
if (this._active?.isHeader()) {
249+
if (this.active?.isHeader()) {
249250
this.nextActiveMatch();
250251
}
251252

@@ -394,9 +395,9 @@ export class TypeaheadContainerComponent implements OnDestroy {
394395
}
395396

396397
protected setActive(value?: TypeaheadMatch): void {
397-
this._active = value;
398+
this.active = value;
398399
let preview;
399-
if (!(this._active == null || this._active.isHeader())) {
400+
if (!(this.active == null || this.active.isHeader())) {
400401
preview = value;
401402
}
402403
this.parent?.typeaheadOnPreview.emit(preview);

src/typeahead/typeahead.directive.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ type TypeaheadOptionArr = TypeaheadOption[] | Observable<TypeaheadOption>;
3737
exportAs: 'bs-typeahead',
3838
host: {
3939
'[attr.aria-activedescendant]': 'activeDescendant',
40-
'[attr.aria-owns]': 'isOpen ? popupId : null',
40+
'[attr.aria-controls]': 'isOpen ? popupId : null',
4141
'[attr.aria-expanded]': 'isOpen',
42-
'[attr.aria-autocomplete]': 'list'
42+
'[attr.aria-autocomplete]': 'list',
43+
'[attr.role]': `'combobox'`
4344
},
4445
standalone: true,
4546
providers: [ComponentLoaderFactory, PositioningService]
@@ -421,7 +422,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
421422
this.element.nativeElement.focus();
422423

423424
this._container.activeChangeEvent.subscribe((activeId: string) => {
424-
this.activeDescendant = activeId;
425+
this.activeDescendant = activeId || undefined;
425426
this.changeDetection.markForCheck();
426427
});
427428
this.isOpen = true;
@@ -433,6 +434,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
433434
this._outsideClickListener();
434435
this._container = void 0;
435436
this.isOpen = false;
437+
this.activeDescendant = void 0;
436438
this.changeDetection.markForCheck();
437439
}
438440
this.typeaheadOnPreview.emit(undefined);

0 commit comments

Comments
 (0)