Skip to content

Commit

Permalink
feat(cdk/a11y): allow focus options to be passed in to focus trap (an…
Browse files Browse the repository at this point in the history
…gular#21769)

Allows for an optional `FocusOptions` object to be passed into the various focus
trap methods.

Fixes angular#21767.
  • Loading branch information
crisbeto authored and wagnermaciel committed Feb 8, 2021
1 parent ce78de5 commit abc8eec
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 20 deletions.
24 changes: 24 additions & 0 deletions src/cdk/a11y/focus-trap/focus-trap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,44 @@ 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.
focusTrapInstance.focusFirstTabbableElement();
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.
focusTrapInstance.focusLastTabbableElement();
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();
Expand Down
28 changes: 14 additions & 14 deletions src/cdk/a11y/focus-trap/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
focusInitialElementWhenReady(options?: FocusOptions): Promise<boolean> {
return new Promise<boolean>(resolve => {
this._executeOnStable(() => resolve(this.focusInitialElement()));
this._executeOnStable(() => resolve(this.focusInitialElement(options)));
});
}

Expand All @@ -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<boolean> {
focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise<boolean> {
return new Promise<boolean>(resolve => {
this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options)));
});
}

Expand All @@ -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<boolean> {
focusLastTabbableElementWhenReady(options?: FocusOptions): Promise<boolean> {
return new Promise<boolean>(resolve => {
this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
this._executeOnStable(() => resolve(this.focusLastTabbableElement(options)));
});
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions tools/public_api_guard/cdk/a11y.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
focusInitialElement(): boolean;
focusInitialElementWhenReady(): Promise<boolean>;
focusLastTabbableElement(): boolean;
focusLastTabbableElementWhenReady(): Promise<boolean>;
focusFirstTabbableElement(options?: FocusOptions): boolean;
focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise<boolean>;
focusInitialElement(options?: FocusOptions): boolean;
focusInitialElementWhenReady(options?: FocusOptions): Promise<boolean>;
focusLastTabbableElement(options?: FocusOptions): boolean;
focusLastTabbableElementWhenReady(options?: FocusOptions): Promise<boolean>;
hasAttached(): boolean;
protected toggleAnchors(enabled: boolean): void;
}
Expand Down

0 comments on commit abc8eec

Please sign in to comment.