feat(focus-trap): return whether shifting focus was successful (#6279)

Adds return types to the focus trap methods, allowing the consumer to react based on whether the focus trap managed to find a focusable element. This is useful for cases like the dialog where focus could be left behind if it is a purely text-based dialog.
This commit is contained in:
Kristiyan Kostadinov
2017-08-08 22:19:48 +02:00
committed by mmalerba
parent a190de7c43
commit 7626c51930
2 changed files with 84 additions and 23 deletions
+30 -3
View File
@@ -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: `
<div cdkTrapFocus>
<p>Hello</p>
</div>
`
})
class FocusTrapWithoutFocusableElements {
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
}
+54 -20
View File
@@ -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<boolean> {
return new Promise<boolean>(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<boolean> {
return new Promise<boolean>(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<boolean> {
return new Promise<boolean>(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). */