Skip to content

Commit dfb6b6e

Browse files
committedAug 8, 2021
improve(focus): ensure correct count of focus events (ngneat#373)
In browsers, HTMLElement.focus() always sets `document.activeElement`, but focus + blur events may or may not be sent depending on focus. This ensures that focus + blur events are always sent if appropriate, and `document.activeElement` is set.
1 parent 9ff8bd4 commit dfb6b6e

File tree

2 files changed

+56
-10
lines changed

2 files changed

+56
-10
lines changed
 

‎projects/spectator/src/lib/internals/element-focus.ts

+46-10
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,47 @@ import { isRunningInJsDom } from '../utils';
44
/** Property name added to HTML Elements to ensure we don't double-patch focus methods on an element. */
55
const IS_FOCUS_PATCHED_PROP = '_patched_focus';
66

7+
/** Ensures that a single set of matching focus and blur events occur when HTMLElement.focus() is called. */
8+
class FocusEventWatcher implements EventListenerObject {
9+
10+
private readonly _active: Element | null;
11+
private _blurred = false;
12+
private _focused = false;
13+
14+
constructor(private readonly _e: HTMLElement) {
15+
this._active = _e.ownerDocument.activeElement;
16+
this._e.addEventListener('focus', this);
17+
this._active?.addEventListener('blur', this);
18+
}
19+
public handleEvent(evt: Event): void {
20+
if (evt.type === 'focus') {
21+
this._focused = true;
22+
}
23+
else if (evt.type === 'blur') {
24+
this._blurred = true;
25+
}
26+
}
27+
28+
/**
29+
* If focus and blur events haven't occurred, fire fake ones.
30+
*/
31+
public ensureFocusEvents() {
32+
this._e.removeEventListener('focus', this);
33+
this._active?.removeEventListener('blur', this);
34+
35+
// Ensure activeElement is blurred
36+
if (this._active && !this._blurred && this._active === this._e.ownerDocument.activeElement) {
37+
dispatchFakeEvent(this._active, 'blur');
38+
}
39+
40+
if (!this._focused) {
41+
dispatchFakeEvent(this._e, 'focus'); // Needed to cause focus event
42+
}
43+
}
44+
}
45+
746
/**
8-
* Patches an elements focus and blur methods to emit events consistently and predictably.
47+
* Patches an element's focus and blur methods to emit events consistently and predictably in tests.
948
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
1049
* while others won't fire them at all if the browser window is not focused.
1150
*
@@ -17,15 +56,12 @@ export function patchElementFocus(element: HTMLElement): void {
1756
if (!isRunningInJsDom() && (element[IS_FOCUS_PATCHED_PROP] === undefined)) {
1857
const baseFocus = element.focus.bind(element);
1958
element.focus = (options) => {
20-
// Blur current active
21-
const active = element.ownerDocument.activeElement;
22-
if (active) {
23-
dispatchFakeEvent(active, 'blur');
24-
}
25-
26-
// Focus
27-
baseFocus(options); // Needed to set document.activeElement
28-
dispatchFakeEvent(element, 'focus'); // Needed to cause focus event
59+
const w = new FocusEventWatcher(element);
60+
61+
// Sets document.activeElement. May or may not send focus + blur events
62+
baseFocus(options);
63+
64+
w.ensureFocusEvents();
2965
}
3066
element.blur = () => dispatchFakeEvent(element, 'blur');
3167
element[IS_FOCUS_PATCHED_PROP] = true;

‎projects/spectator/test/focus/test-focus.component.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,14 @@ describe('SpectatorHost.focus() ', () => {
2929
expect(host.component.blurCount('button2')).toBe(0);
3030
});
3131

32+
it('calling focus() multiple times does not cause multiple patches', () => {
33+
host.focus('#button1');
34+
host.focus();
35+
host.focus('#button1');
36+
37+
expect(host.component.focusCount('app-test-focus')).toBe(1);
38+
expect(host.component.focusCount('button1')).toBe(2);
39+
expect(host.component.blurCount('button1')).toBe(1);
40+
});
41+
3242
});

0 commit comments

Comments
 (0)