primefaces / primeng

The Most Complete Angular UI Component Library
https://primeng.org
Other
10.18k stars 4.54k forks source link

Accessibility: On P-DIALOG, Shadow DOM Component fields doesn't get focus when user clicks "Tab" or "shift + tab" #12682

Open ramanareddygopu opened 1 year ago

ramanareddygopu commented 1 year ago

Describe the bug

On a P-Dialog, I am using input fields on the modal, and few input fields rendered from the components where encapsulation: ViewEncapsulation.ShadowDom components, when i try to access the fields by clicking "TAB" key or "SHIFT + TAB" key the focus is not going to fields which are inside the components where encapsulation is ViewEncapsulation.ShadowDom.

Environment

I am using the PRIMENG with Angular.

Reproducer

No response

Angular version

12.2.0

PrimeNG version

12.0.0

Build / Runtime

Angular CLI App

Language

TypeScript

Node version (for AoT issues node --version)

16.13.0

Browser(s)

Chrome

Steps to reproduce the behavior

1) create a Shadow DOM Component with few fields 2) add the created shadow component in p-dialog as a selector 3) When modal is open try using the "tab" or "Shift + Tab" key, and check if the fields are getting focused.

Expected behavior

No response

antoniomolinadev commented 2 months ago

Any plans to resolve this issue?

I have the same problem in Angular version 13.3.12 and primeNG 13.4.1.

Regards

antoniomolinadev commented 1 month ago

Finally, I modified the PrimeNG directive and added a solution for focusTrap in dialogs with Shadow DOM.

Additionally, I added some lines for the tabindex. In my project, I am using the data-tabindex attribute and ignoring the HTML tabindex because PrimeNG uses it in some components.

My directive:

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

@Directive({
  selector: '[customFocusTrap]',
  host: {
    class: 'p-element'
  }
})
export class CustomFocusTrapDirective {
  @Input() pFocusTrapDisabled: boolean = false;

  constructor(public el: ElementRef) {}

  @HostListener('keydown.tab', ['$event'])
  @HostListener('keydown.shift.tab', ['$event'])
  onkeydown(e: KeyboardEvent) {
    if (this.pFocusTrapDisabled !== true) {
      e.preventDefault();
      e.stopPropagation();
      const focusableElement = this.getNextFocusableElement(this.el.nativeElement, e.shiftKey);

      if (focusableElement) {
        focusableElement.focus();
        focusableElement.select?.();
      }
    }
  }

  public getNextFocusableElement(element: HTMLElement, reverse = false) {
    const focusableElements = this.getFocusableElements(element);
    let index = 0;
    if (focusableElements && focusableElements.length > 0) {
      let activeElement = focusableElements[0].ownerDocument.activeElement.shadowRoot
        ? focusableElements[0].ownerDocument.activeElement.shadowRoot.activeElement
        : focusableElements[0].ownerDocument.activeElement;
      const focusedIndex = focusableElements.indexOf(activeElement);

      if (reverse) {
        if (focusedIndex == -1 || focusedIndex === 0) {
          index = focusableElements.length - 1;
        } else {
          index = focusedIndex - 1;
        }
      } else if (focusedIndex != -1 && focusedIndex !== focusableElements.length - 1) {
        index = focusedIndex + 1;
      }
    }

    return focusableElements[index];
  }

  protected getFocusableElements(element: HTMLElement) {
    let focusableElements = this.find(
      element,
      `button:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]),
            [href]:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]),
            input:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]), select:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]),
            textarea:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]), [tabIndex]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]),
            [contenteditable]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden]):not(.p-disabled)`
    );
    const elementsWithTabIndex = focusableElements
      .filter((el) => el.hasAttribute('data-tabindex'))
      .sort((a, b) => parseInt(a.getAttribute('data-tabindex')) - parseInt(b.getAttribute('data-tabindex')));

    const elementsWithoutTabIndex = focusableElements.filter((el) => !el.hasAttribute('data-tabindex'));
    focusableElements = elementsWithTabIndex.concat(elementsWithoutTabIndex);

    let visibleFocusableElements = [];
    for (let focusableElement of focusableElements) {
      if (!!(focusableElement.offsetWidth || focusableElement.offsetHeight || focusableElement.getClientRects().length))
        visibleFocusableElements.push(focusableElement);
    }
    return visibleFocusableElements;
  }
  protected find(element: any, selector: string): any[] {
    return Array.from(element.querySelectorAll(selector));
  }
}

Regards