Skip to content

Commit 87ebbd6

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 87ebbd6

File tree

2 files changed

+60
-10
lines changed

2 files changed

+60
-10
lines changed

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

Lines changed: 46 additions & 10 deletions
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="first" md-focus-target>SAVE</button>
89+
<button id="last" md-focus-target></button>
90+
<input>
91+
</focus-trap>
92+
`
93+
})
94+
class FocusTrapTargetTestApp { }

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

Lines changed: 14 additions & 0 deletions
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.
@@ -47,6 +49,12 @@ export class FocusTrap {
4749
return root;
4850
}
4951

52+
let focusTarget = root.querySelector(FOCUS_TARGET_SELECTOR) as HTMLElement;
53+
54+
if (focusTarget) {
55+
return focusTarget;
56+
}
57+
5058
// Iterate in DOM order.
5159
let childCount = root.children.length;
5260
for (let i = 0; i < childCount; i++) {
@@ -65,6 +73,12 @@ export class FocusTrap {
6573
return root;
6674
}
6775

76+
let focusTargets = root.querySelectorAll(FOCUS_TARGET_SELECTOR);
77+
78+
if (focusTargets.length) {
79+
return focusTargets[focusTargets.length - 1] as HTMLElement;
80+
}
81+
6882
// Iterate in reverse DOM order.
6983
for (let i = root.children.length - 1; i >= 0; i--) {
7084
let tabbableChild = this._getLastTabbableElement(root.children[i] as HTMLElement);

0 commit comments

Comments
 (0)