Skip to content

Commit cb8226a

Browse files
committed
feat(focus-trap): add the ability to specify a focus target
Adds the ability to specify an element that should take precedence over other focusable elements inside of a focus trap. Fixes #1468.
1 parent a0d85d8 commit cb8226a

File tree

2 files changed

+62
-12
lines changed

2 files changed

+62
-12
lines changed

src/lib/core/a11y/focus-trap.spec.ts

+46-10
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,21 @@ import {InteractivityChecker} from './interactivity-checker';
66

77

88
describe('FocusTrap', () => {
9-
let checker: InteractivityChecker;
10-
let fixture: ComponentFixture<FocusTrapTestApp>;
11-
129
describe('with default element', () => {
10+
let fixture: ComponentFixture<FocusTrapTestApp>;
11+
let focusTrapInstance: FocusTrap;
12+
1313
beforeEach(() => TestBed.configureTestingModule({
1414
declarations: [FocusTrap, FocusTrapTestApp],
1515
providers: [InteractivityChecker]
1616
}));
1717

1818
beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
19-
checker = c;
2019
fixture = TestBed.createComponent(FocusTrapTestApp);
20+
focusTrapInstance = fixture.debugElement.query(By.directive(FocusTrap)).componentInstance;
2121
}));
2222

2323
it('wrap focus from end to start', () => {
24-
let focusTrap = fixture.debugElement.query(By.directive(FocusTrap));
25-
let focusTrapInstance = focusTrap.componentInstance as FocusTrap;
26-
2724
// Because we can't mimic a real tab press focus change in a unit test, just call the
2825
// focus event handler directly.
2926
focusTrapInstance.focusFirstTabbableElement();
@@ -33,9 +30,6 @@ describe('FocusTrap', () => {
3330
});
3431

3532
it('should wrap focus from start to end', () => {
36-
let focusTrap = fixture.debugElement.query(By.directive(FocusTrap));
37-
let focusTrapInstance = focusTrap.componentInstance as FocusTrap;
38-
3933
// Because we can't mimic a real tab press focus change in a unit test, just call the
4034
// focus event handler directly.
4135
focusTrapInstance.focusLastTabbableElement();
@@ -44,6 +38,35 @@ describe('FocusTrap', () => {
4438
.toBe('button', 'Expected button element to be focused');
4539
});
4640
});
41+
42+
describe('with focus targets', () => {
43+
let fixture: ComponentFixture<FocusTrapTargetTestApp>;
44+
let focusTrapInstance: FocusTrap;
45+
46+
beforeEach(() => TestBed.configureTestingModule({
47+
declarations: [FocusTrap, FocusTrapTargetTestApp],
48+
providers: [InteractivityChecker]
49+
}));
50+
51+
beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
52+
fixture = TestBed.createComponent(FocusTrapTargetTestApp);
53+
focusTrapInstance = fixture.debugElement.query(By.directive(FocusTrap)).componentInstance;
54+
}));
55+
56+
it('should be able to prioritize the first focus target', () => {
57+
// Because we can't mimic a real tab press focus change in a unit test, just call the
58+
// focus event handler directly.
59+
focusTrapInstance.focusFirstTabbableElement();
60+
expect(document.activeElement.id).toBe('first');
61+
});
62+
63+
it('should be able to prioritize the last focus target', () => {
64+
// Because we can't mimic a real tab press focus change in a unit test, just call the
65+
// focus event handler directly.
66+
focusTrapInstance.focusLastTabbableElement();
67+
expect(document.activeElement.id).toBe('last');
68+
});
69+
});
4770
});
4871

4972

@@ -56,3 +79,16 @@ describe('FocusTrap', () => {
5679
`
5780
})
5881
class FocusTrapTestApp { }
82+
83+
84+
@Component({
85+
template: `
86+
<focus-trap>
87+
<input>
88+
<button id="last" md-focus-end></button>
89+
<button id="first" md-focus-start>SAVE</button>
90+
<input>
91+
</focus-trap>
92+
`
93+
})
94+
class FocusTrapTargetTestApp { }

src/lib/core/a11y/focus-trap.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Component, ViewEncapsulation, ViewChild, ElementRef} from '@angular/core';
22
import {InteractivityChecker} from './interactivity-checker';
33

4+
/** Selector for nodes that should have a higher priority when looking for focus targets. */
5+
const FOCUS_TARGET_SELECTOR = '[md-focus-target]';
46

57
/**
68
* Directive for trapping focus within a region.
@@ -27,15 +29,27 @@ export class FocusTrap {
2729

2830
/** Focuses the first tabbable element within the focus trap region. */
2931
focusFirstTabbableElement() {
30-
let redirectToElement = this._getFirstTabbableElement(this.trappedContent.nativeElement);
32+
let rootElement = this.trappedContent.nativeElement;
33+
let redirectToElement = rootElement.querySelector('[md-focus-start]') as HTMLElement ||
34+
this._getFirstTabbableElement(rootElement);
35+
3136
if (redirectToElement) {
3237
redirectToElement.focus();
3338
}
3439
}
3540

3641
/** Focuses the last tabbable element within the focus trap region. */
3742
focusLastTabbableElement() {
38-
let redirectToElement = this._getLastTabbableElement(this.trappedContent.nativeElement);
43+
let rootElement = this.trappedContent.nativeElement;
44+
let focusTargets = rootElement.querySelectorAll('[md-focus-end]');
45+
let redirectToElement: HTMLElement = null;
46+
47+
if (focusTargets.length) {
48+
redirectToElement = focusTargets[focusTargets.length - 1] as HTMLElement;
49+
} else {
50+
redirectToElement = this._getLastTabbableElement(rootElement);
51+
}
52+
3953
if (redirectToElement) {
4054
redirectToElement.focus();
4155
}

0 commit comments

Comments
 (0)