From abc8eec4dabb74b89b34a4ee72c1c370832c861e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 5 Feb 2021 10:54:23 +0100 Subject: [PATCH] feat(cdk/a11y): allow focus options to be passed in to focus trap (#21769) Allows for an optional `FocusOptions` object to be passed into the various focus trap methods. Fixes #21767. --- src/cdk/a11y/focus-trap/focus-trap.spec.ts | 24 +++++++++++++++++++ src/cdk/a11y/focus-trap/focus-trap.ts | 28 +++++++++++----------- tools/public_api_guard/cdk/a11y.d.ts | 12 +++++----- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/cdk/a11y/focus-trap/focus-trap.spec.ts b/src/cdk/a11y/focus-trap/focus-trap.spec.ts index 2f9173fbcded..ccdec39bbad3 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.spec.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.spec.ts @@ -129,6 +129,14 @@ describe('FocusTrap', () => { expect(document.activeElement!.id).toBe('middle'); }); + it('should be able to pass in focus options to initial focusable element', () => { + const options = {preventScroll: true}; + const spy = spyOn(fixture.nativeElement.querySelector('#middle'), 'focus').and.callThrough(); + + focusTrapInstance.focusInitialElement(options); + expect(spy).toHaveBeenCalledWith(options); + }); + it('should be able to prioritize the first focus target', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. @@ -136,6 +144,14 @@ describe('FocusTrap', () => { expect(document.activeElement!.id).toBe('first'); }); + it('should be able to pass in focus options to first focusable element', () => { + const options = {preventScroll: true}; + const spy = spyOn(fixture.nativeElement.querySelector('#first'), 'focus').and.callThrough(); + + focusTrapInstance.focusFirstTabbableElement(options); + expect(spy).toHaveBeenCalledWith(options); + }); + it('should be able to prioritize the last focus target', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. @@ -143,6 +159,14 @@ describe('FocusTrap', () => { expect(document.activeElement!.id).toBe('last'); }); + it('should be able to pass in focus options to last focusable element', () => { + const options = {preventScroll: true}; + const spy = spyOn(fixture.nativeElement.querySelector('#last'), 'focus').and.callThrough(); + + focusTrapInstance.focusLastTabbableElement(options); + expect(spy).toHaveBeenCalledWith(options); + }); + it('should warn if the initial focus target is not focusable', () => { const alternateFixture = TestBed.createComponent(FocusTrapUnfocusableTarget); alternateFixture.detectChanges(); diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index 0bbe0a14438b..788c0e2cd716 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -132,9 +132,9 @@ export class FocusTrap { * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ - focusInitialElementWhenReady(): Promise { + focusInitialElementWhenReady(options?: FocusOptions): Promise { return new Promise(resolve => { - this._executeOnStable(() => resolve(this.focusInitialElement())); + this._executeOnStable(() => resolve(this.focusInitialElement(options))); }); } @@ -144,9 +144,9 @@ export class FocusTrap { * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ - focusFirstTabbableElementWhenReady(): Promise { + focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise { return new Promise(resolve => { - this._executeOnStable(() => resolve(this.focusFirstTabbableElement())); + this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options))); }); } @@ -156,9 +156,9 @@ export class FocusTrap { * @returns Returns a promise that resolves with a boolean, depending * on whether focus was moved successfully. */ - focusLastTabbableElementWhenReady(): Promise { + focusLastTabbableElementWhenReady(options?: FocusOptions): Promise { return new Promise(resolve => { - this._executeOnStable(() => resolve(this.focusLastTabbableElement())); + this._executeOnStable(() => resolve(this.focusLastTabbableElement(options))); }); } @@ -197,7 +197,7 @@ export class FocusTrap { * Focuses the element that should be focused when the focus trap is initialized. * @returns Whether focus was moved successfully. */ - focusInitialElement(): boolean { + focusInitialElement(options?: FocusOptions): boolean { // Contains the deprecated version of selector, for temporary backwards comparability. const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` + `[cdkFocusInitial]`) as HTMLElement; @@ -219,26 +219,26 @@ export class FocusTrap { if (!this._checker.isFocusable(redirectToElement)) { const focusableChild = this._getFirstTabbableElement(redirectToElement) as HTMLElement; - focusableChild?.focus(); + focusableChild?.focus(options); return !!focusableChild; } - redirectToElement.focus(); + redirectToElement.focus(options); return true; } - return this.focusFirstTabbableElement(); + return this.focusFirstTabbableElement(options); } /** * Focuses the first tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ - focusFirstTabbableElement(): boolean { + focusFirstTabbableElement(options?: FocusOptions): boolean { const redirectToElement = this._getRegionBoundary('start'); if (redirectToElement) { - redirectToElement.focus(); + redirectToElement.focus(options); } return !!redirectToElement; @@ -248,11 +248,11 @@ export class FocusTrap { * Focuses the last tabbable element within the focus trap region. * @returns Whether focus was moved successfully. */ - focusLastTabbableElement(): boolean { + focusLastTabbableElement(options?: FocusOptions): boolean { const redirectToElement = this._getRegionBoundary('end'); if (redirectToElement) { - redirectToElement.focus(); + redirectToElement.focus(options); } return !!redirectToElement; diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index 69246c1732aa..7f8d4b174337 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -140,12 +140,12 @@ export declare class FocusTrap { constructor(_element: HTMLElement, _checker: InteractivityChecker, _ngZone: NgZone, _document: Document, deferAnchors?: boolean); attachAnchors(): boolean; destroy(): void; - focusFirstTabbableElement(): boolean; - focusFirstTabbableElementWhenReady(): Promise; - focusInitialElement(): boolean; - focusInitialElementWhenReady(): Promise; - focusLastTabbableElement(): boolean; - focusLastTabbableElementWhenReady(): Promise; + focusFirstTabbableElement(options?: FocusOptions): boolean; + focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise; + focusInitialElement(options?: FocusOptions): boolean; + focusInitialElementWhenReady(options?: FocusOptions): Promise; + focusLastTabbableElement(options?: FocusOptions): boolean; + focusLastTabbableElementWhenReady(options?: FocusOptions): Promise; hasAttached(): boolean; protected toggleAnchors(enabled: boolean): void; }