Skip to content

Commit

Permalink
feat(typeahead): improved accessibility #582 (#5547)
Browse files Browse the repository at this point in the history
* Add id to typeahead-container

* Add attribute aria activeDescendant

* Add aria-aria-owns and aria-aria-expanded

* Add role="option" and role="listbox"

* Add id to typeahead child

* Add aria-autocomplete

* Add role='listBox'

* Add comment

* Delete unused property

* Use rendered to set attribute id

* fix(typeahead): fix typeahead position, according to new position service changes

Co-authored-by: Samuel Renier <samuel.renier@cnaf.fr>
Co-authored-by: Dmitriy Danilov <daniloff200@gmail.com>
Co-authored-by: dmitry-zhemchugov <44227371+dmitry-zhemchugov@users.noreply.github.com>
  • Loading branch information
4 people committed Mar 12, 2020
1 parent b405057 commit 00b39c4
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 11 deletions.
5 changes: 5 additions & 0 deletions src/typeahead/typeahead-container.component.html
Expand Up @@ -17,12 +17,15 @@
<ng-template #bs3Template>
<ul class="dropdown-menu"
#ulElement
role="listbox"
[style.overflow-y]="needScrollbar ? 'scroll': 'auto'"
[style.height]="needScrollbar ? guiHeight: 'auto'">
<ng-template ngFor let-match let-i="index" [ngForOf]="matches">
<li #liElements *ngIf="match.isHeader()" class="dropdown-header">{{ match }}</li>
<li #liElements
*ngIf="!match.isHeader()"
[id]="popupId + '-' + i"
role="option"
[@typeaheadAnimation]="animationState"
[class.active]="isActive(match)"
(mouseenter)="selectActive(match)">
Expand All @@ -43,6 +46,8 @@
<h6 *ngIf="match.isHeader()" class="dropdown-header">{{ match }}</h6>
<ng-template [ngIf]="!match.isHeader()">
<button #liElements
[id]="popupId + '-' + i"
role="option"
[@typeaheadAnimation]="animationState"
class="dropdown-item"
(click)="selectMatch(match, $event)"
Expand Down
39 changes: 31 additions & 8 deletions src/typeahead/typeahead-container.component.ts
Expand Up @@ -9,7 +9,9 @@ import {
Renderer2,
TemplateRef,
ViewChild,
ViewChildren
ViewChildren,
Output,
EventEmitter
} from '@angular/core';

import { isBs3, Utils } from 'ngx-bootstrap/utils';
Expand All @@ -22,6 +24,8 @@ import { TypeaheadDirective } from './typeahead.directive';
import { typeaheadAnimation } from './typeahead-animations';
import { TypeaheadOptionItemContext, TypeaheadOptionListContext, TypeaheadTemplateMethods } from './models';

let nextWindowId = 0;

@Component({
selector: 'typeahead-container',
templateUrl: './typeahead-container.component.html',
Expand All @@ -31,7 +35,8 @@ import { TypeaheadOptionItemContext, TypeaheadOptionListContext, TypeaheadTempla
'[style.height]': `isBs4 && needScrollbar ? guiHeight: 'auto'`,
'[style.visibility]': `'inherit'`,
'[class.dropup]': 'dropup',
style: 'position: absolute;display: block;'
style: 'position: absolute;display: block;',
'[attr.role]': `isBs4 ? 'listbox' : null `
},
styles: [
`
Expand All @@ -47,7 +52,11 @@ import { TypeaheadOptionItemContext, TypeaheadOptionListContext, TypeaheadTempla
],
animations: [typeaheadAnimation]
})

export class TypeaheadContainerComponent implements OnDestroy {
// tslint:disable-next-line: no-output-rename
@Output('activeChange') activeChangeEvent = new EventEmitter();

parent: TypeaheadDirective;
query: string[] | string;
isFocused = false;
Expand All @@ -61,6 +70,7 @@ export class TypeaheadContainerComponent implements OnDestroy {
animationState: string;
positionServiceSubscription: Subscription;
height = 0;
popupId = `ngb-typeahead-${nextWindowId++}`;

get isBs4(): boolean {
return !isBs3();
Expand Down Expand Up @@ -92,6 +102,7 @@ export class TypeaheadContainerComponent implements OnDestroy {
public element: ElementRef,
private changeDetectorRef: ChangeDetectorRef
) {
this.renderer.setAttribute(this.element.nativeElement, 'id', this.popupId);
this.positionServiceSubscription = this.positionService.event$.subscribe(
() => {
if (this.isAnimated) {
Expand All @@ -111,6 +122,11 @@ export class TypeaheadContainerComponent implements OnDestroy {
return this._active;
}

set active(active: TypeaheadMatch) {
this._active = active;
this.activeChanged();
}

get matches(): TypeaheadMatch[] {
return this._matches;
}
Expand All @@ -132,7 +148,7 @@ export class TypeaheadContainerComponent implements OnDestroy {
}

if (this.typeaheadIsFirstItemActive && this._matches.length > 0) {
this._active = this._matches[0];
this.active = this._matches[0];

if (this._active.isHeader()) {
this.nextActiveMatch();
Expand All @@ -148,7 +164,7 @@ export class TypeaheadContainerComponent implements OnDestroy {
return;
}

this._active = null;
this.active = null;
}
}

Expand Down Expand Up @@ -195,10 +211,16 @@ export class TypeaheadContainerComponent implements OnDestroy {
}
}

activeChanged(): void {
const index = this.matches.indexOf(this._active);
this.activeChangeEvent.emit(`${this.popupId}-${index}`);
}

prevActiveMatch(): void {

const index = this.matches.indexOf(this._active);

this._active = this.matches[
this.active = this.matches[
index - 1 < 0 ? this.matches.length - 1 : index - 1
];

Expand All @@ -214,10 +236,11 @@ export class TypeaheadContainerComponent implements OnDestroy {
nextActiveMatch(): void {
const index = this.matches.indexOf(this._active);

this._active = this.matches[
this.active = this.matches[
index + 1 > this.matches.length - 1 ? 0 : index + 1
];


if (this._active.isHeader()) {
this.nextActiveMatch();
}
Expand All @@ -229,7 +252,7 @@ export class TypeaheadContainerComponent implements OnDestroy {

selectActive(value: TypeaheadMatch): void {
this.isFocused = true;
this._active = value;
this.active = value;
}

highlight(match: TypeaheadMatch, query: string[] | string): string {
Expand Down Expand Up @@ -276,7 +299,7 @@ export class TypeaheadContainerComponent implements OnDestroy {
}

isActive(value: TypeaheadMatch): boolean {
return this._active === value;
return this.active === value;
}

selectMatch(value: TypeaheadMatch, e: Event = void 0): boolean {
Expand Down
25 changes: 22 additions & 3 deletions src/typeahead/typeahead.directive.ts
Expand Up @@ -29,7 +29,16 @@ import { TypeaheadOptionItemContext, TypeaheadOptionListContext } from './models
type TypeaheadOption = string | {[key in string | number]: any};
type Typeahead = TypeaheadOption[] | Observable<TypeaheadOption[]>;

@Directive({selector: '[typeahead]', exportAs: 'bs-typeahead'})
@Directive({
selector: '[typeahead]',
exportAs: 'bs-typeahead',
host: {
'[attr.aria-activedescendant]': 'activeDescendant',
'[attr.aria-aria-owns]': 'isOpen ? this._container.popupId : null',
'[attr.aria-aria-expanded]': 'isOpen',
'[attr.aria-autocomplete]': 'list'
}
})
export class TypeaheadDirective implements OnInit, OnDestroy {
/** options source, can be Array of strings, objects or
* an Observable for external matching process
Expand Down Expand Up @@ -140,6 +149,8 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
/** if false don't focus the input element the typeahead directive is associated with on selection */
// @Input() protected typeaheadFocusOnSelect:boolean;

activeDescendant: string;
isOpen = false;
_container: TypeaheadContainerComponent;
isActiveItemChanged = false;
isFocused = false;
Expand All @@ -148,7 +159,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
// tslint:disable-next-line:no-any
protected keyUpEventEmitter: EventEmitter<string> = new EventEmitter();
protected _matches: TypeaheadMatch[];
protected placement = 'bottom-left';
protected placement = 'bottom left';

private _typeahead: ComponentLoader<TypeaheadContainerComponent>;
private _subscriptions: Subscription[] = [];
Expand Down Expand Up @@ -344,7 +355,7 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
this._typeahead
.attach(TypeaheadContainerComponent)
.to(this.container)
.position({attachment: `${this.dropup ? 'top' : 'bottom'} start`})
.position({attachment: `${this.dropup ? 'top' : 'bottom'} left`})
.show({
typeaheadRef: this,
placement: this.placement,
Expand Down Expand Up @@ -382,13 +393,21 @@ export class TypeaheadDirective implements OnInit, OnDestroy {

this._container.matches = this._matches;
this.element.nativeElement.focus();

this._container.activeChangeEvent.subscribe((activeId: string) => {
this.activeDescendant = activeId;
this.changeDetection.markForCheck();
});
this.isOpen = true;
}

hide(): void {
if (this._typeahead.isShown) {
this._typeahead.hide();
this._outsideClickListener();
this._container = null;
this.isOpen = false;
this.changeDetection.markForCheck();
}
}

Expand Down

0 comments on commit 00b39c4

Please sign in to comment.