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 #1468.
  • Loading branch information
crisbeto committed Nov 8, 2016
1 parent a0d85d8 commit cb8226a
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 12 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="last" md-focus-end></button>
<button id="first" md-focus-start>SAVE</button>
<input>
</focus-trap>
`
})
class FocusTrapTargetTestApp { }
18 changes: 16 additions & 2 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 All @@ -27,15 +29,27 @@ export class FocusTrap {

/** Focuses the first tabbable element within the focus trap region. */
focusFirstTabbableElement() {
let redirectToElement = this._getFirstTabbableElement(this.trappedContent.nativeElement);
let rootElement = this.trappedContent.nativeElement;
let redirectToElement = rootElement.querySelector('[md-focus-start]') as HTMLElement ||
this._getFirstTabbableElement(rootElement);

if (redirectToElement) {
redirectToElement.focus();
}
}

/** Focuses the last tabbable element within the focus trap region. */
focusLastTabbableElement() {
let redirectToElement = this._getLastTabbableElement(this.trappedContent.nativeElement);
let rootElement = this.trappedContent.nativeElement;
let focusTargets = rootElement.querySelectorAll('[md-focus-end]');
let redirectToElement: HTMLElement = null;

if (focusTargets.length) {
redirectToElement = focusTargets[focusTargets.length - 1] as HTMLElement;
} else {
redirectToElement = this._getLastTabbableElement(rootElement);
}

if (redirectToElement) {
redirectToElement.focus();
}
Expand Down

0 comments on commit cb8226a

Please sign in to comment.