From 00d42d6982c2fd9118b1ac79a4b3061f0c6c2c3e Mon Sep 17 00:00:00 2001 From: Max Okorokov Date: Fri, 25 Oct 2019 10:37:01 +0200 Subject: [PATCH] fix(focustrap): run focus trap event handlers outside Angular --- src/datepicker/datepicker-input.ts | 2 +- src/modal/modal-stack.ts | 7 ++-- src/util/focus-trap.ts | 66 ++++++++++++++++-------------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/datepicker/datepicker-input.ts b/src/datepicker/datepicker-input.ts index 08026a6007..2cabfdabce 100644 --- a/src/datepicker/datepicker-input.ts +++ b/src/datepicker/datepicker-input.ts @@ -348,7 +348,7 @@ export class NgbInputDatepicker implements OnChanges, // focus handling this._elWithFocus = this._document.activeElement; - ngbFocusTrap(this._cRef.location.nativeElement, this.closed, true); + ngbFocusTrap(this._ngZone, this._cRef.location.nativeElement, this.closed, true); this._cRef.instance.focus(); ngbAutoClose( diff --git a/src/modal/modal-stack.ts b/src/modal/modal-stack.ts index 5cda3d2408..fc4d9695ca 100644 --- a/src/modal/modal-stack.ts +++ b/src/modal/modal-stack.ts @@ -6,8 +6,9 @@ import { Inject, Injectable, Injector, + NgZone, RendererFactory2, - TemplateRef, + TemplateRef } from '@angular/core'; import {Subject} from 'rxjs'; @@ -32,12 +33,12 @@ export class NgbModalStack { constructor( private _applicationRef: ApplicationRef, private _injector: Injector, @Inject(DOCUMENT) private _document: any, - private _scrollBar: ScrollBar, private _rendererFactory: RendererFactory2) { + private _scrollBar: ScrollBar, private _rendererFactory: RendererFactory2, private _ngZone: NgZone) { // Trap focus on active WindowCmpt this._activeWindowCmptHasChanged.subscribe(() => { if (this._windowCmpts.length) { const activeWindowCmpt = this._windowCmpts[this._windowCmpts.length - 1]; - ngbFocusTrap(activeWindowCmpt.location.nativeElement, this._activeWindowCmptHasChanged); + ngbFocusTrap(this._ngZone, activeWindowCmpt.location.nativeElement, this._activeWindowCmptHasChanged); this._revertAriaHidden(); this._setAriaHidden(activeWindowCmpt.location.nativeElement); } diff --git a/src/util/focus-trap.ts b/src/util/focus-trap.ts index caef9239af..b1a9e79997 100644 --- a/src/util/focus-trap.ts +++ b/src/util/focus-trap.ts @@ -1,7 +1,9 @@ +import {NgZone} from '@angular/core'; + import {fromEvent, Observable} from 'rxjs'; import {filter, map, takeUntil, withLatestFrom} from 'rxjs/operators'; -import {Key} from '../util/key'; +import {Key} from './key'; const FOCUSABLE_ELEMENTS_SELECTOR = [ @@ -24,43 +26,47 @@ export function getFocusableBoundaryElements(element: HTMLElement): HTMLElement[ * * Works only for clicks inside the element and navigation with 'Tab', ignoring clicks outside of the element * + * @param zone Angular zone * @param element The element around which focus will be trapped inside * @param stopFocusTrap$ The observable stream. When completed the focus trap will clean up listeners * and free internal resources * @param refocusOnClick Put the focus back to the last focused element whenever a click occurs on element (default to * false) */ -export const ngbFocusTrap = (element: HTMLElement, stopFocusTrap$: Observable, refocusOnClick = false) => { - // last focused element - const lastFocusedElement$ = - fromEvent(element, 'focusin').pipe(takeUntil(stopFocusTrap$), map(e => e.target)); +export const ngbFocusTrap = + (zone: NgZone, element: HTMLElement, stopFocusTrap$: Observable, refocusOnClick = false) => { + zone.runOutsideAngular(() => { + // last focused element + const lastFocusedElement$ = + fromEvent(element, 'focusin').pipe(takeUntil(stopFocusTrap$), map(e => e.target)); - // 'tab' / 'shift+tab' stream - fromEvent(element, 'keydown') - .pipe( - takeUntil(stopFocusTrap$), - // tslint:disable:deprecation - filter(e => e.which === Key.Tab), - // tslint:enable:deprecation - withLatestFrom(lastFocusedElement$)) - .subscribe(([tabEvent, focusedElement]) => { - const[first, last] = getFocusableBoundaryElements(element); + // 'tab' / 'shift+tab' stream + fromEvent(element, 'keydown') + .pipe( + takeUntil(stopFocusTrap$), + // tslint:disable:deprecation + filter(e => e.which === Key.Tab), + // tslint:enable:deprecation + withLatestFrom(lastFocusedElement$)) + .subscribe(([tabEvent, focusedElement]) => { + const[first, last] = getFocusableBoundaryElements(element); - if ((focusedElement === first || focusedElement === element) && tabEvent.shiftKey) { - last.focus(); - tabEvent.preventDefault(); - } + if ((focusedElement === first || focusedElement === element) && tabEvent.shiftKey) { + last.focus(); + tabEvent.preventDefault(); + } - if (focusedElement === last && !tabEvent.shiftKey) { - first.focus(); - tabEvent.preventDefault(); + if (focusedElement === last && !tabEvent.shiftKey) { + first.focus(); + tabEvent.preventDefault(); + } + }); + + // inside click + if (refocusOnClick) { + fromEvent(element, 'click') + .pipe(takeUntil(stopFocusTrap$), withLatestFrom(lastFocusedElement$), map(arr => arr[1] as HTMLElement)) + .subscribe(lastFocusedElement => lastFocusedElement.focus()); } }); - - // inside click - if (refocusOnClick) { - fromEvent(element, 'click') - .pipe(takeUntil(stopFocusTrap$), withLatestFrom(lastFocusedElement$), map(arr => arr[1] as HTMLElement)) - .subscribe(lastFocusedElement => lastFocusedElement.focus()); - } -}; + };