valor-software / ngx-bootstrap

Fast and reliable Bootstrap widgets in Angular (supports Ivy engine)
https://valor-software.com/ngx-bootstrap
MIT License
5.53k stars 1.69k forks source link

Typeahead accessibility failed with screen reader NVDA #6681

Open jacques-lebourgeois opened 2 months ago

jacques-lebourgeois commented 2 months ago

Bug description: When using a screen reader, the user is not informed on the currently active line in the dropdown list

Plunker/StackBlitz that reproduces the issue:

Versions of ngx-bootstrap, Angular, and Bootstrap:

ngx-bootstrap: last one

Angular: last one

Bootstrap: last one

Build system: Angular CLI, System.js, webpack, starter seed:

Expected behavior

To make in work in our Angular project, we create a complentary directive that had aria-activedescendant="ngb-typeahead-0-0" to the input element and aria-selected="true|false" to each button of the ldropdown list

lexasq commented 1 month ago

NVDA doesn't work as it suppose to work, aria-label should do the trick, but it doesn't recognize it, but it picks up content of tag on the list. Not sure if its our bug.

jacques-lebourgeois commented 3 weeks ago

hi,

Thanks for your answer.

I am not sure it is not a problem of aria-label.

The problem seems to lie in the fact that with the [typeaheadMinLength]="0" attribute, the first element of the suggetsed list is automatically selected but the screen reader does not have the information.

As NVDA is widely used as a screen reader for the visually impaired, this was blocking the use of our WEB application. So we had in our project a directive to change typeahead behavior that add

  • aria-activedescendant: identifies the currently active element when focus is on a composite widget, combobox, textbox, group, or application.
  • aria-selected: indicates the current “selected” state of various widgets.

It works fine with both chrome and firefox.

Here is the added code

import {
  AfterViewInit,
  Directive,
  ElementRef,
  HostListener,
} from '@angular/core';

@Directive({
  selector: 'input[typeahead]',
})
export class LoTypeaheadDirective implements AfterViewInit {
  private input: HTMLInputElement;
  constructor(private elementRef: ElementRef<HTMLInputElement>) {}

  ngAfterViewInit(): void {
    this.input = this.elementRef.nativeElement;
  }
  @HostListener('keyup', ['$event'])
  public async onKeyUp(e: KeyboardEvent): Promise<void> {
    window.setTimeout(() => {
      const typeahead: HTMLSelectElement = this.input.parentElement.querySelector(
        'typeahead-container'
      );

      if (typeahead) {
        this.input.setAttribute('aria-activedescendant', typeahead.id);
        this.input.parentElement
          .querySelectorAll('typeahead-container .dropdown-item')
          .forEach((elt: HTMLOptionElement, index) => {
            elt.setAttribute('aria-selected', 'false');
          });
        const active: HTMLOptionElement = this.input.parentElement.querySelector(
          'typeahead-container .dropdown-item.active'
        );
        if (active) {
          active.setAttribute('aria-selected', 'true');
          this.input.setAttribute('aria-activedescendant', active.id);
        }
      }
    });
  }
}
lexasq commented 3 weeks ago

@jacques-lebourgeois We were always trying to keep our lib friendly and compliant with accessibility standards, thanks for your input,. I've tried doing this in my fork, I've tried aria-label and aria-selected without much success. I'll experiment with this approach as well and if it'll fit well, it will become a part of the next patch.