Skip to content

Commit

Permalink
fix(focus-trap): improve robustness
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion committed Nov 21, 2016
1 parent 3b80a6c commit bb1ec76
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 17 deletions.
2 changes: 2 additions & 0 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-demo';
import {PlatformDemo} from './platform/platform-demo';

@NgModule({
imports: [
Expand Down Expand Up @@ -84,6 +85,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
SunnyTabContent,
RainyTabContent,
FoggyTabContent,
PlatformDemo
],
entryComponents: [
DemoApp,
Expand Down
3 changes: 2 additions & 1 deletion src/demo-app/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class DemoApp {
{name: 'Snack Bar', route: 'snack-bar'},
{name: 'Tabs', route: 'tabs'},
{name: 'Toolbar', route: 'toolbar'},
{name: 'Tooltip', route: 'tooltip'}
{name: 'Tooltip', route: 'tooltip'},
{name: 'Platform', route: 'platform'}
];
}
2 changes: 2 additions & 0 deletions src/demo-app/demo-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {TooltipDemo} from '../tooltip/tooltip-demo';
import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
import {ProjectionDemo} from '../projection/projection-demo';
import {TABS_DEMO_ROUTES} from '../tabs/routes';
import {PlatformDemo} from '../platform/platform-demo';

export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
Expand Down Expand Up @@ -60,4 +61,5 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'dialog', component: DialogDemo},
{path: 'tooltip', component: TooltipDemo},
{path: 'snack-bar', component: SnackBarDemo},
{path: 'platform', component: PlatformDemo}
];
19 changes: 19 additions & 0 deletions src/demo-app/platform/platform-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';
import {MdPlatform} from '@angular/material';

@Component({
template: `
<p>Is Android: {{ platform.ANDROID }}</p>
<p>Is iOS: {{ platform.IOS }}</p>
<p>Is Firefox: {{ platform.FIREFOX }}</p>
<p>Is Blink: {{ platform.BLINK }}</p>
<p>Is Webkit: {{ platform.WEBKIT }}</p>
<p>Is Trident: {{ platform.TRIDENT }}</p>
`
})
export class PlatformDemo {

constructor(public platform: MdPlatform) {}

}
5 changes: 3 additions & 2 deletions src/lib/core/a11y/focus-trap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {By} from '@angular/platform-browser';
import {Component} from '@angular/core';
import {FocusTrap} from './focus-trap';
import {InteractivityChecker} from './interactivity-checker';
import {MdPlatform} from '../platform/platform';


describe('FocusTrap', () => {
Expand All @@ -12,7 +13,7 @@ describe('FocusTrap', () => {

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

beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
Expand Down Expand Up @@ -45,7 +46,7 @@ describe('FocusTrap', () => {

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

beforeEach(inject([InteractivityChecker], (c: InteractivityChecker) => {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/a11y/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {NgModule, ModuleWithProviders} from '@angular/core';
import {FocusTrap} from './focus-trap';
import {MdLiveAnnouncer} from './live-announcer';
import {InteractivityChecker} from './interactivity-checker';
import {PlatformModule} from '../platform/platform';

export const A11Y_PROVIDERS = [MdLiveAnnouncer, InteractivityChecker];

@NgModule({
imports: [PlatformModule],
declarations: [FocusTrap],
exports: [FocusTrap],
})
Expand Down
193 changes: 192 additions & 1 deletion src/lib/core/a11y/interactivity-checker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {InteractivityChecker} from './interactivity-checker';
import {MdPlatform} from '../platform/platform';
import {async} from '@angular/core/testing';

describe('InteractivityChecker', () => {
let testContainerElement: HTMLElement;
let checker: InteractivityChecker;
let platform: MdPlatform;

beforeEach(() => {
testContainerElement = document.createElement('div');
document.body.appendChild(testContainerElement);

checker = new InteractivityChecker();
platform = new MdPlatform();
checker = new InteractivityChecker(platform);
});

afterEach(() => {
Expand Down Expand Up @@ -280,13 +284,200 @@ describe('InteractivityChecker', () => {
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be tabbable`);
});
});

it('should respect the inherited tabindex inside of frame elements', () => {
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');

appendElements([iframe]);

iframe.tabIndex = -1;
iframe.contentDocument.body.appendChild(button);

expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);

iframe.tabIndex = null;

expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(true);
});

it('should not mark elements inside of object frames as tabbable (BLINK & WEBKIT)', () => {
platform.BLINK = true;

let objectEl = createFromTemplate('<object>', true) as HTMLObjectElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');

appendElements([objectEl]);

// This is a hack to create an empty contentDocument for the frame element.
objectEl.type = 'text/html';
objectEl.contentDocument.body.appendChild(button);

expect(checker.isTabbable(objectEl)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);
});

it('should not mark elements inside of invisible frames as tabbable (BLINK & WEBKIT)', () => {
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');

appendElements([iframe]);

iframe.style.display = 'none';
iframe.contentDocument.body.appendChild(button);

expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);
});

it('should mark elements which are contentEditable as tabbable', async(() => {
let editableEl = createFromTemplate('<div contenteditable="true">', true);

// Wait one tick, because the browser takes some time to update the tabIndex
// according to the contentEditable attribute.
setTimeout(() => {

expect(checker.isTabbable(editableEl)).toBe(true);

editableEl.tabIndex = -1;

expect(checker.isTabbable(editableEl)).toBe(false);

}, 1);

}));

it('should never mark iframe elements as tabbable', () => {
let iframe = createFromTemplate('<iframe>', true);

// iFrame elements will be never marked as tabbable, because it depends on the content
// which is mostly not detectable due to CORS and also the checks will be not reliable.
expect(checker.isTabbable(iframe)).toBe(false);
});

it('should never mark object frame elements as tabbable', () => {
let objectEl = createFromTemplate('<object>', true);

expect(checker.isTabbable(objectEl)).toBe(false);
});

it('should always mark audio elements without controls as not tabbable', () => {
let audio = createFromTemplate('<audio>', true);

expect(checker.isTabbable(audio)).toBe(false);
});

it('should always mark audio elements with controls as tabbable (BLINK)', () => {
platform.BLINK = true;

let audio = createFromTemplate('<audio controls>', true);

expect(checker.isTabbable(audio)).toBe(true);

audio.tabIndex = -1;

// The audio element will be still tabbable because Blink always
// considers them as tabbable.
expect(checker.isTabbable(audio)).toBe(true);

platform.BLINK = false;

expect(checker.isTabbable(audio)).toBe(false);
});

it('should never mark video elements without controls as tabbable (IE11)', () => {
platform.TRIDENT = true;

let video = createFromTemplate('<video>', true);

expect(checker.isTabbable(video)).toBe(false);
});

it('should always mark video elements with controls as tabbable (BLINK & FIREFOX)', () => {
platform.BLINK = true;

let video = createFromTemplate('<video controls>', true);

expect(checker.isTabbable(video)).toBe(true);

video.tabIndex = -1;

expect(checker.isTabbable(video)).toBe(true);
});

it('should respect the tabindex for video elements with controls', () => {
// Don't run the test as Blink or Firefox, because those will always mark
// video elements with controls as tabbable.
platform.BLINK = false;
platform.FIREFOX = false;

let video = createFromTemplate('<video controls>', true);

expect(checker.isTabbable(video)).toBe(true);

video.tabIndex = -1;

expect(checker.isTabbable(video)).toBe(false);
});

describe('for iOS browsers', () => {

beforeEach(() => {
platform.IOS = true;
platform.WEBKIT = true;
});

it('should never allow div elements to be tabbable', () => {
let divEl = createFromTemplate('<div tabindex="0">', true);

expect(checker.isTabbable(divEl)).toBe(false);
});

it('should never allow span elements to be tabbable', () => {
let spanEl = createFromTemplate('<span tabindex="0">Text</span>', true);

expect(checker.isTabbable(spanEl)).toBe(false);
});

it('should never allow button elements to be tabbable', () => {
let buttonEl = createFromTemplate('<button tabindex="0">', true);

expect(checker.isTabbable(buttonEl)).toBe(false);
});

it('should never allow anchor elements to be tabbable', () => {
let anchorEl = createFromTemplate('<a tabindex="0">Link</a>', true);

expect(checker.isTabbable(anchorEl)).toBe(false);
});

});


});

/** Creates an array of elements with the given node names. */
function createElements(...nodeNames: string[]) {
return nodeNames.map(name => document.createElement(name));
}

function createFromTemplate(template: string, append = false) {
let tmpRoot = document.createElement('div');
tmpRoot.innerHTML = template;

let element = tmpRoot.firstElementChild;

tmpRoot.removeChild(element);

if (append) {
appendElements([element]);
}

return element as HTMLElement;
}

/** Appends elements to the testContainerElement. */
function appendElements(elements: Element[]) {
for (let e of elements) {
Expand Down
Loading

0 comments on commit bb1ec76

Please sign in to comment.