Skip to content

Commit 8a6d902

Browse files
mmalerbakara
authored andcommitted
feat(style): add directive to determine how elements were focused. (#2646)
1 parent 2f0dad1 commit 8a6d902

File tree

11 files changed

+438
-5
lines changed

11 files changed

+438
-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, IFrameDialog} from './dialog/dialog-demo';
@@ -38,6 +37,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
3837
import {PlatformDemo} from './platform/platform-demo';
3938
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
4039
import {InputContainerDemo} from './input/input-container-demo';
40+
import {StyleDemo} from './style/style-demo';
4141

4242
@NgModule({
4343
imports: [
@@ -86,6 +86,7 @@ import {InputContainerDemo} from './input/input-container-demo';
8686
SliderDemo,
8787
SlideToggleDemo,
8888
SpagettiPanel,
89+
StyleDemo,
8990
ToolbarDemo,
9091
TooltipDemo,
9192
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
@@ -32,6 +32,7 @@ import {TABS_DEMO_ROUTES} from '../tabs/routes';
3232
import {PlatformDemo} from '../platform/platform-demo';
3333
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
3434
import {InputContainerDemo} from '../input/input-container-demo';
35+
import {StyleDemo} from '../style/style-demo';
3536

3637
export const DEMO_APP_ROUTES: Routes = [
3738
{path: '', component: Home},
@@ -65,5 +66,6 @@ export const DEMO_APP_ROUTES: Routes = [
6566
{path: 'dialog', component: DialogDemo},
6667
{path: 'tooltip', component: TooltipDemo},
6768
{path: 'snack-bar', component: SnackBarDemo},
68-
{path: 'platform', component: PlatformDemo}
69+
{path: 'platform', component: PlatformDemo},
70+
{path: 'style', component: StyleDemo},
6971
];

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<button #b class="demo-button" cdkFocusClasses>focus me!</button>
2+
<button (click)="b.focus()">focus programmatically</button>
3+
4+
<button (click)="fom.focusVia(b, renderer, 'mouse')">focusVia: mouse</button>
5+
<button (click)="fom.focusVia(b, renderer, 'keyboard')">focusVia: keyboard</button>
6+
<button (click)="fom.focusVia(b, renderer, 'program')">focusVia: program</button>
7+
8+
<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-program-focused {
14+
background: blue;
15+
}

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

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

src/lib/core/core.ts

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

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

104104
// Error
105105
export {MdError} from './errors/error';
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
2+
import {Component, Renderer} from '@angular/core';
3+
import {StyleModule} from './index';
4+
import {By} from '@angular/platform-browser';
5+
import {TAB} from '../keyboard/keycodes';
6+
import {FocusOriginMonitor} from './focus-classes';
7+
import {PlatformModule} from '../platform/index';
8+
import {Platform} from '../platform/platform';
9+
10+
11+
// NOTE: Firefox only fires focus & blur events when it is the currently active window.
12+
// This is not always the case on our CI setup, therefore we disable tests that depend on these
13+
// events firing for Firefox. We may be able to fix this by configuring our CI to start Firefox with
14+
// the following preference: focusmanager.testmode = true
15+
16+
17+
describe('FocusOriginMonitor', () => {
18+
let fixture: ComponentFixture<PlainButton>;
19+
let buttonElement: HTMLElement;
20+
let buttonRenderer: Renderer;
21+
let focusOriginMonitor: FocusOriginMonitor;
22+
let platform: Platform;
23+
24+
beforeEach(async(() => {
25+
TestBed.configureTestingModule({
26+
imports: [StyleModule, PlatformModule],
27+
declarations: [
28+
PlainButton,
29+
],
30+
});
31+
32+
TestBed.compileComponents();
33+
}));
34+
35+
beforeEach(inject([FocusOriginMonitor, Platform], (fom: FocusOriginMonitor, pfm: Platform) => {
36+
fixture = TestBed.createComponent(PlainButton);
37+
fixture.detectChanges();
38+
39+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
40+
buttonRenderer = fixture.componentInstance.renderer;
41+
focusOriginMonitor = fom;
42+
platform = pfm;
43+
44+
focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer);
45+
}));
46+
47+
it('manually registered element should receive focus classes', async(() => {
48+
if (platform.FIREFOX) { return; }
49+
50+
buttonElement.focus();
51+
fixture.detectChanges();
52+
53+
setTimeout(() => {
54+
fixture.detectChanges();
55+
56+
expect(buttonElement.classList.contains('cdk-focused'))
57+
.toBe(true, 'button should have cdk-focused class');
58+
}, 0);
59+
}));
60+
61+
it('should detect focus via keyboard', async(() => {
62+
if (platform.FIREFOX) { return; }
63+
64+
// Simulate focus via keyboard.
65+
dispatchKeydownEvent(document, TAB);
66+
buttonElement.focus();
67+
fixture.detectChanges();
68+
69+
setTimeout(() => {
70+
fixture.detectChanges();
71+
72+
expect(buttonElement.classList.length)
73+
.toBe(2, 'button should have exactly 2 focus classes');
74+
expect(buttonElement.classList.contains('cdk-focused'))
75+
.toBe(true, 'button should have cdk-focused class');
76+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
77+
.toBe(true, 'button should have cdk-keyboard-focused class');
78+
}, 0);
79+
}));
80+
81+
it('should detect focus via mouse', async(() => {
82+
if (platform.FIREFOX) { return; }
83+
84+
// Simulate focus via mouse.
85+
dispatchMousedownEvent(document);
86+
buttonElement.focus();
87+
fixture.detectChanges();
88+
89+
setTimeout(() => {
90+
fixture.detectChanges();
91+
92+
expect(buttonElement.classList.length)
93+
.toBe(2, 'button should have exactly 2 focus classes');
94+
expect(buttonElement.classList.contains('cdk-focused'))
95+
.toBe(true, 'button should have cdk-focused class');
96+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
97+
.toBe(true, 'button should have cdk-mouse-focused class');
98+
}, 0);
99+
}));
100+
101+
it('should detect programmatic focus', async(() => {
102+
if (platform.FIREFOX) { return; }
103+
104+
// Programmatically focus.
105+
buttonElement.focus();
106+
fixture.detectChanges();
107+
108+
setTimeout(() => {
109+
fixture.detectChanges();
110+
111+
expect(buttonElement.classList.length)
112+
.toBe(2, 'button should have exactly 2 focus classes');
113+
expect(buttonElement.classList.contains('cdk-focused'))
114+
.toBe(true, 'button should have cdk-focused class');
115+
expect(buttonElement.classList.contains('cdk-program-focused'))
116+
.toBe(true, 'button should have cdk-program-focused class');
117+
}, 0);
118+
}));
119+
120+
it('focusVia keyboard should simulate keyboard focus', async(() => {
121+
if (platform.FIREFOX) { return; }
122+
123+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard');
124+
fixture.detectChanges();
125+
126+
setTimeout(() => {
127+
fixture.detectChanges();
128+
129+
expect(buttonElement.classList.length)
130+
.toBe(2, 'button should have exactly 2 focus classes');
131+
expect(buttonElement.classList.contains('cdk-focused'))
132+
.toBe(true, 'button should have cdk-focused class');
133+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
134+
.toBe(true, 'button should have cdk-keyboard-focused class');
135+
}, 0);
136+
}));
137+
138+
it('focusVia mouse should simulate mouse focus', async(() => {
139+
if (platform.FIREFOX) { return; }
140+
141+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse');
142+
fixture.detectChanges();
143+
144+
setTimeout(() => {
145+
fixture.detectChanges();
146+
147+
expect(buttonElement.classList.length)
148+
.toBe(2, 'button should have exactly 2 focus classes');
149+
expect(buttonElement.classList.contains('cdk-focused'))
150+
.toBe(true, 'button should have cdk-focused class');
151+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
152+
.toBe(true, 'button should have cdk-mouse-focused class');
153+
}, 0);
154+
}));
155+
156+
it('focusVia program should simulate programmatic focus', async(() => {
157+
if (platform.FIREFOX) { return; }
158+
159+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program');
160+
fixture.detectChanges();
161+
162+
setTimeout(() => {
163+
fixture.detectChanges();
164+
165+
expect(buttonElement.classList.length)
166+
.toBe(2, 'button should have exactly 2 focus classes');
167+
expect(buttonElement.classList.contains('cdk-focused'))
168+
.toBe(true, 'button should have cdk-focused class');
169+
expect(buttonElement.classList.contains('cdk-program-focused'))
170+
.toBe(true, 'button should have cdk-program-focused class');
171+
}, 0);
172+
}));
173+
});
174+
175+
176+
describe('cdkFocusClasses', () => {
177+
let fixture: ComponentFixture<ButtonWithFocusClasses>;
178+
let buttonElement: HTMLElement;
179+
let platform: Platform;
180+
181+
beforeEach(async(() => {
182+
TestBed.configureTestingModule({
183+
imports: [StyleModule, PlatformModule],
184+
declarations: [
185+
ButtonWithFocusClasses,
186+
],
187+
});
188+
189+
TestBed.compileComponents();
190+
}));
191+
192+
beforeEach(inject([Platform], (pfm: Platform) => {
193+
fixture = TestBed.createComponent(ButtonWithFocusClasses);
194+
fixture.detectChanges();
195+
196+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
197+
platform = pfm;
198+
}));
199+
200+
it('should initially not be focused', () => {
201+
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
202+
});
203+
204+
it('should detect focus via keyboard', async(() => {
205+
if (platform.FIREFOX) { return; }
206+
207+
// Simulate focus via keyboard.
208+
dispatchKeydownEvent(document, TAB);
209+
buttonElement.focus();
210+
fixture.detectChanges();
211+
212+
setTimeout(() => {
213+
fixture.detectChanges();
214+
215+
expect(buttonElement.classList.length)
216+
.toBe(2, 'button should have exactly 2 focus classes');
217+
expect(buttonElement.classList.contains('cdk-focused'))
218+
.toBe(true, 'button should have cdk-focused class');
219+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
220+
.toBe(true, 'button should have cdk-keyboard-focused class');
221+
}, 0);
222+
}));
223+
224+
it('should detect focus via mouse', async(() => {
225+
if (platform.FIREFOX) { return; }
226+
227+
// Simulate focus via mouse.
228+
dispatchMousedownEvent(document);
229+
buttonElement.focus();
230+
fixture.detectChanges();
231+
232+
setTimeout(() => {
233+
fixture.detectChanges();
234+
235+
expect(buttonElement.classList.length)
236+
.toBe(2, 'button should have exactly 2 focus classes');
237+
expect(buttonElement.classList.contains('cdk-focused'))
238+
.toBe(true, 'button should have cdk-focused class');
239+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
240+
.toBe(true, 'button should have cdk-mouse-focused class');
241+
}, 0);
242+
}));
243+
244+
it('should detect programmatic focus', async(() => {
245+
if (platform.FIREFOX) { return; }
246+
247+
// Programmatically focus.
248+
buttonElement.focus();
249+
fixture.detectChanges();
250+
251+
setTimeout(() => {
252+
fixture.detectChanges();
253+
254+
expect(buttonElement.classList.length)
255+
.toBe(2, 'button should have exactly 2 focus classes');
256+
expect(buttonElement.classList.contains('cdk-focused'))
257+
.toBe(true, 'button should have cdk-focused class');
258+
expect(buttonElement.classList.contains('cdk-program-focused'))
259+
.toBe(true, 'button should have cdk-program-focused class');
260+
}, 0);
261+
}));
262+
});
263+
264+
265+
@Component({template: `<button>focus me!</button>`})
266+
class PlainButton {
267+
constructor(public renderer: Renderer) {}
268+
}
269+
270+
271+
@Component({template: `<button cdkFocusClasses>focus me!</button>`})
272+
class ButtonWithFocusClasses {}
273+
274+
275+
/** Dispatches a mousedown event on the specified element. */
276+
function dispatchMousedownEvent(element: Node) {
277+
let event = document.createEvent('MouseEvent');
278+
event.initMouseEvent(
279+
'mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
280+
element.dispatchEvent(event);
281+
}
282+
283+
284+
/** Dispatches a keydown event on the specified element. */
285+
function dispatchKeydownEvent(element: Node, keyCode: number) {
286+
let event: any = document.createEvent('KeyboardEvent');
287+
(event.initKeyEvent || event.initKeyboardEvent).bind(event)(
288+
'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode);
289+
Object.defineProperty(event, 'keyCode', {
290+
get: function() { return keyCode; }
291+
});
292+
element.dispatchEvent(event);
293+
}

0 commit comments

Comments
 (0)