Skip to content

Commit

Permalink
feat(style): add directive to determine how elements were focused. (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba authored and kara committed Feb 4, 2017
1 parent 2f0dad1 commit 8a6d902
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 5 deletions.
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

0 comments on commit 8a6d902

Please sign in to comment.