diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts index 0831d377c780..fc0960af5252 100644 --- a/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts @@ -14,4 +14,7 @@ export interface ConfigurableFocusTrapConfig { * Whether to defer the creation of FocusTrap elements to be done manually by the user. */ defer: boolean; + + /** Predicate function that determines whether the focus trap will allow focus to escape. */ + focusEscapePredicate?: (target: HTMLElement) => boolean; } diff --git a/src/cdk/a11y/focus-trap/configurable-focus-trap.ts b/src/cdk/a11y/focus-trap/configurable-focus-trap.ts index 8d5c8f10fb13..489bf9cfd975 100644 --- a/src/cdk/a11y/focus-trap/configurable-focus-trap.ts +++ b/src/cdk/a11y/focus-trap/configurable-focus-trap.ts @@ -31,6 +31,9 @@ export class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap } } + /** Determines whether focus is allowed to escape the trap. */ + focusEscapePredicate: (target: HTMLElement) => boolean; + constructor( _element: HTMLElement, _checker: InteractivityChecker, @@ -41,6 +44,7 @@ export class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap config: ConfigurableFocusTrapConfig) { super(_element, _checker, _ngZone, _document, config.defer); this._focusTrapManager.register(this); + this.focusEscapePredicate = config.focusEscapePredicate || (() => false); } /** Notifies the FocusTrapManager that this FocusTrap will be destroyed. */ 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 index 61be797a67ef..a555fe72f4fb 100644 --- a/src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts +++ b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts @@ -56,6 +56,20 @@ describe('EventListenerFocusTrapInertStrategy', () => { 'Expected second focusable element to be focused'); })); + it('should allow focus to escape based on the result of a predicate function', fakeAsync(() => { + const fixture = createComponent(SimpleFocusTrap, providers); + const componentInstance = fixture.componentInstance; + fixture.detectChanges(); + + componentInstance.focusTrap.focusEscapePredicate = () => true; + componentInstance.outsideFocusableElement.nativeElement.focus(); + flush(); + + expect(componentInstance.activeElement).toBe( + componentInstance.outsideFocusableElement.nativeElement, + 'Expected outside focusable element to be focused'); + })); + }); function createComponent(componentType: Type, providers: Provider[] = []): diff --git a/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts index 5c5be8db7478..f04f23ebf447 100644 --- a/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts +++ b/src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts @@ -53,7 +53,8 @@ export class EventListenerFocusTrapInertStrategy implements FocusTrapInertStrate // 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 (!focusTrapRoot.contains(target) && closest(target, 'div.cdk-overlay-pane') === null) { + if (target && !focusTrapRoot.contains(target) && !focusTrap.focusEscapePredicate(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. diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index d7432b33a9ae..2107fd0d1354 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -23,6 +23,7 @@ import { } from '@angular/core'; import {take} from 'rxjs/operators'; import {InteractivityChecker} from '../interactivity-checker/interactivity-checker'; +import {ConfigurableFocusTrapConfig} from './configurable-focus-trap-config'; /** @@ -372,13 +373,21 @@ export class FocusTrapFactory { /** * 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. + * @param config The focus trap configuration. * @returns The created focus trap instance. */ - create(element: HTMLElement, deferCaptureElements: boolean = false): FocusTrap { - return new FocusTrap( - element, this._checker, this._ngZone, this._document, deferCaptureElements); + create(element: HTMLElement, config?: ConfigurableFocusTrapConfig): FocusTrap; + + /** + * @deprecated Pass a config object instead of the `deferCaptureElements` flag. + * @breaking-change 11.0.0 + */ + create(element: HTMLElement, deferCaptureElements: boolean): FocusTrap; + + create(element: HTMLElement, + configOrDefer: ConfigurableFocusTrapConfig|boolean = false): FocusTrap { + return new FocusTrap(element, this._checker, this._ngZone, this._document, + typeof configOrDefer === 'boolean' ? configOrDefer : configOrDefer.defer); } } diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 17804b88a89d..94e0b02e0533 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -354,7 +354,10 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr } ngAfterContentInit() { - this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, { + defer: false, + focusEscapePredicate: target => !!this._container?._element.nativeElement.contains(target) + }); this._updateFocusTrapState(); } @@ -593,7 +596,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy } constructor(@Optional() private _dir: Directionality, - private _element: ElementRef, + public _element: ElementRef, private _ngZone: NgZone, private _changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index a0a3fb03d7b9..149c5bf4b7fa 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -65,6 +65,7 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChan export declare class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap { get enabled(): boolean; set enabled(value: boolean); + focusEscapePredicate: (target: HTMLElement) => boolean; constructor(_element: HTMLElement, _checker: InteractivityChecker, _ngZone: NgZone, _document: Document, _focusTrapManager: FocusTrapManager, _inertStrategy: FocusTrapInertStrategy, config: ConfigurableFocusTrapConfig); _disable(): void; _enable(): void; @@ -73,6 +74,7 @@ export declare class ConfigurableFocusTrap extends FocusTrap implements ManagedF export interface ConfigurableFocusTrapConfig { defer: boolean; + focusEscapePredicate?: (target: HTMLElement) => boolean; } export declare class ConfigurableFocusTrapFactory { @@ -157,7 +159,8 @@ export declare class FocusTrap { export declare class FocusTrapFactory { constructor(_checker: InteractivityChecker, _ngZone: NgZone, _document: any); - create(element: HTMLElement, deferCaptureElements?: boolean): FocusTrap; + create(element: HTMLElement, config?: ConfigurableFocusTrapConfig): FocusTrap; + create(element: HTMLElement, deferCaptureElements: boolean): FocusTrap; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵprov: i0.ɵɵInjectableDef; } diff --git a/tools/public_api_guard/material/sidenav.d.ts b/tools/public_api_guard/material/sidenav.d.ts index dfa4945d4eec..66b823177ed1 100644 --- a/tools/public_api_guard/material/sidenav.d.ts +++ b/tools/public_api_guard/material/sidenav.d.ts @@ -59,6 +59,7 @@ export declare class MatDrawerContainer implements AfterContentInit, DoCheck, On right: number | null; }; _drawers: QueryList; + _element: ElementRef; _userContent: MatDrawerContent; get autosize(): boolean; set autosize(value: boolean);