How to make PrimeNG Dropdown Keyboard Accessible

Viewed 2688

I want to make dropdown keyboard accessible. Right now, its not working when i am using keyboard up and down arrow. I applied tabindex but still not working. Anyone have any idea about this..

<p-dropdown [options]="cities" tabindex="1" placeholder="Select an option"></p-dropdown
2 Answers

PrimeNG not support reading dropdown options by default. Default behavior is navigation by arrows and change value. Default tabulator key action is hide overlay with element items. My solution forces tabulator key to choose dropdown option and when you hit enter key the value is changed.

I prepared angular directive which override default dropdown behavior. You can add add this directive do module and import it in place where you want use this change.

Testing for PrimeNG 8.0.0:

@Directive({
    selector: 'p-dropdown',
})
export class DropdownDirective implements OnInit, OnDestroy {
    readonly KEY_DOWN_EVENT: string = 'keydown';
    readonly FOCUS_IN_EVENT: string = 'focusin';
    readonly TABINDEX_ATTRIBUTE: string = 'tabindex';
    readonly LIST_ITEM_SELECTOR: string = 'li';

    private focusInSubscription: Subscription = new Subscription();
    private subscriptions: Subscription = new Subscription();
    private listElementSubscriptions: Subscription[] = [];
    private readonly dropdownHtmlElement: HTMLElement;

    constructor(private dropdown: Dropdown,
                private elementRef: ElementRef) {
        this.dropdownHtmlElement = this.elementRef.nativeElement as HTMLElement;
    }

    ngOnInit(): void {
        this.replaceKeyDownAction();
        this.subscribeToDropdownShowEvent();
        this.subscribeToDropdownHideEvent();
    }

    ngOnDestroy(): void {
        this.subscriptions.unsubscribe();
    }

    private subscribeToDropdownShowEvent() {
        this.subscriptions.add(
            this.dropdown.onShow.subscribe(() => {
                this.updateElementsList();
                this.subscribeToFocusInEvent();
            }),
        );
    }

    private subscribeToDropdownHideEvent() {
        this.subscriptions.add(
            this.dropdown.onHide.subscribe(() => {
                this.unsubscribeFromFocusInEvent();
                this.unsubscribeFromListElementsKeyDownEvents();
            }),
        );
    }

    private updateElementsList() {
        const listElements = this.dropdownHtmlElement.querySelectorAll<HTMLLIElement>(this.LIST_ITEM_SELECTOR);

        listElements.forEach((listElement: HTMLLIElement) => {
            this.subscribeToListElementKeyDownEvent(listElement);
            listElement.setAttribute(this.TABINDEX_ATTRIBUTE, '0');
        });
    }

    private subscribeToListElementKeyDownEvent(listElement: HTMLLIElement) {
        this.listElementSubscriptions.push(
            fromEvent(listElement, this.KEY_DOWN_EVENT)
                .pipe(filter((event: KeyboardEvent) => event.key === KEYBOARD_KEY.ENTER))
                .subscribe(() => {
                    // Simulation of mouse click of list element (trigger with (click) event in p-dropdownItem component which is child element of p-dropdown)
                    listElement.click();
                }),
        );
    }

    private unsubscribeFromListElementsKeyDownEvents() {
        this.listElementSubscriptions.forEach((singleSubscription: Subscription) => singleSubscription.unsubscribe());
        this.listElementSubscriptions = [];
    }

    private subscribeToFocusInEvent() {
        this.focusInSubscription = fromEvent(document, this.FOCUS_IN_EVENT).subscribe(({target}) => {

            // Situation when focus element is outside dropdown component
            if (!this.dropdownHtmlElement.contains(target as HTMLElement)) {
                this.dropdown.hide();
            }
        });
    }

    private unsubscribeFromFocusInEvent() {
        this.focusInSubscription.unsubscribe();
    }

    /**
     * Overwrite default onKeydown method from PrimeNG dropdown component
     */
    private replaceKeyDownAction() {
        const onKeyDownOriginFn = this.dropdown.onKeydown.bind(this.dropdown);

        this.dropdown.onKeydown = (event: KeyboardEvent, search: boolean) => {
            if (event.which === 9) {
                // Napisuję domyślne zachowanie tabulatora zdefiniowanego w klasie komponentu Dropdown z biblioteki PrimeNG
            } else {
                onKeyDownOriginFn(event, search);
            }
        }
    }
}
Related