Skip to content

Commit 20b47d7

Browse files
jelbournjosephperrott
authored andcommitted
feat(a11y): add autoCapture option to cdkTrapFocus (#7641)
This also renames `FocusTrapDirective` to `CdkTrapFocus` and re-exports it under the old name.
1 parent 7610c7c commit 20b47d7

File tree

4 files changed

+80
-12
lines changed

4 files changed

+80
-12
lines changed

src/cdk/a11y/a11y-module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10-
import {FocusTrapDeprecatedDirective, FocusTrapDirective, FocusTrapFactory} from './focus-trap';
10+
import {FocusTrapDeprecatedDirective, CdkTrapFocus, FocusTrapFactory} from './focus-trap';
1111
import {LIVE_ANNOUNCER_PROVIDER} from './live-announcer';
1212
import {InteractivityChecker} from './interactivity-checker';
1313
import {CommonModule} from '@angular/common';
@@ -17,8 +17,8 @@ import {CdkMonitorFocus, FOCUS_MONITOR_PROVIDER} from './focus-monitor';
1717

1818
@NgModule({
1919
imports: [CommonModule, PlatformModule],
20-
declarations: [FocusTrapDirective, FocusTrapDeprecatedDirective, CdkMonitorFocus],
21-
exports: [FocusTrapDirective, FocusTrapDeprecatedDirective, CdkMonitorFocus],
20+
declarations: [CdkTrapFocus, FocusTrapDeprecatedDirective, CdkMonitorFocus],
21+
exports: [CdkTrapFocus, FocusTrapDeprecatedDirective, CdkMonitorFocus],
2222
providers: [
2323
InteractivityChecker,
2424
FocusTrapFactory,

src/cdk/a11y/focus-trap.spec.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Platform} from '@angular/cdk/platform';
22
import {Component, ViewChild} from '@angular/core';
33
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
4-
import {FocusTrap, FocusTrapDirective, FocusTrapFactory} from './focus-trap';
4+
import {FocusTrap, CdkTrapFocus, FocusTrapFactory} from './focus-trap';
55
import {InteractivityChecker} from './interactivity-checker';
66

77

@@ -10,12 +10,13 @@ describe('FocusTrap', () => {
1010
beforeEach(async(() => {
1111
TestBed.configureTestingModule({
1212
declarations: [
13-
FocusTrapDirective,
13+
CdkTrapFocus,
1414
FocusTrapWithBindings,
1515
SimpleFocusTrap,
1616
FocusTrapTargets,
1717
FocusTrapWithSvg,
1818
FocusTrapWithoutFocusableElements,
19+
FocusTrapWithAutoCapture,
1920
],
2021
providers: [InteractivityChecker, Platform, FocusTrapFactory]
2122
});
@@ -154,6 +155,27 @@ describe('FocusTrap', () => {
154155
expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow();
155156
});
156157
});
158+
159+
describe('with autoCapture', () => {
160+
it('should automatically capture and return focus on init / destroy', async(() => {
161+
const fixture = TestBed.createComponent(FocusTrapWithAutoCapture);
162+
fixture.detectChanges();
163+
164+
const buttonOutsideTrappedRegion = fixture.nativeElement.querySelector('button');
165+
buttonOutsideTrappedRegion.focus();
166+
expect(document.activeElement).toBe(buttonOutsideTrappedRegion);
167+
168+
fixture.componentInstance.showTrappedRegion = true;
169+
fixture.detectChanges();
170+
171+
fixture.whenStable().then(() => {
172+
expect(document.activeElement.id).toBe('auto-capture-target');
173+
174+
fixture.destroy();
175+
expect(document.activeElement).toBe(buttonOutsideTrappedRegion);
176+
});
177+
}));
178+
});
157179
});
158180

159181

@@ -166,7 +188,21 @@ describe('FocusTrap', () => {
166188
`
167189
})
168190
class SimpleFocusTrap {
169-
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
191+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
192+
}
193+
194+
@Component({
195+
template: `
196+
<button type="button">Toggle</button>
197+
<div *ngIf="showTrappedRegion" cdkTrapFocus cdkTrapFocusAutoCapture>
198+
<input id="auto-capture-target">
199+
<button>SAVE</button>
200+
</div>
201+
`
202+
})
203+
class FocusTrapWithAutoCapture {
204+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
205+
showTrappedRegion = false;
170206
}
171207

172208

@@ -179,7 +215,7 @@ class SimpleFocusTrap {
179215
`
180216
})
181217
class FocusTrapWithBindings {
182-
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
218+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
183219
renderFocusTrap = true;
184220
_isFocusTrapEnabled = true;
185221
}
@@ -199,7 +235,7 @@ class FocusTrapWithBindings {
199235
`
200236
})
201237
class FocusTrapTargets {
202-
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
238+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
203239
}
204240

205241

@@ -213,7 +249,7 @@ class FocusTrapTargets {
213249
`
214250
})
215251
class FocusTrapWithSvg {
216-
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
252+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
217253
}
218254

219255
@Component({
@@ -224,5 +260,5 @@ class FocusTrapWithSvg {
224260
`
225261
})
226262
class FocusTrapWithoutFocusableElements {
227-
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
263+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
228264
}

src/cdk/a11y/focus-trap.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,51 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
348348
selector: '[cdkTrapFocus]',
349349
exportAs: 'cdkTrapFocus',
350350
})
351-
export class FocusTrapDirective implements OnDestroy, AfterContentInit {
351+
export class CdkTrapFocus implements OnDestroy, AfterContentInit {
352352
/** Underlying FocusTrap instance. */
353353
focusTrap: FocusTrap;
354354

355+
/** Previously focused element to restore focus to upon destroy when using autoCapture. */
356+
private _previouslyFocusedElement: HTMLElement | null = null;
357+
355358
/** Whether the focus trap is active. */
356359
@Input('cdkTrapFocus')
357360
get enabled(): boolean { return this.focusTrap.enabled; }
358361
set enabled(value: boolean) { this.focusTrap.enabled = coerceBooleanProperty(value); }
359362

360-
constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) {
363+
/**
364+
* Whether the directive should automatially move focus into the trapped region upon
365+
* initialization and return focus to the previous activeElement upon destruction.
366+
*/
367+
@Input('cdkTrapFocusAutoCapture')
368+
get autoCapture(): boolean { return this._autoCapture; }
369+
set autoCapture(value: boolean) { this._autoCapture = coerceBooleanProperty(value); }
370+
private _autoCapture: boolean;
371+
372+
constructor(
373+
private _elementRef: ElementRef,
374+
private _focusTrapFactory: FocusTrapFactory,
375+
private _platform: Platform) {
361376
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
362377
}
363378

364379
ngOnDestroy() {
365380
this.focusTrap.destroy();
381+
382+
// If we stored a previously focused element when using autoCapture, return focus to that
383+
// element now that the trapped region is being destroyed.
384+
if (this._previouslyFocusedElement) {
385+
this._previouslyFocusedElement.focus();
386+
this._previouslyFocusedElement = null;
387+
}
366388
}
367389

368390
ngAfterContentInit() {
369391
this.focusTrap.attachAnchors();
392+
393+
if (this.autoCapture && this._platform.isBrowser) {
394+
this._previouslyFocusedElement = document.activeElement as HTMLElement;
395+
this.focusTrap.focusInitialElementWhenReady();
396+
}
370397
}
371398
}

src/cdk/a11y/public-api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
import {CdkTrapFocus} from './focus-trap';
9+
810

911
export * from './activedescendant-key-manager';
1012
export * from './aria-describer';
@@ -16,3 +18,6 @@ export * from './list-key-manager';
1618
export * from './live-announcer';
1719
export * from './focus-monitor';
1820
export * from './a11y-module';
21+
22+
/** @deprecated Renamed to CdkTrapFocus. */
23+
export {CdkTrapFocus as FocusTrapDirective};

0 commit comments

Comments
 (0)