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: `
+
+ `
+})
+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). */