feat(a11y): add autoCapture option to cdkTrapFocus (#7641)

This also renames `FocusTrapDirective` to `CdkTrapFocus` and re-exports it under the old name.
This commit is contained in:
Jeremy Elbourn
2017-11-07 09:58:41 -08:00
committed by Joey Perrott
parent 7610c7c567
commit 20b47d72b1
4 changed files with 80 additions and 12 deletions
+3 -3
View File
@@ -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,
+43 -7
View File
@@ -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: `
<button type="button">Toggle</button>
<div *ngIf="showTrappedRegion" cdkTrapFocus cdkTrapFocusAutoCapture>
<input id="auto-capture-target">
<button>SAVE</button>
</div>
`
})
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;
}
+29 -2
View File
@@ -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();
}
}
}
+5
View File
@@ -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};