Skip to content

Add directive to determine how elements were focused. #2646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 4, 2017
5 changes: 3 additions & 2 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {HttpModule} from '@angular/http';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {DemoApp, Home} from './demo-app/demo-app';
import {RouterModule} from '@angular/router';
import {MaterialModule, OverlayContainer,
FullscreenOverlayContainer} from '@angular/material';
import {MaterialModule, OverlayContainer, FullscreenOverlayContainer} from '@angular/material';
import {DEMO_APP_ROUTES} from './demo-app/routes';
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo';
Expand Down Expand Up @@ -38,6 +37,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
import {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputContainerDemo} from './input/input-container-demo';
import {StyleDemo} from './style/style-demo';

@NgModule({
imports: [
Expand Down Expand Up @@ -86,6 +86,7 @@ import {InputContainerDemo} from './input/input-container-demo';
SliderDemo,
SlideToggleDemo,
SpagettiPanel,
StyleDemo,
ToolbarDemo,
TooltipDemo,
TabsDemo,
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 @@ -50,7 +50,8 @@ export class DemoApp {
{name: 'Tabs', route: 'tabs'},
{name: 'Toolbar', route: 'toolbar'},
{name: 'Tooltip', route: 'tooltip'},
{name: 'Platform', route: 'platform'}
{name: 'Platform', route: 'platform'},
{name: 'Style', route: 'style'}
];

constructor(private _element: ElementRef) {
Expand Down
4 changes: 3 additions & 1 deletion src/demo-app/demo-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {TABS_DEMO_ROUTES} from '../tabs/routes';
import {PlatformDemo} from '../platform/platform-demo';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {InputContainerDemo} from '../input/input-container-demo';
import {StyleDemo} from '../style/style-demo';

export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
Expand Down Expand Up @@ -65,5 +66,6 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'dialog', component: DialogDemo},
{path: 'tooltip', component: TooltipDemo},
{path: 'snack-bar', component: SnackBarDemo},
{path: 'platform', component: PlatformDemo}
{path: 'platform', component: PlatformDemo},
{path: 'style', component: StyleDemo},
];
8 changes: 8 additions & 0 deletions src/demo-app/style/style-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<button #b class="demo-button" cdkFocusClasses>focus me!</button>
<button (click)="b.focus()">focus programmatically</button>

<button (click)="fom.focusVia(b, renderer, 'mouse')">focusVia: mouse</button>
<button (click)="fom.focusVia(b, renderer, 'keyboard')">focusVia: keyboard</button>
<button (click)="fom.focusVia(b, renderer, 'program')">focusVia: program</button>

<div>Active classes: {{b.classList}}</div>
15 changes: 15 additions & 0 deletions src/demo-app/style/style-demo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.demo-button.cdk-focused {
border: 2px solid red;
}

.demo-button.cdk-mouse-focused {
background: green;
}

.demo-button.cdk-keyboard-focused {
background: yellow;
}

.demo-button.cdk-program-focused {
background: blue;
}
13 changes: 13 additions & 0 deletions src/demo-app/style/style-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Component, Renderer} from '@angular/core';
import {FocusOriginMonitor} from '@angular/material';


@Component({
moduleId: module.id,
selector: 'style-demo',
templateUrl: 'style-demo.html',
styleUrls: ['style-demo.css'],
})
export class StyleDemo {
constructor(public renderer: Renderer, public fom: FocusOriginMonitor) {}
}
2 changes: 1 addition & 1 deletion src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export {
export {MdLineModule, MdLine, MdLineSetter} from './line/line';

// Style
export {applyCssTransform} from './style/apply-transform';
export * from './style/index';

// Error
export {MdError} from './errors/error';
Expand Down
293 changes: 293 additions & 0 deletions src/lib/core/style/focus-classes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
import {Component, Renderer} from '@angular/core';
import {StyleModule} from './index';
import {By} from '@angular/platform-browser';
import {TAB} from '../keyboard/keycodes';
import {FocusOriginMonitor} from './focus-classes';
import {PlatformModule} from '../platform/index';
import {Platform} from '../platform/platform';


// NOTE: Firefox only fires focus & blur events when it is the currently active window.
// This is not always the case on our CI setup, therefore we disable tests that depend on these
// events firing for Firefox. We may be able to fix this by configuring our CI to start Firefox with
// the following preference: focusmanager.testmode = true


describe('FocusOriginMonitor', () => {
let fixture: ComponentFixture<PlainButton>;
let buttonElement: HTMLElement;
let buttonRenderer: Renderer;
let focusOriginMonitor: FocusOriginMonitor;
let platform: Platform;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [StyleModule, PlatformModule],
declarations: [
PlainButton,
],
});

TestBed.compileComponents();
}));

beforeEach(inject([FocusOriginMonitor, Platform], (fom: FocusOriginMonitor, pfm: Platform) => {
fixture = TestBed.createComponent(PlainButton);
fixture.detectChanges();

buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
buttonRenderer = fixture.componentInstance.renderer;
focusOriginMonitor = fom;
platform = pfm;

focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer);
}));

it('manually registered element should receive focus classes', async(() => {
if (platform.FIREFOX) { return; }

buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
}, 0);
}));

it('should detect focus via keyboard', async(() => {
if (platform.FIREFOX) { return; }

// Simulate focus via keyboard.
dispatchKeydownEvent(document, TAB);
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
.toBe(true, 'button should have cdk-keyboard-focused class');
}, 0);
}));

it('should detect focus via mouse', async(() => {
if (platform.FIREFOX) { return; }

// Simulate focus via mouse.
dispatchMousedownEvent(document);
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-mouse-focused'))
.toBe(true, 'button should have cdk-mouse-focused class');
}, 0);
}));

it('should detect programmatic focus', async(() => {
if (platform.FIREFOX) { return; }

// Programmatically focus.
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-program-focused'))
.toBe(true, 'button should have cdk-program-focused class');
}, 0);
}));

it('focusVia keyboard should simulate keyboard focus', async(() => {
if (platform.FIREFOX) { return; }

focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard');
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
.toBe(true, 'button should have cdk-keyboard-focused class');
}, 0);
}));

it('focusVia mouse should simulate mouse focus', async(() => {
if (platform.FIREFOX) { return; }

focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse');
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-mouse-focused'))
.toBe(true, 'button should have cdk-mouse-focused class');
}, 0);
}));

it('focusVia program should simulate programmatic focus', async(() => {
if (platform.FIREFOX) { return; }

focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program');
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-program-focused'))
.toBe(true, 'button should have cdk-program-focused class');
}, 0);
}));
});


describe('cdkFocusClasses', () => {
let fixture: ComponentFixture<ButtonWithFocusClasses>;
let buttonElement: HTMLElement;
let platform: Platform;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [StyleModule, PlatformModule],
declarations: [
ButtonWithFocusClasses,
],
});

TestBed.compileComponents();
}));

beforeEach(inject([Platform], (pfm: Platform) => {
fixture = TestBed.createComponent(ButtonWithFocusClasses);
fixture.detectChanges();

buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
platform = pfm;
}));

it('should initially not be focused', () => {
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
});

it('should detect focus via keyboard', async(() => {
if (platform.FIREFOX) { return; }

// Simulate focus via keyboard.
dispatchKeydownEvent(document, TAB);
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
.toBe(true, 'button should have cdk-keyboard-focused class');
}, 0);
}));

it('should detect focus via mouse', async(() => {
if (platform.FIREFOX) { return; }

// Simulate focus via mouse.
dispatchMousedownEvent(document);
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-mouse-focused'))
.toBe(true, 'button should have cdk-mouse-focused class');
}, 0);
}));

it('should detect programmatic focus', async(() => {
if (platform.FIREFOX) { return; }

// Programmatically focus.
buttonElement.focus();
fixture.detectChanges();

setTimeout(() => {
fixture.detectChanges();

expect(buttonElement.classList.length)
.toBe(2, 'button should have exactly 2 focus classes');
expect(buttonElement.classList.contains('cdk-focused'))
.toBe(true, 'button should have cdk-focused class');
expect(buttonElement.classList.contains('cdk-program-focused'))
.toBe(true, 'button should have cdk-program-focused class');
}, 0);
}));
});


@Component({template: `<button>focus me!</button>`})
class PlainButton {
constructor(public renderer: Renderer) {}
}


@Component({template: `<button cdkFocusClasses>focus me!</button>`})
class ButtonWithFocusClasses {}


/** Dispatches a mousedown event on the specified element. */
function dispatchMousedownEvent(element: Node) {
let event = document.createEvent('MouseEvent');
event.initMouseEvent(
'mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}


/** Dispatches a keydown event on the specified element. */
function dispatchKeydownEvent(element: Node, keyCode: number) {
let event: any = document.createEvent('KeyboardEvent');
(event.initKeyEvent || event.initKeyboardEvent).bind(event)(
'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode);
Object.defineProperty(event, 'keyCode', {
get: function() { return keyCode; }
});
element.dispatchEvent(event);
}
Loading