Skip to content

Commit

Permalink
feat(focus-trap): add the ability to specify a focus target
Browse files Browse the repository at this point in the history
Adds the ability to specify an element that should take precedence over other focusable elements inside of a focus trap.

Fixes angular#1468.
  • Loading branch information
crisbeto committed Nov 6, 2016
1 parent a0d85d8 commit 87ebbd6
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 10 deletions.
56 changes: 46 additions & 10 deletions src/lib/core/a11y/focus-trap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,21 @@ import {InteractivityChecker} from './interactivity-checker';


describe('FocusTrap', () => {
let checker: InteractivityChecker;
let fixture: ComponentFixture<FocusTrapTestApp>;

describe('with default element', () => {
let fixture: ComponentFixture<FocusTrapTestApp>;
let focusTrapInstance: FocusTrap;

beforeEach(() => TestBed.configureTestingModule({
declarations: [FocusTrap, FocusTrapTestApp],
providers: [InteractivityChecker]
}));

beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
checker = c;
fixture = TestBed.createComponent(FocusTrapTestApp);
focusTrapInstance = fixture.debugElement.query(By.directive(FocusTrap)).componentInstance;
}));

it('wrap focus from end to start', () => {
let focusTrap = fixture.debugElement.query(By.directive(FocusTrap));
let focusTrapInstance = focusTrap.componentInstance as FocusTrap;

// Because we can't mimic a real tab press focus change in a unit test, just call the
// focus event handler directly.
focusTrapInstance.focusFirstTabbableElement();
Expand All @@ -33,9 +30,6 @@ describe('FocusTrap', () => {
});

it('should wrap focus from start to end', () => {
let focusTrap = fixture.debugElement.query(By.directive(FocusTrap));
let focusTrapInstance = focusTrap.componentInstance as FocusTrap;

// Because we can't mimic a real tab press focus change in a unit test, just call the
// focus event handler directly.
focusTrapInstance.focusLastTabbableElement();
Expand All @@ -44,6 +38,35 @@ describe('FocusTrap', () => {
.toBe('button', 'Expected button element to be focused');
});
});

describe('with focus targets', () => {
let fixture: ComponentFixture<FocusTrapTargetTestApp>;
let focusTrapInstance: FocusTrap;

beforeEach(() => TestBed.configureTestingModule({
declarations: [FocusTrap, FocusTrapTargetTestApp],
providers: [InteractivityChecker]
}));

beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
fixture = TestBed.createComponent(FocusTrapTargetTestApp);
focusTrapInstance = fixture.debugElement.query(By.directive(FocusTrap)).componentInstance;
}));

it('should be able to prioritize the first focus target', () => {
// Because we can't mimic a real tab press focus change in a unit test, just call the
// focus event handler directly.
focusTrapInstance.focusFirstTabbableElement();
expect(document.activeElement.id).toBe('first');
});

it('should be able to prioritize the last focus target', () => {
// Because we can't mimic a real tab press focus change in a unit test, just call the
// focus event handler directly.
focusTrapInstance.focusLastTabbableElement();
expect(document.activeElement.id).toBe('last');
});
});
});


Expand All @@ -56,3 +79,16 @@ describe('FocusTrap', () => {
`
})
class FocusTrapTestApp { }


@Component({
template: `
<focus-trap>
<input>
<button id="first" md-focus-target>SAVE</button>
<button id="last" md-focus-target></button>
<input>
</focus-trap>
`
})
class FocusTrapTargetTestApp { }
14 changes: 14 additions & 0 deletions src/lib/core/a11y/focus-trap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Component, ViewEncapsulation, ViewChild, ElementRef} from '@angular/core';
import {InteractivityChecker} from './interactivity-checker';

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

/**
* Directive for trapping focus within a region.
Expand Down Expand Up @@ -47,6 +49,12 @@ export class FocusTrap {
return root;
}

let focusTarget = root.querySelector(FOCUS_TARGET_SELECTOR) as HTMLElement;

if (focusTarget) {
return focusTarget;
}

// Iterate in DOM order.
let childCount = root.children.length;
for (let i = 0; i < childCount; i++) {
Expand All @@ -65,6 +73,12 @@ export class FocusTrap {
return root;
}

let focusTargets = root.querySelectorAll(FOCUS_TARGET_SELECTOR);

if (focusTargets.length) {
return focusTargets[focusTargets.length - 1] as HTMLElement;
}

// Iterate in reverse DOM order.
for (let i = root.children.length - 1; i >= 0; i--) {
let tabbableChild = this._getLastTabbableElement(root.children[i] as HTMLElement);
Expand Down

0 comments on commit 87ebbd6

Please sign in to comment.