diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index 47b54dbb1036..541909dbf171 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -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';
@@ -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: [
@@ -86,6 +86,7 @@ import {InputContainerDemo} from './input/input-container-demo';
SliderDemo,
SlideToggleDemo,
SpagettiPanel,
+ StyleDemo,
ToolbarDemo,
TooltipDemo,
TabsDemo,
diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts
index 98c029d770cc..cdbdcd904484 100644
--- a/src/demo-app/demo-app/demo-app.ts
+++ b/src/demo-app/demo-app/demo-app.ts
@@ -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) {
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index 3a1b3ba916b6..c1d92d0fab43 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -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},
@@ -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},
];
diff --git a/src/demo-app/style/style-demo.html b/src/demo-app/style/style-demo.html
new file mode 100644
index 000000000000..a3aee1f7bb3f
--- /dev/null
+++ b/src/demo-app/style/style-demo.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
Active classes: {{b.classList}}
diff --git a/src/demo-app/style/style-demo.scss b/src/demo-app/style/style-demo.scss
new file mode 100644
index 000000000000..a04570ce0dee
--- /dev/null
+++ b/src/demo-app/style/style-demo.scss
@@ -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;
+}
diff --git a/src/demo-app/style/style-demo.ts b/src/demo-app/style/style-demo.ts
new file mode 100644
index 000000000000..e1151da131f8
--- /dev/null
+++ b/src/demo-app/style/style-demo.ts
@@ -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) {}
+}
diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts
index b0c08443f1b0..38aba4c9665f 100644
--- a/src/lib/core/core.ts
+++ b/src/lib/core/core.ts
@@ -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';
diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts
new file mode 100644
index 000000000000..e3e86b2004c3
--- /dev/null
+++ b/src/lib/core/style/focus-classes.spec.ts
@@ -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;
+ 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;
+ 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: ``})
+class PlainButton {
+ constructor(public renderer: Renderer) {}
+}
+
+
+@Component({template: ``})
+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);
+}
diff --git a/src/lib/core/style/focus-classes.ts b/src/lib/core/style/focus-classes.ts
new file mode 100644
index 000000000000..efea43939010
--- /dev/null
+++ b/src/lib/core/style/focus-classes.ts
@@ -0,0 +1,85 @@
+import {Directive, Injectable, Optional, SkipSelf, Renderer, ElementRef} from '@angular/core';
+
+
+export type FocusOrigin = 'mouse' | 'keyboard' | 'program';
+
+
+/** Monitors mouse and keyboard events to determine the cause of focus events. */
+@Injectable()
+export class FocusOriginMonitor {
+ /** The focus origin that the next focus event is a result of. */
+ private _origin: FocusOrigin = null;
+
+ constructor() {
+ // Listen to keydown and mousedown in the capture phase so we can detect them even if the user
+ // stops propagation.
+ // TODO(mmalerba): Figure out how to handle touchstart
+ document.addEventListener(
+ 'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true);
+ document.addEventListener(
+ 'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true);
+ }
+
+ /** Register an element to receive focus classes. */
+ registerElementForFocusClasses(element: Element, renderer: Renderer) {
+ renderer.listen(element, 'focus', () => this._onFocus(element, renderer));
+ renderer.listen(element, 'blur', () => this._onBlur(element, renderer));
+ }
+
+ /** Focuses the element via the specified focus origin. */
+ focusVia(element: Node, renderer: Renderer, origin: FocusOrigin) {
+ this._setOriginForCurrentEventQueue(origin);
+ renderer.invokeElementMethod(element, 'focus');
+ }
+
+ /** Sets the origin and schedules an async function to clear it at the end of the event queue. */
+ private _setOriginForCurrentEventQueue(origin: FocusOrigin) {
+ this._origin = origin;
+ setTimeout(() => this._origin = null, 0);
+ }
+
+ /** Handles focus events on a registered element. */
+ private _onFocus(element: Element, renderer: Renderer) {
+ renderer.setElementClass(element, 'cdk-focused', true);
+ renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard');
+ renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse');
+ renderer.setElementClass(element, 'cdk-program-focused',
+ !this._origin || this._origin == 'program');
+ this._origin = null;
+ }
+
+ /** Handles blur events on a registered element. */
+ private _onBlur(element: Element, renderer: Renderer) {
+ renderer.setElementClass(element, 'cdk-focused', false);
+ renderer.setElementClass(element, 'cdk-keyboard-focused', false);
+ renderer.setElementClass(element, 'cdk-mouse-focused', false);
+ renderer.setElementClass(element, 'cdk-program-focused', false);
+ }
+}
+
+
+/**
+ * Directive that determines how a particular element was focused (via keyboard, mouse, or
+ * programmatically) and adds corresponding classes to the element.
+ */
+@Directive({
+ selector: '[cdkFocusClasses]',
+})
+export class CdkFocusClasses {
+ constructor(elementRef: ElementRef, focusOriginMonitor: FocusOriginMonitor, renderer: Renderer) {
+ focusOriginMonitor.registerElementForFocusClasses(elementRef.nativeElement, renderer);
+ }
+}
+
+
+export function FOCUS_ORIGIN_MONITOR_PROVIDER_FACTORY(parentDispatcher: FocusOriginMonitor) {
+ return parentDispatcher || new FocusOriginMonitor();
+}
+
+
+export const FOCUS_ORIGIN_MONITOR_PROVIDER = {
+ // If there is already a FocusOriginMonitor available, use that. Otherwise, provide a new one.
+ provide: FocusOriginMonitor,
+ deps: [[new Optional(), new SkipSelf(), FocusOriginMonitor]],
+ useFactory: FOCUS_ORIGIN_MONITOR_PROVIDER_FACTORY
+};
diff --git a/src/lib/core/style/index.ts b/src/lib/core/style/index.ts
new file mode 100644
index 000000000000..13c58e4ea291
--- /dev/null
+++ b/src/lib/core/style/index.ts
@@ -0,0 +1,13 @@
+import {NgModule} from '@angular/core';
+import {CdkFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './focus-classes';
+
+export * from './focus-classes';
+export * from './apply-transform';
+
+
+@NgModule({
+ declarations: [CdkFocusClasses],
+ exports: [CdkFocusClasses],
+ providers: [FOCUS_ORIGIN_MONITOR_PROVIDER],
+})
+export class StyleModule {}
diff --git a/src/lib/module.ts b/src/lib/module.ts
index 2c395929e1a5..490e419cb133 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -35,6 +35,7 @@ import {MdMenuModule} from './menu/index';
import {MdDialogModule} from './dialog/index';
import {PlatformModule} from './core/platform/index';
import {MdAutocompleteModule} from './autocomplete/index';
+import {StyleModule} from './core/style/index';
const MATERIAL_MODULES = [
MdAutocompleteModule,
@@ -64,6 +65,7 @@ const MATERIAL_MODULES = [
OverlayModule,
PortalModule,
RtlModule,
+ StyleModule,
A11yModule,
PlatformModule,
ProjectionModule,