Skip to content

fix: add predicate function to configurable focus trap and allow focus to escape sidenav #21962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cdk/a11y/focus-trap/configurable-focus-trap-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name bikesheding: WDYT about allowFocusEscape?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too attached to the name, but I wanted to have "predicate" in there since we do the same in a few other places. To me, allowFocusEscape sounds like something you'd call on the focus trap to allow focus to escape, e.g. focusTrap.allowFocusEscape().

}
4 changes: 4 additions & 0 deletions src/cdk/a11y/focus-trap/configurable-focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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. */
Expand Down
14 changes: 14 additions & 0 deletions src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(componentType: Type<T>, providers: Provider[] = []):
Expand Down
3 changes: 2 additions & 1 deletion src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 14 additions & 5 deletions src/cdk/a11y/focus-trap/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


/**
Expand Down Expand Up @@ -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);
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/material/sidenav/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -593,7 +596,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
}

constructor(@Optional() private _dir: Directionality,
private _element: ElementRef<HTMLElement>,
public _element: ElementRef<HTMLElement>,
private _ngZone: NgZone,
private _changeDetectorRef: ChangeDetectorRef,
viewportRuler: ViewportRuler,
Expand Down
5 changes: 4 additions & 1 deletion tools/public_api_guard/cdk/a11y.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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<FocusTrapFactory, never>;
static ɵprov: i0.ɵɵInjectableDef<FocusTrapFactory>;
}
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/material/sidenav.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export declare class MatDrawerContainer implements AfterContentInit, DoCheck, On
right: number | null;
};
_drawers: QueryList<MatDrawer>;
_element: ElementRef<HTMLElement>;
_userContent: MatDrawerContent;
get autosize(): boolean;
set autosize(value: boolean);
Expand Down