Skip to content

Commit 46d8cab

Browse files
committed
addressed comments and added more tests
1 parent 0a8e0ed commit 46d8cab

File tree

4 files changed

+196
-103
lines changed

4 files changed

+196
-103
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
<button (click)="fom.focusVia(b, renderer, 'mouse')">focusVia: mouse</button>
55
<button (click)="fom.focusVia(b, renderer, 'keyboard')">focusVia: keyboard</button>
6-
<button (click)="fom.focusVia(b, renderer, 'programmatic')">focusVia: programmatic</button>
6+
<button (click)="fom.focusVia(b, renderer, 'program')">focusVia: program</button>
77

88
<div>Active classes: {{b.classList}}</div>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
background: yellow;
1111
}
1212

13-
.demo-button.cdk-programmatically-focused {
13+
.demo-button.cdk-program-focused {
1414
background: blue;
1515
}

src/lib/core/style/focus-classes.spec.ts

Lines changed: 173 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,130 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2-
import {Component} from '@angular/core';
1+
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
2+
import {Component, Renderer} from '@angular/core';
33
import {StyleModule} from './index';
44
import {By} from '@angular/platform-browser';
55
import {TAB} from '../keyboard/keycodes';
6+
import {FocusOriginMonitor} from './focus-classes';
7+
8+
9+
describe('FocusOriginMonitor', () => {
10+
let fixture: ComponentFixture<PlainButton>;
11+
let buttonElement: HTMLElement;
12+
let buttonRenderer: Renderer;
13+
let focusOriginMonitor: FocusOriginMonitor;
14+
15+
beforeEach(async(() => {
16+
TestBed.configureTestingModule({
17+
imports: [StyleModule],
18+
declarations: [
19+
PlainButton,
20+
],
21+
});
22+
23+
TestBed.compileComponents();
24+
}));
25+
26+
beforeEach(inject([FocusOriginMonitor], (fom: FocusOriginMonitor) => {
27+
fixture = TestBed.createComponent(PlainButton);
28+
fixture.detectChanges();
29+
30+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
31+
buttonRenderer = fixture.componentInstance.renderer;
32+
focusOriginMonitor = fom;
33+
34+
focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer);
35+
}));
36+
37+
it('manually registered element should receive focus classes', () => {
38+
buttonElement.focus();
39+
fixture.detectChanges();
40+
41+
expect(buttonElement.classList.contains('cdk-focused'))
42+
.toBe(true, 'button should have cdk-focused class');
43+
});
44+
45+
it('should detect focus via keyboard', () => {
46+
// Simulate focus via keyboard.
47+
dispatchKeydownEvent(document, TAB);
48+
buttonElement.focus();
49+
fixture.detectChanges();
50+
51+
expect(buttonElement.classList.length)
52+
.toBe(2, 'button should have exactly 2 focus classes');
53+
expect(buttonElement.classList.contains('cdk-focused'))
54+
.toBe(true, 'button should have cdk-focused class');
55+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
56+
.toBe(true, 'button should have cdk-keyboard-focused class');
57+
});
58+
59+
it('should detect focus via mouse', () => {
60+
// Simulate focus via mouse.
61+
dispatchMousedownEvent(document);
62+
buttonElement.focus();
63+
fixture.detectChanges();
64+
65+
expect(buttonElement.classList.length)
66+
.toBe(2, 'button should have exactly 2 focus classes');
67+
expect(buttonElement.classList.contains('cdk-focused'))
68+
.toBe(true, 'button should have cdk-focused class');
69+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
70+
.toBe(true, 'button should have cdk-mouse-focused class');
71+
});
72+
73+
it('should detect programmatic focus', () => {
74+
// Programmatically focus.
75+
buttonElement.focus();
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-program-focused'))
83+
.toBe(true, 'button should have cdk-program-focused class');
84+
});
85+
86+
it('focusVia keyboard should simulate keyboard focus', () => {
87+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard');
88+
fixture.detectChanges();
89+
90+
expect(buttonElement.classList.length)
91+
.toBe(2, 'button should have exactly 2 focus classes');
92+
expect(buttonElement.classList.contains('cdk-focused'))
93+
.toBe(true, 'button should have cdk-focused class');
94+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
95+
.toBe(true, 'button should have cdk-keyboard-focused class');
96+
});
97+
98+
it('focusVia mouse should simulate mouse focus', () => {
99+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse');
100+
fixture.detectChanges();
101+
102+
expect(buttonElement.classList.length)
103+
.toBe(2, 'button should have exactly 2 focus classes');
104+
expect(buttonElement.classList.contains('cdk-focused'))
105+
.toBe(true, 'button should have cdk-focused class');
106+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
107+
.toBe(true, 'button should have cdk-mouse-focused class');
108+
});
109+
110+
it('focusVia program should simulate programmatic focus', () => {
111+
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program');
112+
fixture.detectChanges();
113+
114+
expect(buttonElement.classList.length)
115+
.toBe(2, 'button should have exactly 2 focus classes');
116+
expect(buttonElement.classList.contains('cdk-focused'))
117+
.toBe(true, 'button should have cdk-focused class');
118+
expect(buttonElement.classList.contains('cdk-program-focused'))
119+
.toBe(true, 'button should have cdk-program-focused class');
120+
});
121+
});
6122

7123

8124
describe('cdkFocusClasses', () => {
125+
let fixture: ComponentFixture<ButtonWithFocusClasses>;
126+
let buttonElement: HTMLElement;
127+
9128
beforeEach(async(() => {
10129
TestBed.configureTestingModule({
11130
imports: [StyleModule],
@@ -17,77 +136,66 @@ describe('cdkFocusClasses', () => {
17136
TestBed.compileComponents();
18137
}));
19138

20-
describe('cdkFocusClasses', () => {
21-
let fixture: ComponentFixture<ButtonWithFocusClasses>;
22-
let buttonElement: HTMLElement;
139+
beforeEach(() => {
140+
fixture = TestBed.createComponent(ButtonWithFocusClasses);
141+
fixture.detectChanges();
23142

24-
beforeEach(() => {
25-
fixture = TestBed.createComponent(ButtonWithFocusClasses);
26-
fixture.detectChanges();
143+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
144+
});
27145

28-
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
29-
});
146+
it('should initially not be focused', () => {
147+
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
148+
});
30149

31-
it('should initially not be focused', () => {
32-
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
33-
});
150+
it('should detect focus via keyboard', () => {
151+
// Simulate focus via keyboard.
152+
dispatchKeydownEvent(document, TAB);
153+
buttonElement.focus();
154+
fixture.detectChanges();
155+
156+
expect(buttonElement.classList.length)
157+
.toBe(2, 'button should have exactly 2 focus classes');
158+
expect(buttonElement.classList.contains('cdk-focused'))
159+
.toBe(true, 'button should have cdk-focused class');
160+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
161+
.toBe(true, 'button should have cdk-keyboard-focused class');
162+
});
34163

35-
it('should detect focus via keyboard', async(() => {
36-
// Simulate focus via keyboard.
37-
dispatchKeydownEvent(document, TAB);
38-
buttonElement.focus();
39-
fixture.detectChanges();
40-
41-
setTimeout(() => {
42-
fixture.detectChanges();
43-
44-
expect(buttonElement.classList.length)
45-
.toBe(2, 'button should have exactly 2 focus classes');
46-
expect(buttonElement.classList.contains('cdk-focused'))
47-
.toBe(true, 'button should have cdk-focused class');
48-
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
49-
.toBe(true, 'button should have cdk-keyboard-focused class');
50-
}, 0);
51-
}));
52-
53-
it('should detect focus via mouse', async(() => {
54-
// Simulate focus via mouse.
55-
dispatchMousedownEvent(document);
56-
buttonElement.focus();
57-
fixture.detectChanges();
58-
59-
setTimeout(() => {
60-
fixture.detectChanges();
61-
62-
expect(buttonElement.classList.length)
63-
.toBe(2, 'button should have exactly 2 focus classes');
64-
expect(buttonElement.classList.contains('cdk-focused'))
65-
.toBe(true, 'button should have cdk-focused class');
66-
expect(buttonElement.classList.contains('cdk-mouse-focused'))
67-
.toBe(true, 'button should have cdk-mouse-focused class');
68-
}, 0);
69-
}));
70-
71-
it('should detect programmatic focus', async(() => {
72-
// Programmatically focus.
73-
buttonElement.focus();
74-
fixture.detectChanges();
75-
76-
setTimeout(() => {
77-
fixture.detectChanges();
78-
79-
expect(buttonElement.classList.length)
80-
.toBe(2, 'button should have exactly 2 focus classes');
81-
expect(buttonElement.classList.contains('cdk-focused'))
82-
.toBe(true, 'button should have cdk-focused class');
83-
expect(buttonElement.classList.contains('cdk-programmatically-focused'))
84-
.toBe(true, 'button should have cdk-programmatically-focused class');
85-
}, 0);
86-
}));
164+
it('should detect focus via mouse', () => {
165+
// Simulate focus via mouse.
166+
dispatchMousedownEvent(document);
167+
buttonElement.focus();
168+
fixture.detectChanges();
169+
170+
expect(buttonElement.classList.length)
171+
.toBe(2, 'button should have exactly 2 focus classes');
172+
expect(buttonElement.classList.contains('cdk-focused'))
173+
.toBe(true, 'button should have cdk-focused class');
174+
expect(buttonElement.classList.contains('cdk-mouse-focused'))
175+
.toBe(true, 'button should have cdk-mouse-focused class');
176+
});
177+
178+
it('should detect programmatic focus', () => {
179+
// Programmatically focus.
180+
buttonElement.focus();
181+
fixture.detectChanges();
182+
183+
expect(buttonElement.classList.length)
184+
.toBe(2, 'button should have exactly 2 focus classes');
185+
expect(buttonElement.classList.contains('cdk-focused'))
186+
.toBe(true, 'button should have cdk-focused class');
187+
expect(buttonElement.classList.contains('cdk-program-focused'))
188+
.toBe(true, 'button should have cdk-program-focused class');
87189
});
88190
});
89191

90192

193+
@Component({template: `<button>focus me!</button>`})
194+
class PlainButton {
195+
constructor(public renderer: Renderer) {}
196+
}
197+
198+
91199
@Component({template: `<button cdkFocusClasses>focus me!</button>`})
92200
class ButtonWithFocusClasses {}
93201

src/lib/core/style/focus-classes.ts

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,55 @@
11
import {Directive, Injectable, Optional, SkipSelf, Renderer, ElementRef} from '@angular/core';
22

33

4-
export type FocusOrigin = 'mouse' | 'keyboard' | 'programmatic';
4+
export type FocusOrigin = 'mouse' | 'keyboard' | 'program';
55

66

77
/** Monitors mouse and keyboard events to determine the cause of focus events. */
88
@Injectable()
99
export class FocusOriginMonitor {
10-
/** Whether a keydown event has just occurred. */
11-
private _keydownOccurred = false;
12-
13-
/** Whether a mousedown event has just occurred. */
14-
private _mousedownOccurred = false;
15-
16-
/** The focus origin that we're pretending the next focus event is a result of. */
17-
private _fakeOrigin: FocusOrigin = null;
18-
19-
/** A function to clear the fake origin. */
20-
private _clearFakeOrigin = (): void => {
21-
setTimeout(() => this._fakeOrigin = null, 0);
22-
document.removeEventListener('focus', this._clearFakeOrigin, true);
23-
};
10+
/** The focus origin that the next focus event is a result of. */
11+
private _origin: FocusOrigin = null;
2412

2513
constructor() {
2614
// Listen to keydown and mousedown in the capture phase so we can detect them even if the user
2715
// stops propagation.
2816
// TODO(mmalerba): Figure out how to handle touchstart
29-
document.addEventListener('keydown', () => {
30-
this._keydownOccurred = true;
31-
setTimeout(() => this._keydownOccurred = false, 0);
32-
}, true);
33-
34-
document.addEventListener('mousedown', () => {
35-
this._mousedownOccurred = true;
36-
setTimeout(() => this._mousedownOccurred = false, 0);
37-
}, true);
17+
document.addEventListener(
18+
'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true);
19+
20+
document.addEventListener(
21+
'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true);
3822
}
3923

4024
/** Register an element to receive focus classes. */
4125
registerElementForFocusClasses(element: Element, renderer: Renderer) {
4226
renderer.listen(element, 'focus', () => {
43-
let isKeyboard = this._fakeOrigin ? this._fakeOrigin === 'keyboard' : this._keydownOccurred;
44-
let isMouse = this._fakeOrigin ? this._fakeOrigin === 'mouse' : this._mousedownOccurred;
45-
let isProgrammatic = this._fakeOrigin ?
46-
this._fakeOrigin === 'programmatic' : !this._keydownOccurred && !this._mousedownOccurred;
47-
4827
renderer.setElementClass(element, 'cdk-focused', true);
49-
renderer.setElementClass(element, 'cdk-keyboard-focused', isKeyboard);
50-
renderer.setElementClass(element, 'cdk-mouse-focused', isMouse);
51-
renderer.setElementClass(element, 'cdk-programmatically-focused', isProgrammatic);
28+
renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard');
29+
renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse');
30+
renderer.setElementClass(element, 'cdk-program-focused',
31+
!this._origin || this._origin == 'program');
5232
});
5333

5434
renderer.listen(element, 'blur', () => {
5535
renderer.setElementClass(element, 'cdk-focused', false);
5636
renderer.setElementClass(element, 'cdk-keyboard-focused', false);
5737
renderer.setElementClass(element, 'cdk-mouse-focused', false);
58-
renderer.setElementClass(element, 'cdk-programmatically-focused', false);
38+
renderer.setElementClass(element, 'cdk-program-focused', false);
5939
});
6040
}
6141

6242
/** Focuses the element via the specified focus origin. */
63-
focusVia(element: Node, renderer: Renderer, focusOrigin: FocusOrigin) {
64-
this._fakeOrigin = focusOrigin;
65-
document.addEventListener('focus', this._clearFakeOrigin, true);
43+
focusVia(element: Node, renderer: Renderer, origin: FocusOrigin) {
44+
this._setOriginForCurrentEventQueue(origin);
6645
renderer.invokeElementMethod(element, 'focus');
6746
}
47+
48+
/** Sets the origin and schedules an async function to clear it at the end of the event queue. */
49+
private _setOriginForCurrentEventQueue(origin: FocusOrigin) {
50+
this._origin = origin;
51+
setTimeout(() => this._origin = null, 0);
52+
}
6853
}
6954

7055

0 commit comments

Comments
 (0)