mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Joey Perrott
parent
7610c7c567
commit
20b47d72b1
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
Reference in New Issue
Block a user