diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts new file mode 100644 index 000000000000..8e5ea67a533d --- /dev/null +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 + */ + + export class ConfigurableFocusTrapConfig { + defer: boolean = false; + } diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap-factory.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap-factory.ts new file mode 100644 index 000000000000..33aae4ef9512 --- /dev/null +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap-factory.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {DOCUMENT} from '@angular/common'; +import { + Inject, + Injectable, + Optional, + NgZone, +} from '@angular/core'; +import {InteractivityChecker} from '../interactivity-checker/interactivity-checker'; +import {ConfigurableFocusTrap} from './configurable-focus-trap'; +import {ConfigurableFocusTrapConfig} from './configurable-focus-trap-config'; +import {FOCUS_TRAP_INERT_STRATEGY, FocusTrapInertStrategy} from './focus-trap-inert-strategy'; +import {EventListenerFocusTrapInertStrategy} from './event-listener-inert-strategy'; +import {FocusTrapManager} from './focus-trap-manager'; + +/** Factory that allows easy instantiation of configurable focus traps. */ +@Injectable({providedIn: 'root'}) +export class ConfigurableFocusTrapFactory { + private _document: Document; + private _inertStrategy: FocusTrapInertStrategy; + + constructor( + private _checker: InteractivityChecker, + private _ngZone: NgZone, + private _focusTrapManager: FocusTrapManager, + @Inject(DOCUMENT) _document: any, + @Optional() @Inject(FOCUS_TRAP_INERT_STRATEGY) _inertStrategy?: FocusTrapInertStrategy) { + + this._document = _document; + // TODO split up the strategies into different modules, similar to DateAdapter. + this._inertStrategy = _inertStrategy || new EventListenerFocusTrapInertStrategy(); + } + + /** + * Creates a focus-trapped region around the given element. + * @param element The element around which focus will be trapped. + * @param deferCaptureElements Defers the creation of focus-capturing elements to be done + * manually by the user. + * @returns The created focus trap instance. + */ + create(element: HTMLElement, config: ConfigurableFocusTrapConfig = + new ConfigurableFocusTrapConfig()): ConfigurableFocusTrap { + return new ConfigurableFocusTrap( + element, this._checker, this._ngZone, this._document, this._focusTrapManager, + this._inertStrategy, config); + } +} diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap.spec.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap.spec.ts new file mode 100644 index 000000000000..c810714b4a8f --- /dev/null +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap.spec.ts @@ -0,0 +1,127 @@ +import {AfterViewInit, Component, ElementRef, Type, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import { + A11yModule, + ConfigurableFocusTrap, + ConfigurableFocusTrapFactory, + FOCUS_TRAP_INERT_STRATEGY, + FocusTrap, + FocusTrapInertStrategy +} from '../index'; +import {FocusTrapManager} from './focus-trap-manager'; + +describe('ConfigurableFocusTrap', () => { + let providers: Array; + + describe('with FocusTrapInertStrategy', () => { + let mockInertStrategy: FocusTrapInertStrategy; + + beforeEach(() => { + mockInertStrategy = new MockFocusTrapInertStrategy(); + providers = [{provide: FOCUS_TRAP_INERT_STRATEGY, useValue: mockInertStrategy}]; + }); + + it('Calls preventFocus when it is created', () => { + spyOn(mockInertStrategy, 'preventFocus'); + spyOn(mockInertStrategy, 'allowFocus'); + + const fixture = createComponent(SimpleFocusTrap, providers); + fixture.detectChanges(); + + expect(mockInertStrategy.preventFocus).toHaveBeenCalledTimes(1); + expect(mockInertStrategy.allowFocus).not.toHaveBeenCalled(); + }); + + it('Calls preventFocus when it is enabled', () => { + spyOn(mockInertStrategy, 'preventFocus'); + + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.focusTrap.enabled = true; + + expect(mockInertStrategy.preventFocus).toHaveBeenCalledTimes(2); + }); + + it('Calls allowFocus when it is disabled', () => { + spyOn(mockInertStrategy, 'allowFocus'); + + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.focusTrap.enabled = false; + + expect(mockInertStrategy.allowFocus).toHaveBeenCalledTimes(1); + }); + }); + + describe('with FocusTrapManager', () => { + let manager: FocusTrapManager; + + beforeEach(() => { + manager = new FocusTrapManager(); + providers = [{provide: FocusTrapManager, useValue: manager}]; + }); + + it('Registers when it is created', () => { + spyOn(manager, 'register'); + + const fixture = createComponent(SimpleFocusTrap, providers); + fixture.detectChanges(); + + expect(manager.register).toHaveBeenCalledTimes(1); + }); + + it('Deregisters when it is disabled', () => { + spyOn(manager, 'deregister'); + + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.focusTrap.enabled = false; + + expect(manager.deregister).toHaveBeenCalledTimes(1); + }); + }); +}); + +function createComponent(componentType: Type, providers: Array = [] + ): ComponentFixture { + TestBed.configureTestingModule({ + imports: [A11yModule], + declarations: [componentType], + providers: providers + }).compileComponents(); + + return TestBed.createComponent(componentType); + } + +@Component({ + template: ` +
+ + +
+ ` +}) +class SimpleFocusTrap implements AfterViewInit { + @ViewChild('focusTrapElement') focusTrapElement!: ElementRef; + + focusTrap: ConfigurableFocusTrap; + + constructor(private _focusTrapFactory: ConfigurableFocusTrapFactory) { + } + + ngAfterViewInit() { + this.focusTrap = this._focusTrapFactory.create(this.focusTrapElement.nativeElement); + } +} + +class MockFocusTrapInertStrategy implements FocusTrapInertStrategy { + preventFocus(focusTrap: FocusTrap) {} + + allowFocus(focusTrap: FocusTrap) {} +} diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap.ts new file mode 100644 index 000000000000..8d5c8f10fb13 --- /dev/null +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {NgZone} from '@angular/core'; +import {InteractivityChecker} from '../interactivity-checker/interactivity-checker'; +import {FocusTrap} from './focus-trap'; +import {FocusTrapManager, ManagedFocusTrap} from './focus-trap-manager'; +import {FocusTrapInertStrategy} from './focus-trap-inert-strategy'; +import {ConfigurableFocusTrapConfig} from './configurable-focus-trap-config'; + +/** + * Class that allows for trapping focus within a DOM element. + * + * This class uses a strategy pattern that determines how it traps focus. + * See FocusTrapInertStrategy. + */ +export class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap { + /** Whether the FocusTrap is enabled. */ + get enabled(): boolean { return this._enabled; } + set enabled(value: boolean) { + this._enabled = value; + if (this._enabled) { + this._focusTrapManager.register(this); + } else { + this._focusTrapManager.deregister(this); + } + } + + constructor( + _element: HTMLElement, + _checker: InteractivityChecker, + _ngZone: NgZone, + _document: Document, + private _focusTrapManager: FocusTrapManager, + private _inertStrategy: FocusTrapInertStrategy, + config: ConfigurableFocusTrapConfig) { + super(_element, _checker, _ngZone, _document, config.defer); + this._focusTrapManager.register(this); + } + + /** Notifies the FocusTrapManager that this FocusTrap will be destroyed. */ + destroy() { + this._focusTrapManager.deregister(this); + super.destroy(); + } + + /** @docs-private Implemented as part of ManagedFocusTrap. */ + _enable() { + this._inertStrategy.preventFocus(this); + this.toggleAnchors(true); + } + + /** @docs-private Implemented as part of ManagedFocusTrap. */ + _disable() { + this._inertStrategy.allowFocus(this); + this.toggleAnchors(false); + } +} diff --git a/src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts new file mode 100644 index 000000000000..a5090e2e275e --- /dev/null +++ b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts @@ -0,0 +1,80 @@ +import {AfterViewInit, Component, ElementRef, Type, ViewChild} from '@angular/core'; +import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; +import { + A11yModule, + ConfigurableFocusTrapFactory, + ConfigurableFocusTrap, + EventListenerFocusTrapInertStrategy, + FOCUS_TRAP_INERT_STRATEGY, +} from '../index'; + + +describe('EventListenerFocusTrapInertStrategy', () => { + const providers = [ + {provide: FOCUS_TRAP_INERT_STRATEGY, useValue: new EventListenerFocusTrapInertStrategy()}]; + + it('refocuses the first FocusTrap element when focus moves outside the FocusTrap', + fakeAsync(() => { + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.outsideFocusableElement.nativeElement.focus(); + flush(); + + expect(document.activeElement).toBe( + componentInstance.firstFocusableElement.nativeElement, + 'Expected first focusable element to be focused'); + })); + + it('does not intercept focus when focus moves to another element in the FocusTrap', + fakeAsync(() => { + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.secondFocusableElement.nativeElement.focus(); + flush(); + + expect(document.activeElement).toBe( + componentInstance.secondFocusableElement.nativeElement, + 'Expected second focusable element to be focused'); + })); +}); + +function createComponent(componentType: Type, providers: Array = [] + ): ComponentFixture { + TestBed.configureTestingModule({ + imports: [A11yModule], + declarations: [componentType], + providers: providers + }).compileComponents(); + + return TestBed.createComponent(componentType); + } + +@Component({ + template: ` + +
+ + +
+ ` +}) +class SimpleFocusTrap implements AfterViewInit { + @ViewChild('focusTrapElement') focusTrapElement!: ElementRef; + @ViewChild('outsideFocusable') outsideFocusableElement!: ElementRef; + @ViewChild('firstFocusable') firstFocusableElement!: ElementRef; + @ViewChild('secondFocusable') secondFocusableElement!: ElementRef; + + focusTrap: ConfigurableFocusTrap; + + constructor(private _focusTrapFactory: ConfigurableFocusTrapFactory) { + } + + ngAfterViewInit() { + this.focusTrap = this._focusTrapFactory.create(this.focusTrapElement.nativeElement); + this.focusTrap.focusFirstTabbableElementWhenReady(); + } +} diff --git a/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts new file mode 100644 index 000000000000..4b9da49e81ce --- /dev/null +++ b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {FocusTrapInertStrategy} from './focus-trap-inert-strategy'; +import {ConfigurableFocusTrap} from './configurable-focus-trap'; +import {closest} from './polyfill'; + +/** + * Lightweight FocusTrapInertStrategy that adds a document focus event + * listener to redirect focus back inside the FocusTrap. + */ +export class EventListenerFocusTrapInertStrategy implements FocusTrapInertStrategy { + /** Focus event handler. */ + private _listener: ((e: FocusEvent) => void) | null = null; + + /** Adds a document event listener that keeps focus inside the FocusTrap. */ + preventFocus(focusTrap: ConfigurableFocusTrap): void { + // Ensure there's only one listener per document + if (this._listener) { + focusTrap._document.removeEventListener('focus', this._listener!, true); + } + + this._listener = (e: FocusEvent) => this._trapFocus(focusTrap, e); + focusTrap._ngZone.runOutsideAngular(() => { + focusTrap._document.addEventListener('focus', this._listener!, true); + }); + } + + /** Removes the event listener added in preventFocus. */ + allowFocus(focusTrap: ConfigurableFocusTrap): void { + if (!this._listener) { + return; + } + focusTrap._document.removeEventListener('focus', this._listener!, true); + this._listener = null; + } + + /** + * Refocuses the first element in the FocusTrap if the focus event target was outside + * the FocusTrap. + * + * This is an event listener callback. The event listener is added in runOutsideAngular, + * so all this code runs outside Angular as well. + */ + private _trapFocus(focusTrap: ConfigurableFocusTrap, event: FocusEvent) { + const target = event.target as HTMLElement; + // Don't refocus if target was in an overlay, because the overlay might be associated + // with an element inside the FocusTrap, ex. mat-select. + if (!focusTrap._element.contains(target) && + closest(target, 'div.cdk-overlay-pane') === null) { + // Some legacy FocusTrap usages have logic that focuses some element on the page + // just before FocusTrap is destroyed. For backwards compatibility, wait + // to be sure FocusTrap is still enabled before refocusing. + setTimeout(() => { + if (focusTrap.enabled) { + focusTrap.focusFirstTabbableElement(); + } + }); + } + } +} diff --git a/src/cdk/a11y/focus-trap/focus-trap-inert-strategy.ts b/src/cdk/a11y/focus-trap/focus-trap-inert-strategy.ts new file mode 100644 index 000000000000..137fb75cdfe1 --- /dev/null +++ b/src/cdk/a11y/focus-trap/focus-trap-inert-strategy.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 { + InjectionToken, +} from '@angular/core'; +import {FocusTrap} from './focus-trap'; + +/** The injection token used to specify the inert strategy. */ +export const FOCUS_TRAP_INERT_STRATEGY = + new InjectionToken('FOCUS_TRAP_INERT_STRATEGY'); + +/** + * A strategy that dictates how FocusTrap should prevent elements + * outside of the FocusTrap from being focused. + */ +export interface FocusTrapInertStrategy { + /** Makes all elements outside focusTrap unfocusable. */ + preventFocus(focusTrap: FocusTrap): void; + /** Reverts elements made unfocusable by preventFocus to their previous state. */ + allowFocus(focusTrap: FocusTrap): void; +} diff --git a/src/cdk/a11y/focus-trap/focus-trap-manager.spec.ts b/src/cdk/a11y/focus-trap/focus-trap-manager.spec.ts new file mode 100644 index 000000000000..f972e33f9e50 --- /dev/null +++ b/src/cdk/a11y/focus-trap/focus-trap-manager.spec.ts @@ -0,0 +1,48 @@ +import {FocusTrapManager, ManagedFocusTrap} from './focus-trap-manager'; + +describe('FocusTrapManager', () => { + let manager: FocusTrapManager; + + beforeEach(() => { + manager = new FocusTrapManager(); + }); + + it('Enables a FocusTrap when it is registered', () => { + const focusTrap = new MockManagedFocusTrap(); + spyOn(focusTrap, '_enable'); + manager.register(focusTrap); + expect(focusTrap._enable).toHaveBeenCalledTimes(1); + }); + + it('Disables a FocusTrap when it is deregistered', () => { + const focusTrap = new MockManagedFocusTrap(); + spyOn(focusTrap, '_disable'); + manager.deregister(focusTrap); + expect(focusTrap._disable).toHaveBeenCalledTimes(1); + }); + + it('Disables the previous FocusTrap when a new FocusTrap is registered', () => { + const focusTrap1 = new MockManagedFocusTrap(); + const focusTrap2 = new MockManagedFocusTrap(); + spyOn(focusTrap1, '_disable'); + manager.register(focusTrap1); + manager.register(focusTrap2); + expect(focusTrap1._disable).toHaveBeenCalledTimes(1); + }); + + it('Filters duplicates before registering a new FocusTrap', () => { + const focusTrap = new MockManagedFocusTrap(); + spyOn(focusTrap, '_disable'); + manager.register(focusTrap); + manager.register(focusTrap); + expect(focusTrap._disable).not.toHaveBeenCalled(); + }); +}); + +class MockManagedFocusTrap implements ManagedFocusTrap { + _enable() {} + _disable() {} + focusInitialElementWhenReady(): Promise { + return Promise.resolve(true); + } +} diff --git a/src/cdk/a11y/focus-trap/focus-trap-manager.ts b/src/cdk/a11y/focus-trap/focus-trap-manager.ts new file mode 100644 index 000000000000..213a10c70f61 --- /dev/null +++ b/src/cdk/a11y/focus-trap/focus-trap-manager.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {Injectable} from '@angular/core'; + +/** + * A FocusTrap managed by FocusTrapManager. + * Implemented by ConfigurableFocusTrap to avoid circular dependency. + */ +export interface ManagedFocusTrap { + _enable(): void; + _disable(): void; + focusInitialElementWhenReady(): Promise; +} + +/** Injectable that ensures only the most recently enabled FocusTrap is active. */ +@Injectable({providedIn: 'root'}) +export class FocusTrapManager { + // A stack of the FocusTraps on the page. Only the FocusTrap at the + // top of the stack is active. + private _focusTrapStack: ManagedFocusTrap[] = []; + + /** + * Disables the FocusTrap at the top of the stack, and then pushes + * the new FocusTrap onto the stack. + */ + register(focusTrap: ManagedFocusTrap): void { + // Dedupe focusTraps that register multiple times. + this._focusTrapStack = this._focusTrapStack.filter((ft) => ft !== focusTrap); + + let stack = this._focusTrapStack; + + if (stack.length) { + stack[stack.length - 1]._disable(); + } + + stack.push(focusTrap); + focusTrap._enable(); + } + + /** + * Removes the FocusTrap from the stack, and activates the + * FocusTrap that is the new top of the stack. + */ + deregister(focusTrap: ManagedFocusTrap): void { + focusTrap._disable(); + + const stack = this._focusTrapStack; + + const i = stack.indexOf(focusTrap); + if (i !== -1) { + stack.splice(i, 1); + if (stack.length) { + stack[stack.length - 1]._enable(); + } + } + } +} diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index 3ff81bad43fe..ccd2f5fb2b8d 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -30,6 +30,9 @@ import {InteractivityChecker} from '../interactivity-checker/interactivity-check * This class currently uses a relatively simple approach to focus trapping. * It assumes that the tab order is the same as DOM order, which is not necessarily true. * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause to two to misalign. + * + * @deprecated Use `ConfigurableFocusTrap` instead. + * @breaking-change for 11.0.0 Remove this class. */ export class FocusTrap { private _startAnchor: HTMLElement | null; @@ -50,13 +53,13 @@ export class FocusTrap { this._toggleAnchorTabIndex(value, this._endAnchor); } } - private _enabled: boolean = true; + protected _enabled: boolean = true; constructor( - private _element: HTMLElement, + readonly _element: HTMLElement, private _checker: InteractivityChecker, - private _ngZone: NgZone, - private _document: Document, + readonly _ngZone: NgZone, + readonly _document: Document, deferAnchors = false) { if (!deferAnchors) { @@ -319,6 +322,17 @@ export class FocusTrap { isEnabled ? anchor.setAttribute('tabindex', '0') : anchor.removeAttribute('tabindex'); } + /** + * Toggles the`tabindex` of both anchors to either trap Tab focus or allow it to escape. + * @param enabled: Whether the anchors should trap Tab. + */ + protected toggleAnchors(enabled: boolean) { + if (this._startAnchor && this._endAnchor) { + this._toggleAnchorTabIndex(enabled, this._startAnchor); + this._toggleAnchorTabIndex(enabled, this._endAnchor); + } + } + /** Executes a function when the zone is stable. */ private _executeOnStable(fn: () => any): void { if (this._ngZone.isStable) { @@ -329,8 +343,11 @@ export class FocusTrap { } } - -/** Factory that allows easy instantiation of focus traps. */ +/** + * Factory that allows easy instantiation of focus traps. + * @deprecated Use `ConfigurableFocusTrapFactory` instead. + * @breaking-change for 11.0.0 Remove this class. + */ @Injectable({providedIn: 'root'}) export class FocusTrapFactory { private _document: Document; diff --git a/src/cdk/a11y/focus-trap/polyfill.ts b/src/cdk/a11y/focus-trap/polyfill.ts new file mode 100644 index 000000000000..cd89b3900251 --- /dev/null +++ b/src/cdk/a11y/focus-trap/polyfill.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 + */ + +/** IE 11 compatible closest implementation that is able to start from non-Element Nodes. */ +export function closest(element: EventTarget|Element|null|undefined, selector: string): + Element|null { + if (!(element instanceof Node)) { return null; } + + let curr: Node|null = element; + while (curr != null && !(curr instanceof Element)) { + curr = curr.parentNode; + } + + return curr && (hasNativeClosest ? + curr.closest(selector) : polyfillClosest(curr, selector)) as Element|null; +} + +/** Polyfill for browsers without Element.closest. */ +function polyfillClosest(element: Element, selector: string): Element|null { + let curr: Node|null = element; + while (curr != null && !(curr instanceof Element && matches(curr, selector))) { + curr = curr.parentNode; + } + + return (curr || null) as Element|null; +} + +const hasNativeClosest = typeof Element != 'undefined' && !!Element.prototype.closest; + +/** IE 11 compatible matches implementation. */ +function matches(element: Element, selector: string): boolean { + return element.matches ? + element.matches(selector) : + (element as any)['msMatchesSelector'](selector); +} diff --git a/src/cdk/a11y/public-api.ts b/src/cdk/a11y/public-api.ts index 26df94b5275e..cb70ff27f286 100644 --- a/src/cdk/a11y/public-api.ts +++ b/src/cdk/a11y/public-api.ts @@ -9,7 +9,11 @@ export * from './aria-describer/aria-describer'; export * from './key-manager/activedescendant-key-manager'; export * from './key-manager/focus-key-manager'; export * from './key-manager/list-key-manager'; +export * from './focus-trap/configurable-focus-trap'; +export * from './focus-trap/event-listener-inert-strategy'; export * from './focus-trap/focus-trap'; +export * from './focus-trap/configurable-focus-trap-factory'; +export * from './focus-trap/focus-trap-inert-strategy'; export * from './interactivity-checker/interactivity-checker'; export * from './live-announcer/live-announcer'; export * from './live-announcer/live-announcer-tokens'; diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index f04013f4cb9d..2e7d03b4752e 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -57,6 +57,29 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, DoChec static ɵfac: i0.ɵɵFactoryDef; } +export declare class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap { + get enabled(): boolean; + set enabled(value: boolean); + constructor(_element: HTMLElement, _checker: InteractivityChecker, _ngZone: NgZone, _document: Document, _focusTrapManager: FocusTrapManager, _inertStrategy: FocusTrapInertStrategy, config: ConfigurableFocusTrapConfig); + _disable(): void; + _enable(): void; + destroy(): void; +} + +export declare class ConfigurableFocusTrapFactory { + constructor(_checker: InteractivityChecker, _ngZone: NgZone, _focusTrapManager: FocusTrapManager, _document: any, _inertStrategy?: FocusTrapInertStrategy); + create(element: HTMLElement, config?: ConfigurableFocusTrapConfig): ConfigurableFocusTrap; + static ɵfac: i0.ɵɵFactoryDef; + static ɵprov: i0.ɵɵInjectableDef; +} + +export declare class EventListenerFocusTrapInertStrategy implements FocusTrapInertStrategy { + allowFocus(focusTrap: ConfigurableFocusTrap): void; + preventFocus(focusTrap: ConfigurableFocusTrap): void; +} + +export declare const FOCUS_TRAP_INERT_STRATEGY: InjectionToken; + export interface FocusableOption extends ListKeyManagerOption { focus(origin?: FocusOrigin): void; } @@ -88,6 +111,10 @@ export interface FocusOptions { export declare type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null; export declare class FocusTrap { + readonly _document: Document; + readonly _element: HTMLElement; + protected _enabled: boolean; + readonly _ngZone: NgZone; get enabled(): boolean; set enabled(value: boolean); protected endAnchorListener: () => boolean; @@ -102,6 +129,7 @@ export declare class FocusTrap { focusLastTabbableElement(): boolean; focusLastTabbableElementWhenReady(): Promise; hasAttached(): boolean; + protected toggleAnchors(enabled: boolean): void; } export declare class FocusTrapFactory { @@ -111,6 +139,11 @@ export declare class FocusTrapFactory { static ɵprov: i0.ɵɵInjectableDef; } +export interface FocusTrapInertStrategy { + allowFocus(focusTrap: FocusTrap): void; + preventFocus(focusTrap: FocusTrap): void; +} + export declare const enum HighContrastMode { NONE = 0, BLACK_ON_WHITE = 1,