diff --git a/src/cdk/a11y/focus-trap.spec.ts b/src/cdk/a11y/focus-trap.spec.ts index 66e42601212b..28d07e46842c 100644 --- a/src/cdk/a11y/focus-trap.spec.ts +++ b/src/cdk/a11y/focus-trap.spec.ts @@ -15,6 +15,7 @@ describe('FocusTrap', () => { SimpleFocusTrap, FocusTrapTargets, FocusTrapWithSvg, + FocusTrapWithoutFocusableElements, ], providers: [InteractivityChecker, Platform, FocusTrapFactory] }); @@ -35,22 +36,37 @@ describe('FocusTrap', () => { it('wrap focus from end to start', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. - focusTrapInstance.focusFirstTabbableElement(); + const result = focusTrapInstance.focusFirstTabbableElement(); expect(document.activeElement.nodeName.toLowerCase()) .toBe('input', 'Expected input element to be focused'); + expect(result).toBe(true, 'Expected return value to be true if focus was shifted.'); }); it('should wrap focus from start to end', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. - focusTrapInstance.focusLastTabbableElement(); + const result = focusTrapInstance.focusLastTabbableElement(); // In iOS button elements are never tabbable, so the last element will be the input. - let lastElement = new Platform().IOS ? 'input' : 'button'; + const lastElement = new Platform().IOS ? 'input' : 'button'; expect(document.activeElement.nodeName.toLowerCase()) .toBe(lastElement, `Expected ${lastElement} element to be focused`); + + expect(result).toBe(true, 'Expected return value to be true if focus was shifted.'); + }); + + it('should return false if it did not manage to find a focusable element', () => { + fixture.destroy(); + + const newFixture = TestBed.createComponent(FocusTrapWithoutFocusableElements); + newFixture.detectChanges(); + + const focusTrap = newFixture.componentInstance.focusTrapDirective.focusTrap; + const result = focusTrap.focusFirstTabbableElement(); + + expect(result).toBe(false); }); it('should be enabled by default', () => { @@ -199,3 +215,14 @@ class FocusTrapTargets { class FocusTrapWithSvg { @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective; } + +@Component({ + template: ` +
+

Hello

+
+ ` +}) +class FocusTrapWithoutFocusableElements { + @ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective; +} diff --git a/src/cdk/a11y/focus-trap.ts b/src/cdk/a11y/focus-trap.ts index a4045fccd58f..cb09557accba 100644 --- a/src/cdk/a11y/focus-trap.ts +++ b/src/cdk/a11y/focus-trap.ts @@ -88,8 +88,13 @@ export class FocusTrap { } this._ngZone.runOutsideAngular(() => { - this._startAnchor!.addEventListener('focus', () => this.focusLastTabbableElement()); - this._endAnchor!.addEventListener('focus', () => this.focusFirstTabbableElement()); + this._startAnchor!.addEventListener('focus', () => { + this.focusLastTabbableElement(); + }); + + this._endAnchor!.addEventListener('focus', () => { + this.focusFirstTabbableElement(); + }); if (this._element.parentNode) { this._element.parentNode.insertBefore(this._startAnchor!, this._element); @@ -100,26 +105,38 @@ export class FocusTrap { /** * Waits for the zone to stabilize, then either focuses the first element that the - * user specified, or the first tabbable element.. + * user specified, or the first tabbable element. + * @returns Returns a promise that resolves with a boolean, depending + * on whether focus was moved successfuly. */ - focusInitialElementWhenReady() { - this._executeOnStable(() => this.focusInitialElement()); + focusInitialElementWhenReady(): Promise { + return new Promise(resolve => { + this._executeOnStable(() => resolve(this.focusInitialElement())); + }); } /** * Waits for the zone to stabilize, then focuses * the first tabbable element within the focus trap region. + * @returns Returns a promise that resolves with a boolean, depending + * on whether focus was moved successfuly. */ - focusFirstTabbableElementWhenReady() { - this._executeOnStable(() => this.focusFirstTabbableElement()); + focusFirstTabbableElementWhenReady(): Promise { + return new Promise(resolve => { + this._executeOnStable(() => resolve(this.focusFirstTabbableElement())); + }); } /** * Waits for the zone to stabilize, then focuses * the last tabbable element within the focus trap region. + * @returns Returns a promise that resolves with a boolean, depending + * on whether focus was moved successfuly. */ - focusLastTabbableElementWhenReady() { - this._executeOnStable(() => this.focusLastTabbableElement()); + focusLastTabbableElementWhenReady(): Promise { + return new Promise(resolve => { + this._executeOnStable(() => resolve(this.focusLastTabbableElement())); + }); } /** @@ -146,30 +163,47 @@ export class FocusTrap { markers[markers.length - 1] : this._getLastTabbableElement(this._element); } - /** Focuses the element that should be focused when the focus trap is initialized. */ - focusInitialElement() { - let redirectToElement = this._element.querySelector('[cdk-focus-initial]') as HTMLElement; + /** + * Focuses the element that should be focused when the focus trap is initialized. + * @returns Returns whether focus was moved successfuly. + */ + focusInitialElement(): boolean { + const redirectToElement = this._element.querySelector('[cdk-focus-initial]') as HTMLElement; + if (redirectToElement) { redirectToElement.focus(); - } else { - this.focusFirstTabbableElement(); + return true; } + + return this.focusFirstTabbableElement(); } - /** Focuses the first tabbable element within the focus trap region. */ - focusFirstTabbableElement() { - let redirectToElement = this._getRegionBoundary('start'); + /** + * Focuses the first tabbable element within the focus trap region. + * @returns Returns whether focus was moved successfuly. + */ + focusFirstTabbableElement(): boolean { + const redirectToElement = this._getRegionBoundary('start'); + if (redirectToElement) { redirectToElement.focus(); } + + return !!redirectToElement; } - /** Focuses the last tabbable element within the focus trap region. */ - focusLastTabbableElement() { - let redirectToElement = this._getRegionBoundary('end'); + /** + * Focuses the last tabbable element within the focus trap region. + * @returns Returns whether focus was moved successfuly. + */ + focusLastTabbableElement(): boolean { + const redirectToElement = this._getRegionBoundary('end'); + if (redirectToElement) { redirectToElement.focus(); } + + return !!redirectToElement; } /** Get the first tabbable element from a DOM subtree (inclusive). */