angular - Component accessibility issue - Stack Overflow

admin2025-04-26  3

I am trying to make zorro library's select component accessible in my angular application, but when I go through select dropdown options with the help of keyboard navigation screen reader reads the options as 'blank' instead of announcing the option name properly.

I tried implementing a custom directive that we can add to select component which will add aria attributes to select component and make it ADA compliant,directive is able to add those attributes ,but screen reader is still not able to read the option name proprly. Below is the custom directive I implemented for the same.

import { Directive, ElementRef, Renderer2, Input, HostListener, OnDestroy, AfterViewInit } from '@angular/core';
import { ChangeDetectorRef } from '@angular/core';

@Directive({
  selector: '[appAccessibleSelect]'
})
export class AccessibleSelectDirective implements AfterViewInit, OnDestroy {
  @Input() labelId!: string;
  private mutationObserver!: MutationObserver;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private cdRef: ChangeDetectorRef
  ) {}

  ngAfterViewInit() {
    this.setAriaAttributes();
    this.addInputAccessibility();
  }

  private setAriaAttributes() {
    const selectElement = this.el.nativeElement.querySelector('.ant-select-selector');
    if (selectElement) {
      this.renderer.setAttribute(selectElement, 'role', 'combobox');
      this.renderer.setAttribute(selectElement, 'aria-labelledby', this.labelId);
      this.renderer.setAttribute(selectElement, 'aria-expanded', 'false');
      this.renderer.setAttribute(selectElement, 'aria-haspopup', 'listbox');
    }
  }

  private addInputAccessibility() {
    const inputElement = this.el.nativeElement.querySelector('input.ant-select-selection-search-input');
    if (inputElement) {
      this.renderer.setAttribute(inputElement, 'aria-label', this.labelId);
      setTimeout(() => {
        this.cdRef.detectChanges();
      }, 1000);
    } else {
      console.error('Input element not found.');
    }
  }

  private initializeMutationObserver(dropdownElement: HTMLElement) {
    const optionElements = dropdownElement.querySelectorAll('nz-option-item');
    optionElements.forEach((option, index) => {
      const optionElement = option as HTMLElement;
      const optionId = `option-${index}`;

      // Ensure aria-label is set for each option
      if (!optionElement.hasAttribute('aria-label')) {
        const optionLabel = optionElement.innerText.trim();
        this.renderer.setAttribute(optionElement, 'aria-label', optionLabel);
      }

      // Ensure role and id are properly set for each option
      this.renderer.setAttribute(optionElement, 'role', 'option');
      this.renderer.setAttribute(optionElement, 'id', optionId);

      // Ensure content is visible for screen readers
      const contentElement = optionElement.querySelector('.ant-select-item-option-content');
      if (contentElement) {
        // Set aria-labelledby in case content needs to be specifically linked
        // this.renderer.setAttribute(contentElement, 'aria-live', 'assertive');
        this.renderer.setAttribute(contentElement, 'role', 'region')
        this.renderer.setAttribute(contentElement, 'tabindex', '0')
      }

      // Listen to focus and blur events to update aria-selected
      this.renderer.listen(optionElement, 'focus', () => {
        this.renderer.setAttribute(optionElement, 'aria-selected', 'true');
        this.renderer.setAttribute(this.el.nativeElement, 'aria-activedescendant', optionId);
      });

      this.renderer.listen(optionElement, 'blur', () => {
        this.renderer.setAttribute(optionElement, 'aria-selected', 'false');
      });
    });

    this.mutationObserver = new MutationObserver(() => {
      this.updateOptionsAccessibility(dropdownElement);
    });

    this.mutationObserver.observe(dropdownElement, { childList: true, subtree: true });
  }


  private updateOptionsAccessibility(dropdownElement: HTMLElement) {
    const optionElements = dropdownElement.querySelectorAll('nz-option-item');
    optionElements.forEach((option, index) => {
      const optionElement = option as HTMLElement;
      const optionId = `option-${index}`;

      if (!optionElement.hasAttribute('aria-label')) {
        this.renderer.setAttribute(optionElement, 'aria-label', optionElement.innerText);
      }

      this.renderer.setAttribute(optionElement, 'role', 'option');
      this.renderer.setAttribute(optionElement, 'id', optionId);
    });
  }

  private selectOption(optionElement: HTMLElement) {
    const optionId = optionElement.getAttribute('id');
    this.renderer.setAttribute(this.el.nativeElement, 'aria-activedescendant', optionId!);
    this.renderer.setAttribute(optionElement, 'aria-selected', 'true');
    // Update the select box with the selected option value
    const selectElement = this.el.nativeElement.querySelector('.ant-select-selection-placeholder');
    if (selectElement) {
      selectElement.textContent = optionElement.innerText;
    }
  }

  @HostListener('nzOpenChange', ['$event'])
  onOpenChange(isOpen: boolean) {
    const selectElement = this.el.nativeElement.querySelector('.ant-select-selector');
    if (selectElement) {
      this.renderer.setAttribute(selectElement, 'aria-expanded', isOpen.toString());
    }

    if (isOpen) {
      setTimeout(() => {
        const dropdownElement = document.querySelector('.ant-select-dropdown') as HTMLElement;
        if (dropdownElement) {
          this.initializeMutationObserver(dropdownElement);
        }
      });
    } else if (this.mutationObserver) {
      this.mutationObserver.disconnect();
    }
  }

  ngOnDestroy() {
    if (this.mutationObserver) {
      this.mutationObserver.disconnect();
    }
  }
}

below is how the directive is used in the template file

 <nz-select formControlName="OrderStatus" id="fcOrderStatus"  appAccessibleSelect
  labelId="{{orderListHelperService.statusLabel!}} Status"
  nzPlaceHolder="Select {{orderListHelperService.statusLabel}} Status" >
   <nz-option nzValue='-1' nzLabel="--Select--"></nz-option>
    <nz-option [nzLabel]="obj.name" [nzValue]="obj.id" *ngFor="let obj of orderListHelperService.orderStatusList" [attr.aria-label]="obj.name"  tabindex="0" role="option"></nz-option>   </nz-select>

If anybody knows the solution please share.

转载请注明原文地址:http://anycun.com/QandA/1745597631a90973.html