mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
mmalerba
parent
a190de7c43
commit
7626c51930
@@ -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
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user