From cb8226a6b2fcc11ae5b92847642dc9ab7ca5de33 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 6 Nov 2016 15:53:34 +0100 Subject: [PATCH] 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. --- src/lib/core/a11y/focus-trap.spec.ts | 56 +++++++++++++++++++++++----- src/lib/core/a11y/focus-trap.ts | 18 ++++++++- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/lib/core/a11y/focus-trap.spec.ts b/src/lib/core/a11y/focus-trap.spec.ts index 279ebadb275d..e4c6a7d0458e 100644 --- a/src/lib/core/a11y/focus-trap.spec.ts +++ b/src/lib/core/a11y/focus-trap.spec.ts @@ -6,24 +6,21 @@ import {InteractivityChecker} from './interactivity-checker'; describe('FocusTrap', () => { - let checker: InteractivityChecker; - let fixture: ComponentFixture; - describe('with default element', () => { + let fixture: ComponentFixture; + 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(); @@ -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(); @@ -44,6 +38,35 @@ describe('FocusTrap', () => { .toBe('button', 'Expected button element to be focused'); }); }); + + describe('with focus targets', () => { + let fixture: ComponentFixture; + 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'); + }); + }); }); @@ -56,3 +79,16 @@ describe('FocusTrap', () => { ` }) class FocusTrapTestApp { } + + +@Component({ + template: ` + + + + + + + ` +}) +class FocusTrapTargetTestApp { } diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index 539a7f77bed7..c9e03e8b9045 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -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. @@ -27,7 +29,10 @@ 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(); } @@ -35,7 +40,16 @@ export class FocusTrap { /** 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(); }