Skip to content

Commit

Permalink
fix(focustrap): run focus trap event handlers outside Angular (#3435)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxokorokov committed Nov 8, 2019
1 parent 5977dcb commit d1752ac
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/datepicker/datepicker-input.ts
Expand Up @@ -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(
Expand Down
7 changes: 4 additions & 3 deletions src/modal/modal-stack.ts
Expand Up @@ -6,8 +6,9 @@ import {
Inject,
Injectable,
Injector,
NgZone,
RendererFactory2,
TemplateRef,
TemplateRef
} from '@angular/core';
import {Subject} from 'rxjs';

Expand All @@ -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);
}
Expand Down
66 changes: 36 additions & 30 deletions 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 = [
Expand All @@ -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<any>, refocusOnClick = false) => {
// last focused element
const lastFocusedElement$ =
fromEvent<FocusEvent>(element, 'focusin').pipe(takeUntil(stopFocusTrap$), map(e => e.target));
export const ngbFocusTrap =
(zone: NgZone, element: HTMLElement, stopFocusTrap$: Observable<any>, refocusOnClick = false) => {
zone.runOutsideAngular(() => {
// last focused element
const lastFocusedElement$ =
fromEvent<FocusEvent>(element, 'focusin').pipe(takeUntil(stopFocusTrap$), map(e => e.target));

// 'tab' / 'shift+tab' stream
fromEvent<KeyboardEvent>(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<KeyboardEvent>(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());
}
};
};

0 comments on commit d1752ac

Please sign in to comment.