mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-14 02:08:50 +02:00
Fixes for dynamic modal a11y (#518)
* Do not close modal if click finishes on background * Trap tab focus in modals, use ESC to close modal * Fix Angular change detection errors in modals * Reset focus on next modal after closing modal * Minor fixes and linting * Attach focusTrap to modal-dialog element * Change mousedown event back to click * Make topModal private * Add new div for dismissing modal by clicking bg * Focus element in modal if no autoFocus directive * Use backdrop for dismissal * Fix typo
This commit is contained in:
parent
815b436f7c
commit
24fe836032
@ -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<any>;
|
||||
setComponentParameters: (component: any) => void;
|
||||
|
||||
private focusTrap: ConfigurableFocusTrap;
|
||||
|
||||
constructor(private modalService: ModalService, private cd: ChangeDetectorRef,
|
||||
private el: ElementRef<HTMLElement>, public modalRef: ModalRef) {}
|
||||
private el: ElementRef<HTMLElement>, 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<any>) {
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,19 +22,32 @@ export class ModalConfig<D = any> {
|
||||
|
||||
@Injectable()
|
||||
export class ModalService {
|
||||
protected modalCount = 0;
|
||||
protected modalList: ComponentRef<DynamicModalComponent>[] = [];
|
||||
|
||||
// Lazy loaded modules are not available in componentFactoryResolver,
|
||||
// therefore modules needs to manually initialize their resolvers.
|
||||
private factoryResolvers: Map<Type<any>, 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<T>(componentType: Type<T>, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user