#Simple focus trap and checking for disabled elements

4 messages · Page 1 of 1 (latest)

tepid kite
#

I'm building a simple focus trap as an attribute directive. Part of this will check the child elements within the host to make sure they are actually focusable, i.e. looking to see if an element has the disabled attribute. I've written a little function that does that, it lives outside of my directive class

function isDisabled(element: HTMLElement) {
  return element.hasAttribute('disabled');
}

The problem is that it always returns false even if I know one of the elements in the form has the disabled attribute. My assumption is that this is happening because this disabled attribute is set with property binding, so the element might not yet have the disabled property at the time isDisabled() has been called/executed. Assuming this is true, what can I do to solve this? I've attempted to use a different lifecycle hooks in order to catch the component at the "right time" but I've had no success so far.

See the following comment for entire Directive code:

#

Here is the complete Directive:

function isDisabled(element: HTMLElement) {
  return element.hasAttribute('disabled');
}

function isFocusableFormElement(element: HTMLElement) {
  console.log(isDisabled(element));
  const nodeName = element ? element.nodeName.toLowerCase() : '';

  if (nodeName === 'input') {
    return true;
  }
  if (nodeName === 'textarea') {
    return true;
  }
  if (nodeName === 'select') {
    return true;
  }
  if (nodeName === 'button') {
    return true;
  }
  return false;
}

@Directive({
  selector: '[FocusTrap]',
  standalone: true,
})
export class FocusTrapDirective {
  private _document = inject(DOCUMENT);
  private _elementStack = signal<HTMLElement[]>([]);

  ngAfterContentInit() {
    this.getTabbableFormElements(this._elementRef.nativeElement);
  }

  getTabbableFormElements(element: HTMLElement) {
    const children = element.children;

    for (let i = 0; i < children.length; i++) {
      let tabbableChild: HTMLElement;
      if (children[i].nodeType === this._document.ELEMENT_NODE) {
        tabbableChild = this.getTabbableFormElements(children[i] as HTMLElement);
      }

      if (isFocusableFormElement(children[i] as HTMLElement)) {
        this._elementStack.update((update) => [...update, children[i] as HTMLElement]);
      }

      if (tabbableChild) {
        return tabbableChild;
      }
    }

    return null;
  }

  @HostListener('keydown.tab', ['$event'])
  handleTabKey(event: KeyboardEvent) {
    const currentElement = event.target as HTMLElement;
    const lastElement = this._elementStack()[this._elementStack().length - 1];
    if (currentElement === lastElement) {
      event.preventDefault();
      const nextElement = this._elementStack()[0];
      nextElement.focus();
    }
  }
}

#

Since I'm testing the isDisabled() function, I'm only calling it in a console.log to see what it is returning when being passed elements. It always returns false, even tho one of the elements it is testing has disabled set with property binding

tepid kite
#

SOLVED: After a TON of trial and error with lifecycle hooks, I found that executing my code during ngAfterContentInit and ngDoCheck gets the job done, but I honestly have no idea if this is bad/counter to best practices.