diff --git a/angular/src/components/modal/dynamic-modal.component.ts b/angular/src/components/modal/dynamic-modal.component.ts index d3816520dd8..5194a44f205 100644 --- a/angular/src/components/modal/dynamic-modal.component.ts +++ b/angular/src/components/modal/dynamic-modal.component.ts @@ -10,6 +10,11 @@ import { ViewContainerRef } from '@angular/core'; +import { + ConfigurableFocusTrap, + ConfigurableFocusTrapFactory, +} from '@angular/cdk/a11y'; + import { ModalService } from '../../services/modal.service'; import { ModalRef } from './modal.ref'; @@ -26,8 +31,11 @@ export class DynamicModalComponent implements AfterViewInit, OnDestroy { childComponentType: Type; setComponentParameters: (component: any) => void; + private focusTrap: ConfigurableFocusTrap; + constructor(private modalService: ModalService, private cd: ChangeDetectorRef, - private el: ElementRef, public modalRef: ModalRef) {} + private el: ElementRef, private focusTrapFactory: ConfigurableFocusTrapFactory, + public modalRef: ModalRef) { } ngAfterViewInit() { this.loadChildComponent(this.childComponentType); @@ -37,6 +45,10 @@ export class DynamicModalComponent implements AfterViewInit, OnDestroy { this.cd.detectChanges(); this.modalRef.created(this.el.nativeElement); + this.focusTrap = this.focusTrapFactory.create(this.el.nativeElement.querySelector('.modal-dialog')); + if (this.el.nativeElement.querySelector('[appAutoFocus]') == null) { + this.focusTrap.focusFirstTabbableElementWhenReady(); + } } loadChildComponent(componentType: Type) { @@ -50,9 +62,15 @@ export class DynamicModalComponent implements AfterViewInit, OnDestroy { if (this.componentRef) { this.componentRef.destroy(); } + this.focusTrap.destroy(); } close() { this.modalRef.close(); } + + getFocus() { + const autoFocusEl = this.el.nativeElement.querySelector('[appAutoFocus]') as HTMLElement; + autoFocusEl?.focus(); + } } diff --git a/angular/src/directives/autofocus.directive.ts b/angular/src/directives/autofocus.directive.ts index 262fe157790..89b0cfd938b 100644 --- a/angular/src/directives/autofocus.directive.ts +++ b/angular/src/directives/autofocus.directive.ts @@ -2,8 +2,11 @@ import { Directive, ElementRef, Input, + NgZone, } from '@angular/core'; +import { take } from 'rxjs/operators'; + import { Utils } from 'jslib-common/misc/utils'; @Directive({ @@ -16,11 +19,15 @@ export class AutofocusDirective { private autofocus: boolean; - constructor(private el: ElementRef) { } + constructor(private el: ElementRef, private ngZone: NgZone) { } ngOnInit() { if (!Utils.isMobileBrowser && this.autofocus) { - this.el.nativeElement.focus(); + if (this.ngZone.isStable) { + this.el.nativeElement.focus(); + } else { + this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus()); + } } } } diff --git a/angular/src/services/modal.service.ts b/angular/src/services/modal.service.ts index 9f85a8102da..9674cd1a773 100644 --- a/angular/src/services/modal.service.ts +++ b/angular/src/services/modal.service.ts @@ -22,19 +22,32 @@ export class ModalConfig { @Injectable() export class ModalService { - protected modalCount = 0; + protected modalList: ComponentRef[] = []; // Lazy loaded modules are not available in componentFactoryResolver, // therefore modules needs to manually initialize their resolvers. private factoryResolvers: Map, ComponentFactoryResolver> = new Map(); constructor(private componentFactoryResolver: ComponentFactoryResolver, private applicationRef: ApplicationRef, - private injector: Injector) {} + private injector: Injector) { + document.addEventListener('keyup', event => { + if (event.key === 'Escape' && this.modalCount > 0) { + this.topModal.instance.close(); + } + }); + } + + get modalCount() { + return this.modalList.length; + } + + private get topModal() { + return this.modalList[this.modalCount - 1]; + } async openViewRef(componentType: Type, viewContainerRef: ViewContainerRef, setComponentParameters: (component: T) => void = null): Promise<[ModalRef, T]> { - this.modalCount++; const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false); modalComponentRef.instance.setComponentParameters = setComponentParameters; @@ -49,7 +62,6 @@ export class ModalService { if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) { return; } - this.modalCount++; const [modalRef, _] = this.openInternal(componentType, config, true); @@ -85,11 +97,17 @@ export class ModalService { this.applicationRef.detachView(componentRef.hostView); } componentRef.destroy(); - this.modalCount--; + + this.modalList.pop(); + if (this.modalCount > 0) { + this.topModal.instance.getFocus(); + } }); this.setupHandlers(modalRef); + this.modalList.push(componentRef); + return [modalRef, componentRef]; } @@ -100,19 +118,20 @@ export class ModalService { modalRef.onCreated.pipe(first()).subscribe(el => { document.body.classList.add('modal-open'); + const modalEl: HTMLElement = el.querySelector('.modal'); + const dialogEl = modalEl.querySelector('.modal-dialog') as HTMLElement; + backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade'; backdrop.style.zIndex = `${this.modalCount}040`; - document.body.appendChild(backdrop); + modalEl.prepend(backdrop); - el.querySelector('.modal-dialog').addEventListener('click', (e: Event) => { + dialogEl.addEventListener('click', (e: Event) => { e.stopPropagation(); }); + dialogEl.style.zIndex = `${this.modalCount}050`; - const modalEl: HTMLElement = el.querySelector('.modal'); - modalEl.style.zIndex = `${this.modalCount}050`; - - const modals = Array.from(el.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')); + const modals = Array.from(el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]')); for (const closeElement of modals) { closeElement.addEventListener('click', event => { modalRef.close(); @@ -127,10 +146,6 @@ export class ModalService { if (this.modalCount === 0) { document.body.classList.remove('modal-open'); } - - if (backdrop != null) { - document.body.removeChild(backdrop); - } }); }