Skip to content

Commit 62c9af3

Browse files
committed
feat(cdk/a11y): add focus escape function to configurable focus trap
Adds the ability to pass in a predicate function to the `ConfigurableFocusTrap` which will determine whether focus is allowed to escape. Relates to #21955.
1 parent 119684e commit 62c9af3

File tree

6 files changed

+41
-7
lines changed

6 files changed

+41
-7
lines changed

src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ export interface ConfigurableFocusTrapConfig {
1414
* Whether to defer the creation of FocusTrap elements to be done manually by the user.
1515
*/
1616
defer: boolean;
17+
18+
/** Predicate function that determines whether the focus trap will allow focus to escape. */
19+
focusEscapePredicate?: (target: HTMLElement) => boolean;
1720
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap
3131
}
3232
}
3333

34+
/** Determines whether focus is allowed to escape the trap. */
35+
focusEscapePredicate: (target: HTMLElement) => boolean;
36+
3437
constructor(
3538
_element: HTMLElement,
3639
_checker: InteractivityChecker,
@@ -41,6 +44,7 @@ export class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap
4144
config: ConfigurableFocusTrapConfig) {
4245
super(_element, _checker, _ngZone, _document, config.defer);
4346
this._focusTrapManager.register(this);
47+
this.focusEscapePredicate = config.focusEscapePredicate || (() => false);
4448
}
4549

4650
/** Notifies the FocusTrapManager that this FocusTrap will be destroyed. */

src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ describe('EventListenerFocusTrapInertStrategy', () => {
5656
'Expected second focusable element to be focused');
5757
}));
5858

59+
it('should allow focus to escape based on the result of a predicate function', fakeAsync(() => {
60+
const fixture = createComponent(SimpleFocusTrap, providers);
61+
const componentInstance = fixture.componentInstance;
62+
fixture.detectChanges();
63+
64+
componentInstance.focusTrap.focusEscapePredicate = () => true;
65+
componentInstance.outsideFocusableElement.nativeElement.focus();
66+
flush();
67+
68+
expect(componentInstance.activeElement).toBe(
69+
componentInstance.outsideFocusableElement.nativeElement,
70+
'Expected outside focusable element to be focused');
71+
}));
72+
5973
});
6074

6175
function createComponent<T>(componentType: Type<T>, providers: Provider[] = []):

src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export class EventListenerFocusTrapInertStrategy implements FocusTrapInertStrate
5353

5454
// Don't refocus if target was in an overlay, because the overlay might be associated
5555
// with an element inside the FocusTrap, ex. mat-select.
56-
if (!focusTrapRoot.contains(target) && closest(target, 'div.cdk-overlay-pane') === null) {
56+
if (target && !focusTrapRoot.contains(target) && !focusTrap.focusEscapePredicate(target) &&
57+
closest(target, 'div.cdk-overlay-pane') === null) {
5758
// Some legacy FocusTrap usages have logic that focuses some element on the page
5859
// just before FocusTrap is destroyed. For backwards compatibility, wait
5960
// to be sure FocusTrap is still enabled before refocusing.

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '@angular/core';
2424
import {take} from 'rxjs/operators';
2525
import {InteractivityChecker} from '../interactivity-checker/interactivity-checker';
26+
import {ConfigurableFocusTrapConfig} from './configurable-focus-trap-config';
2627

2728

2829
/**
@@ -372,13 +373,21 @@ export class FocusTrapFactory {
372373
/**
373374
* Creates a focus-trapped region around the given element.
374375
* @param element The element around which focus will be trapped.
375-
* @param deferCaptureElements Defers the creation of focus-capturing elements to be done
376-
* manually by the user.
376+
* @param config The focus trap configuration.
377377
* @returns The created focus trap instance.
378378
*/
379-
create(element: HTMLElement, deferCaptureElements: boolean = false): FocusTrap {
380-
return new FocusTrap(
381-
element, this._checker, this._ngZone, this._document, deferCaptureElements);
379+
create(element: HTMLElement, config?: ConfigurableFocusTrapConfig): FocusTrap;
380+
381+
/**
382+
* @deprecated Pass a config object instead of the `deferCaptureElements` flag.
383+
* @breaking-change 11.0.0
384+
*/
385+
create(element: HTMLElement, deferCaptureElements: boolean): FocusTrap;
386+
387+
create(element: HTMLElement,
388+
configOrDefer: ConfigurableFocusTrapConfig|boolean = false): FocusTrap {
389+
return new FocusTrap(element, this._checker, this._ngZone, this._document,
390+
typeof configOrDefer === 'boolean' ? configOrDefer : configOrDefer.defer);
382391
}
383392
}
384393

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChan
6565
export declare class ConfigurableFocusTrap extends FocusTrap implements ManagedFocusTrap {
6666
get enabled(): boolean;
6767
set enabled(value: boolean);
68+
focusEscapePredicate: (target: HTMLElement) => boolean;
6869
constructor(_element: HTMLElement, _checker: InteractivityChecker, _ngZone: NgZone, _document: Document, _focusTrapManager: FocusTrapManager, _inertStrategy: FocusTrapInertStrategy, config: ConfigurableFocusTrapConfig);
6970
_disable(): void;
7071
_enable(): void;
@@ -73,6 +74,7 @@ export declare class ConfigurableFocusTrap extends FocusTrap implements ManagedF
7374

7475
export interface ConfigurableFocusTrapConfig {
7576
defer: boolean;
77+
focusEscapePredicate?: (target: HTMLElement) => boolean;
7678
}
7779

7880
export declare class ConfigurableFocusTrapFactory {
@@ -157,7 +159,8 @@ export declare class FocusTrap {
157159

158160
export declare class FocusTrapFactory {
159161
constructor(_checker: InteractivityChecker, _ngZone: NgZone, _document: any);
160-
create(element: HTMLElement, deferCaptureElements?: boolean): FocusTrap;
162+
create(element: HTMLElement, config?: ConfigurableFocusTrapConfig): FocusTrap;
163+
create(element: HTMLElement, deferCaptureElements: boolean): FocusTrap;
161164
static ɵfac: i0.ɵɵFactoryDeclaration<FocusTrapFactory, never>;
162165
static ɵprov: i0.ɵɵInjectableDef<FocusTrapFactory>;
163166
}

0 commit comments

Comments
 (0)