diff --git a/src/cdk/a11y/a11y-module.ts b/src/cdk/a11y/a11y-module.ts
index 1cb501da20d..27b754af6a4 100644
--- a/src/cdk/a11y/a11y-module.ts
+++ b/src/cdk/a11y/a11y-module.ts
@@ -7,7 +7,7 @@
*/
import {NgModule} from '@angular/core';
-import {FocusTrapDeprecatedDirective, FocusTrapDirective, FocusTrapFactory} from './focus-trap';
+import {FocusTrapDeprecatedDirective, CdkTrapFocus, FocusTrapFactory} from './focus-trap';
import {LIVE_ANNOUNCER_PROVIDER} from './live-announcer';
import {InteractivityChecker} from './interactivity-checker';
import {CommonModule} from '@angular/common';
@@ -17,8 +17,8 @@ import {CdkMonitorFocus, FOCUS_MONITOR_PROVIDER} from './focus-monitor';
@NgModule({
imports: [CommonModule, PlatformModule],
- declarations: [FocusTrapDirective, FocusTrapDeprecatedDirective, CdkMonitorFocus],
- exports: [FocusTrapDirective, FocusTrapDeprecatedDirective, CdkMonitorFocus],
+ declarations: [CdkTrapFocus, FocusTrapDeprecatedDirective, CdkMonitorFocus],
+ exports: [CdkTrapFocus, FocusTrapDeprecatedDirective, CdkMonitorFocus],
providers: [
InteractivityChecker,
FocusTrapFactory,
diff --git a/src/cdk/a11y/focus-trap.spec.ts b/src/cdk/a11y/focus-trap.spec.ts
index bb3657308f1..13cbc9ca1c0 100644
--- a/src/cdk/a11y/focus-trap.spec.ts
+++ b/src/cdk/a11y/focus-trap.spec.ts
@@ -1,7 +1,7 @@
import {Platform} from '@angular/cdk/platform';
import {Component, ViewChild} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
-import {FocusTrap, FocusTrapDirective, FocusTrapFactory} from './focus-trap';
+import {FocusTrap, CdkTrapFocus, FocusTrapFactory} from './focus-trap';
import {InteractivityChecker} from './interactivity-checker';
@@ -10,12 +10,13 @@ describe('FocusTrap', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
- FocusTrapDirective,
+ CdkTrapFocus,
FocusTrapWithBindings,
SimpleFocusTrap,
FocusTrapTargets,
FocusTrapWithSvg,
FocusTrapWithoutFocusableElements,
+ FocusTrapWithAutoCapture,
],
providers: [InteractivityChecker, Platform, FocusTrapFactory]
});
@@ -154,6 +155,27 @@ describe('FocusTrap', () => {
expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow();
});
});
+
+ describe('with autoCapture', () => {
+ it('should automatically capture and return focus on init / destroy', async(() => {
+ const fixture = TestBed.createComponent(FocusTrapWithAutoCapture);
+ fixture.detectChanges();
+
+ const buttonOutsideTrappedRegion = fixture.nativeElement.querySelector('button');
+ buttonOutsideTrappedRegion.focus();
+ expect(document.activeElement).toBe(buttonOutsideTrappedRegion);
+
+ fixture.componentInstance.showTrappedRegion = true;
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(document.activeElement.id).toBe('auto-capture-target');
+
+ fixture.destroy();
+ expect(document.activeElement).toBe(buttonOutsideTrappedRegion);
+ });
+ }));
+ });
});
@@ -166,7 +188,21 @@ describe('FocusTrap', () => {
`
})
class SimpleFocusTrap {
- @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
+}
+
+@Component({
+ template: `
+
+
+
+
+
+ `
+})
+class FocusTrapWithAutoCapture {
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
+ showTrappedRegion = false;
}
@@ -179,7 +215,7 @@ class SimpleFocusTrap {
`
})
class FocusTrapWithBindings {
- @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
renderFocusTrap = true;
_isFocusTrapEnabled = true;
}
@@ -199,7 +235,7 @@ class FocusTrapWithBindings {
`
})
class FocusTrapTargets {
- @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
}
@@ -213,7 +249,7 @@ class FocusTrapTargets {
`
})
class FocusTrapWithSvg {
- @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
}
@Component({
@@ -224,5 +260,5 @@ class FocusTrapWithSvg {
`
})
class FocusTrapWithoutFocusableElements {
- @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
+ @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
}
diff --git a/src/cdk/a11y/focus-trap.ts b/src/cdk/a11y/focus-trap.ts
index a89b8cdd0fd..f6e217133c6 100644
--- a/src/cdk/a11y/focus-trap.ts
+++ b/src/cdk/a11y/focus-trap.ts
@@ -348,24 +348,51 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
selector: '[cdkTrapFocus]',
exportAs: 'cdkTrapFocus',
})
-export class FocusTrapDirective implements OnDestroy, AfterContentInit {
+export class CdkTrapFocus implements OnDestroy, AfterContentInit {
/** Underlying FocusTrap instance. */
focusTrap: FocusTrap;
+ /** Previously focused element to restore focus to upon destroy when using autoCapture. */
+ private _previouslyFocusedElement: HTMLElement | null = null;
+
/** Whether the focus trap is active. */
@Input('cdkTrapFocus')
get enabled(): boolean { return this.focusTrap.enabled; }
set enabled(value: boolean) { this.focusTrap.enabled = coerceBooleanProperty(value); }
- constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) {
+ /**
+ * Whether the directive should automatially move focus into the trapped region upon
+ * initialization and return focus to the previous activeElement upon destruction.
+ */
+ @Input('cdkTrapFocusAutoCapture')
+ get autoCapture(): boolean { return this._autoCapture; }
+ set autoCapture(value: boolean) { this._autoCapture = coerceBooleanProperty(value); }
+ private _autoCapture: boolean;
+
+ constructor(
+ private _elementRef: ElementRef,
+ private _focusTrapFactory: FocusTrapFactory,
+ private _platform: Platform) {
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
}
ngOnDestroy() {
this.focusTrap.destroy();
+
+ // If we stored a previously focused element when using autoCapture, return focus to that
+ // element now that the trapped region is being destroyed.
+ if (this._previouslyFocusedElement) {
+ this._previouslyFocusedElement.focus();
+ this._previouslyFocusedElement = null;
+ }
}
ngAfterContentInit() {
this.focusTrap.attachAnchors();
+
+ if (this.autoCapture && this._platform.isBrowser) {
+ this._previouslyFocusedElement = document.activeElement as HTMLElement;
+ this.focusTrap.focusInitialElementWhenReady();
+ }
}
}
diff --git a/src/cdk/a11y/public-api.ts b/src/cdk/a11y/public-api.ts
index c615cd7d5f6..552cd7a5bfc 100644
--- a/src/cdk/a11y/public-api.ts
+++ b/src/cdk/a11y/public-api.ts
@@ -5,6 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
+import {CdkTrapFocus} from './focus-trap';
+
export * from './activedescendant-key-manager';
export * from './aria-describer';
@@ -16,3 +18,6 @@ export * from './list-key-manager';
export * from './live-announcer';
export * from './focus-monitor';
export * from './a11y-module';
+
+/** @deprecated Renamed to CdkTrapFocus. */
+export {CdkTrapFocus as FocusTrapDirective};