Search code examples
angularmodal-dialogfocusaccessibilitywai-aria

Trapping focus inside modal making ADA compliant in Angular 10


I'm figuring out to make my existing modal popup compliant with ADA, so as the focus of the keyboard Tab remain under this modal container made used of user defined handleTabKeyFocus(). But querySelector selector always returns null with querySelectorAll results in

ERROR TypeError: Cannot read property 'querySelectorAll' of null

<!-- Modal Popup for Candidate Information -->
<ng-template #candidateInfoTemplate>
    <div class="modal-header" id="xc">
        <h5 class="modal-title float-left"> <b>Candidate Information </b></h5>
        <button type="button" class="close pull-right" aria-label="Close" (click)="closeModalWithFocus(candidateDetailTCInfo.examUserID)" title="CLOSE">
            <span aria-hidden="true">x</span>
        </button>
    </div>
    <!-- Modal body -->
    <div class="modal-body backgroundColorWhite" id="modalBody">
        <div class="row mb-1 backgroundColorgray" *ngIf="!disableColumn">
            <div class="col-sm-5">
                Candidate Id
            </div>
            <div class="col-sm-1">
                :
            </div>
            <div class="col-sm-6">
                {{candidateDetailTCInfo?.loginName}}
            </div>
        </div>
        <div class="clearfix"></div>
        <!-- Modal Footer-->
        <div style="text-align:center" ModFocus>
            <button title="{{ResourceKeys.sbclose}}" type="button" class="btn btn-light btn_mng1 mr5" aria-label="Close" (click)="closeModalWithFocus(candidateDetailTCInfo.examUserID)">
                {{ResourceKeys.sbclose}}
            </button>
        </div>
    </div>
</ng-template>

@HostListener('document:keydown', ['$event'])
handleTabKeyFocus(e) {
    if (e.keyCode === 9) {
        let focusable = document.querySelector('#candidateInfoTemplate').querySelectorAll('input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');
        // It returns null
        if (focusable.length) {
            let first = focusable[0];
            let last = focusable[focusable.length - 1];
            let shift = e.shiftKey;
            if (shift) {
                if (e.target === first) { // shift-tab pressed on first input in dialog
                    (last as HTMLElement).focus();
                    e.preventDefault();
                }
            } else {
                if (e.target === last) { // tab pressed on last input in dialog
                    (first as HTMLElement).focus();
                    e.preventDefault();
                }
            }
        }
    }
}

<div (click)="opencandidateDetailsModal(candidateInfoTemplate, detail)" >
    <a href="#" onclick="return false;" [attr.id]="'candidateNameFocus_' + detail.examUserID" > {{detail.candidateName}} </a>
</div>

opencandidateDetailsModal(template: TemplateRef<any>, candObj: any) {
    try {
        this.getCandidateDetailTestInfo(candObj);
        this.modalRef = this.modalService.show(template, this.modalPopupconfig);
    } catch (error) {
        console.log('Method: opencandidateDetailsModal', error);
    }
}

closeModalWithFocus(examUserID: any) {
    try {
        this.modalRef.hide();
        const candidateFocusElement: HTMLElement = (<HTMLElement>document.getElementById('candidateNameFocus_' + examUserID));
        if (candidateFocusElement !== null) {
            candidateFocusElement?.focus();
        }
        const previousActiveElement = document.activeElement;
        if (document.body.contains(previousActiveElement)) {
            (previousActiveElement as HTMLElement)?.focus();
        }
    } catch (error) {
        console.log('Method: closeModalWithFocus', error);
    }
}

Inbuilt ngx-bootstrap uses ng-template with the given id as candidateInfoTemplate

let focusable = document.querySelector('#candidateInfoTemplate').querySelectorAll('input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');

Before opening the modal popup throws an error when a keyboard event occurs Cannot read property 'querySelectorAll' of null

How do we retain focus with in the modal popup with ngx-bootstrap?...


Solution

  • #candidateInfoTemplate is not an id, it is a reference to <ng-template>. You can access that using @ViewChild decorator.

    But for above issue, you can fix that by adding extra <div> inside <ng-template> tag with an id

    <ng-template #candidateInfoTemplate>
        <div id="candidateInfoTemplate">
        ...
        </div>
    </ng-template>