Skip to content

Commit 179933e

Browse files
committed
Add directive to determine how elements were focused.
1 parent 4a6b6a6 commit 179933e

11 files changed

+246
-5
lines changed

src/demo-app/demo-app-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {HttpModule} from '@angular/http';
44
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
55
import {DemoApp, Home} from './demo-app/demo-app';
66
import {RouterModule} from '@angular/router';
7-
import {MaterialModule, OverlayContainer,
8-
FullscreenOverlayContainer} from '@angular/material';
7+
import {MaterialModule, OverlayContainer, FullscreenOverlayContainer} from '@angular/material';
98
import {DEMO_APP_ROUTES} from './demo-app/routes';
109
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
1110
import {JazzDialog, ContentElementDialog, DialogDemo} from './dialog/dialog-demo';
@@ -39,6 +38,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
3938
import {PlatformDemo} from './platform/platform-demo';
4039
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
4140
import {InputContainerDemo} from './input/input-container-demo';
41+
import {StyleDemo} from './style/style-demo';
4242

4343
@NgModule({
4444
imports: [
@@ -87,6 +87,7 @@ import {InputContainerDemo} from './input/input-container-demo';
8787
SliderDemo,
8888
SlideToggleDemo,
8989
SpagettiPanel,
90+
StyleDemo,
9091
ToolbarDemo,
9192
TooltipDemo,
9293
TabsDemo,

src/demo-app/demo-app/demo-app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export class DemoApp {
5050
{name: 'Tabs', route: 'tabs'},
5151
{name: 'Toolbar', route: 'toolbar'},
5252
{name: 'Tooltip', route: 'tooltip'},
53-
{name: 'Platform', route: 'platform'}
53+
{name: 'Platform', route: 'platform'},
54+
{name: 'Style', route: 'style'}
5455
];
5556

5657
constructor(private _element: ElementRef) {

src/demo-app/demo-app/routes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {TABS_DEMO_ROUTES} from '../tabs/routes';
3333
import {PlatformDemo} from '../platform/platform-demo';
3434
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
3535
import {InputContainerDemo} from '../input/input-container-demo';
36+
import {StyleDemo} from '../style/style-demo';
3637

3738
export const DEMO_APP_ROUTES: Routes = [
3839
{path: '', component: Home},
@@ -67,5 +68,6 @@ export const DEMO_APP_ROUTES: Routes = [
6768
{path: 'dialog', component: DialogDemo},
6869
{path: 'tooltip', component: TooltipDemo},
6970
{path: 'snack-bar', component: SnackBarDemo},
70-
{path: 'platform', component: PlatformDemo}
71+
{path: 'platform', component: PlatformDemo},
72+
{path: 'style', component: StyleDemo},
7173
];

src/demo-app/style/style-demo.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<button #b class="demo-button" cdkAddFocusClasses>focus me!</button>
2+
<button (click)="b.focus()">focus programmatically</button>
3+
<div>Active classes: {{b.classList}}</div>

src/demo-app/style/style-demo.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.demo-button.cdk-focused {
2+
border: 2px solid red;
3+
}
4+
5+
.demo-button.cdk-mouse-focused {
6+
background: green;
7+
}
8+
9+
.demo-button.cdk-keyboard-focused {
10+
background: yellow;
11+
}
12+
13+
.demo-button.cdk-programmatically-focused {
14+
background: blue;
15+
}

src/demo-app/style/style-demo.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Component} from '@angular/core';
2+
3+
4+
@Component({
5+
moduleId: module.id,
6+
selector: 'style-demo',
7+
templateUrl: 'style-demo.html',
8+
styleUrls: ['style-demo.css'],
9+
})
10+
export class StyleDemo {}

src/lib/core/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export {
9494
export {MdLineModule, MdLine, MdLineSetter} from './line/line';
9595

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

9999
// Error
100100
export {MdError} from './errors/error';
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {Component} from '@angular/core';
3+
import {StyleModule} from './index';
4+
import {By} from '@angular/platform-browser';
5+
6+
7+
describe('MdSlider', () => {
8+
beforeEach(async(() => {
9+
TestBed.configureTestingModule({
10+
imports: [StyleModule],
11+
declarations: [
12+
ButtonWithFocusClasses,
13+
],
14+
});
15+
16+
TestBed.compileComponents();
17+
}));
18+
19+
describe('cdkAddFocusClasses', () => {
20+
let fixture: ComponentFixture<ButtonWithFocusClasses>;
21+
let buttonElement: HTMLElement;
22+
23+
beforeEach(() => {
24+
fixture = TestBed.createComponent(ButtonWithFocusClasses);
25+
fixture.detectChanges();
26+
27+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
28+
});
29+
30+
it('should initially not be focused', () => {
31+
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
32+
});
33+
34+
it('should detect focus via keyboard', async(() => {
35+
// Simulate focus via keyboard.
36+
dispatchKeydownEvent(document, 9 /* tab */);
37+
buttonElement.focus();
38+
fixture.detectChanges();
39+
40+
setTimeout(() => {
41+
fixture.detectChanges();
42+
43+
expect(buttonElement.classList.length)
44+
.toBe(2, 'button should have exactly 2 focus classes');
45+
expect(buttonElement.classList.contains('cdk-focused'))
46+
.toBe(true, 'button should have cdk-focused class');
47+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
48+
.toBe(true, 'button should have cdk-keyboard-focused class');
49+
}, 0);
50+
}));
51+
52+
it('should detect focus via mouse', async(() => {
53+
// Simulate focus via mouse.
54+
dispatchMousedownEvent(document);
55+
buttonElement.focus();
56+
fixture.detectChanges();
57+
58+
setTimeout(() => {
59+
fixture.detectChanges();
60+
61+
expect(buttonElement.classList.length)
62+
.toBe(2, 'button should have exactly 2 focus classes');
63+
expect(buttonElement.classList.contains('cdk-focused'))
64+
.toBe(true, 'button should have cdk-focused class');
65+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
66+
.toBe(true, 'button should have cdk-mouse-focused class');
67+
}, 0);
68+
}));
69+
70+
it('should detect programmatic focus', async(() => {
71+
// Programmatically focus.
72+
buttonElement.focus();
73+
fixture.detectChanges();
74+
75+
setTimeout(() => {
76+
fixture.detectChanges();
77+
78+
expect(buttonElement.classList.length)
79+
.toBe(2, 'button should have exactly 2 focus classes');
80+
expect(buttonElement.classList.contains('cdk-focused'))
81+
.toBe(true, 'button should have cdk-focused class');
82+
expect(buttonElement.classList.contains('cdk-programmatically-focused'))
83+
.toBe(true, 'button should have cdk-programmatically-focused class');
84+
}, 0);
85+
}));
86+
});
87+
});
88+
89+
90+
@Component({template: `<button cdkAddFocusClasses>focus me!</button>`})
91+
class ButtonWithFocusClasses {}
92+
93+
94+
/** Dispatches a mousedown event on the specified element. */
95+
function dispatchMousedownEvent(element: Node) {
96+
let event = document.createEvent('MouseEvent');
97+
event.initMouseEvent(
98+
'mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
99+
element.dispatchEvent(event);
100+
}
101+
102+
103+
/** Dispatches a keydown event on the specified element. */
104+
function dispatchKeydownEvent(element: Node, keyCode: number) {
105+
let event: any = document.createEvent('KeyboardEvent');
106+
(event.initKeyEvent || event.initKeyboardEvent).bind(event)(
107+
'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode);
108+
Object.defineProperty(event, 'keyCode', {
109+
get: function() { return keyCode; }
110+
});
111+
element.dispatchEvent(event);
112+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {Directive, Injectable, Optional, SkipSelf} from '@angular/core';
2+
3+
4+
/** Singleton that allows all instances of CdkAddFocusClasses to share document event listeners. */
5+
@Injectable()
6+
export class CdkFocusCauseDetector {
7+
/** Whether a keydown event has just occurred. */
8+
get keydownOccurred() { return this._keydownOccurred; }
9+
private _keydownOccurred = false;
10+
11+
get mousedownOccurred() { return this._mousedownOccurred; }
12+
private _mousedownOccurred = false;
13+
14+
constructor() {
15+
document.addEventListener('keydown', () => {
16+
this._keydownOccurred = true;
17+
setTimeout(() => this._keydownOccurred = false, 0);
18+
}, true);
19+
20+
document.addEventListener('mousedown', () => {
21+
this._mousedownOccurred = true;
22+
setTimeout(() => this._mousedownOccurred = false, 0);
23+
}, true);
24+
}
25+
}
26+
27+
28+
/**
29+
* Directive that determines how a particular element was focused (via keyboard, mouse, or
30+
* programmatically) and adds corresponding classes to the element.
31+
*/
32+
@Directive({
33+
selector: '[cdkAddFocusClasses]',
34+
host: {
35+
'[class.cdk-focused]': 'keyboardFocused || mouseFocused || programmaticallyFocused',
36+
'[class.cdk-keyboard-focused]': 'keyboardFocused',
37+
'[class.cdk-mouse-focused]': 'mouseFocused',
38+
'[class.cdk-programmatically-focused]': 'programmaticallyFocused',
39+
'(focus)': '_onFocus()',
40+
'(blur)': '_onBlur()',
41+
}
42+
})
43+
export class CdkAddFocusClasses {
44+
/** Whether the elmenet is focused due to a keyboard event. */
45+
keyboardFocused = false;
46+
47+
/** Whether the element is focused due to a mouse event. */
48+
mouseFocused = false;
49+
50+
/** Whether the has been programmatically focused. */
51+
programmaticallyFocused = false;
52+
53+
constructor(private _focusCauseDetector: CdkFocusCauseDetector) {}
54+
55+
/** Handles focus event on the element. */
56+
_onFocus() {
57+
this.keyboardFocused = this._focusCauseDetector.keydownOccurred;
58+
this.mouseFocused = this._focusCauseDetector.mousedownOccurred;
59+
this.programmaticallyFocused = !this.keyboardFocused && !this.mouseFocused;
60+
}
61+
62+
/** Handles blur event on the element. */
63+
_onBlur() {
64+
this.keyboardFocused = this.mouseFocused = this.programmaticallyFocused = false;
65+
}
66+
}
67+
68+
69+
export function FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY(
70+
parentDispatcher: CdkFocusCauseDetector) {
71+
return parentDispatcher || new CdkFocusCauseDetector();
72+
}
73+
74+
75+
export const FOCUS_CAUSE_DETECTOR_PROVIDER = {
76+
// If there is already a CdkFocusCauseDetector available, use that. Otherwise, provide a new one.
77+
provide: CdkFocusCauseDetector,
78+
deps: [[new Optional(), new SkipSelf(), CdkFocusCauseDetector]],
79+
useFactory: FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY
80+
};

src/lib/core/style/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {NgModule} from '@angular/core';
2+
import {CdkAddFocusClasses, FOCUS_CAUSE_DETECTOR_PROVIDER} from './add-focus-classes';
3+
import {DefaultStyleCompatibilityModeModule} from '../compatibility/default-mode';
4+
5+
export * from './add-focus-classes';
6+
export * from './apply-transform';
7+
8+
9+
@NgModule({
10+
imports: [DefaultStyleCompatibilityModeModule],
11+
declarations: [CdkAddFocusClasses],
12+
exports: [CdkAddFocusClasses, DefaultStyleCompatibilityModeModule],
13+
providers: [FOCUS_CAUSE_DETECTOR_PROVIDER],
14+
})
15+
export class StyleModule {}

src/lib/module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {MdMenuModule} from './menu/index';
3535
import {MdDialogModule} from './dialog/index';
3636
import {PlatformModule} from './core/platform/index';
3737
import {MdAutocompleteModule} from './autocomplete/index';
38+
import {StyleModule} from './core/style/index';
3839

3940
const MATERIAL_MODULES = [
4041
MdAutocompleteModule,
@@ -64,6 +65,7 @@ const MATERIAL_MODULES = [
6465
OverlayModule,
6566
PortalModule,
6667
RtlModule,
68+
StyleModule,
6769
A11yModule,
6870
PlatformModule,
6971
ProjectionModule,

0 commit comments

Comments
 (0)