Skip to content

Commit d5f5022

Browse files
committed
fix(cdk/testing): simulate focusin/focusout events
Fixes that our fake fallback focus events weren't dispatching `focusin` and `focusout` events as well. Fixes #23757.
1 parent a931de5 commit d5f5022

File tree

6 files changed

+38
-13
lines changed

6 files changed

+38
-13
lines changed

src/cdk/testing/testbed/fake-events/element-focus.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,20 @@ function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') {
1414
element.addEventListener(event, handler);
1515
element[event]();
1616
element.removeEventListener(event, handler);
17+
18+
// Some browsers won't move focus if the browser window is blurred while other will move it
19+
// asynchronously. If that is the case, we fake the event sequence as a fallback.
1720
if (!eventFired) {
18-
dispatchFakeEvent(element, event);
21+
simulateFocusSequence(element, event);
1922
}
2023
}
2124

25+
/** Simulates the full event sequence for a focus event. */
26+
function simulateFocusSequence(element: HTMLElement, event: 'focus' | 'blur') {
27+
dispatchFakeEvent(element, event);
28+
dispatchFakeEvent(element, event === 'focus' ? 'focusin' : 'focusout');
29+
}
30+
2231
/**
2332
* Patches an elements focus and blur methods to emit events consistently and predictably.
2433
* This is necessary, because some browsers can call the focus handlers asynchronously,
@@ -28,8 +37,8 @@ function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') {
2837
// TODO: Check if this element focus patching is still needed for local testing,
2938
// where browser is not necessarily focused.
3039
export function patchElementFocus(element: HTMLElement) {
31-
element.focus = () => dispatchFakeEvent(element, 'focus');
32-
element.blur = () => dispatchFakeEvent(element, 'blur');
40+
element.focus = () => simulateFocusSequence(element, 'focus');
41+
element.blur = () => simulateFocusSequence(element, 'blur');
3342
}
3443

3544
/** @docs-private */

src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
3838
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
3939
// a little earlier. This avoids issues where IE delays the focusing of the input.
4040
'(focusin)': '_handleFocus()',
41-
'(blur)': '_onTouched()',
41+
'(blur)': '_handleBlur()',
4242
'(input)': '_handleInput($event)',
4343
'(keydown)': '_handleKeydown($event)',
4444
},

src/material-experimental/mdc-autocomplete/autocomplete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@ describe('MDC-based MatAutocomplete', () => {
891891
});
892892

893893
it('should mark the autocomplete control as touched on blur', () => {
894-
fixture.componentInstance.trigger.openPanel();
894+
dispatchFakeEvent(input, 'focusin');
895895
fixture.detectChanges();
896896
expect(fixture.componentInstance.stateCtrl.touched)
897897
.withContext(`Expected control to start out untouched.`)

src/material/autocomplete/autocomplete-trigger.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export abstract class _MatAutocompleteTriggerBase
104104
private _portal: TemplatePortal;
105105
private _componentDestroyed = false;
106106
private _autocompleteDisabled = false;
107+
private _hasFocus: boolean;
107108
private _scrollStrategy: () => ScrollStrategy;
108109

109110
/** Old value of the native input. Used to work around issues with the `input` event on IE. */
@@ -434,12 +435,25 @@ export abstract class _MatAutocompleteTriggerBase
434435
}
435436

436437
_handleFocus(): void {
437-
if (!this._canOpenOnNextFocus) {
438-
this._canOpenOnNextFocus = true;
439-
} else if (this._canOpen()) {
440-
this._previousValue = this._element.nativeElement.value;
441-
this._attachOverlay();
442-
this._floatLabel(true);
438+
// Normally the event won't fire again if the input already
439+
// has focus, but it may receive a fake event during tests.
440+
if (!this._hasFocus) {
441+
this._hasFocus = true;
442+
443+
if (!this._canOpenOnNextFocus) {
444+
this._canOpenOnNextFocus = true;
445+
} else if (this._canOpen()) {
446+
this._previousValue = this._element.nativeElement.value;
447+
this._attachOverlay();
448+
this._floatLabel(true);
449+
}
450+
}
451+
}
452+
453+
_handleBlur(): void {
454+
if (this._hasFocus) {
455+
this._hasFocus = false;
456+
this._onTouched();
443457
}
444458
}
445459

@@ -786,7 +800,7 @@ export abstract class _MatAutocompleteTriggerBase
786800
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
787801
// a little earlier. This avoids issues where IE delays the focusing of the input.
788802
'(focusin)': '_handleFocus()',
789-
'(blur)': '_onTouched()',
803+
'(blur)': '_handleBlur()',
790804
'(input)': '_handleInput($event)',
791805
'(keydown)': '_handleKeydown($event)',
792806
},

src/material/autocomplete/autocomplete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ describe('MatAutocomplete', () => {
886886
});
887887

888888
it('should mark the autocomplete control as touched on blur', () => {
889-
fixture.componentInstance.trigger.openPanel();
889+
dispatchFakeEvent(input, 'focusin');
890890
fixture.detectChanges();
891891
expect(fixture.componentInstance.stateCtrl.touched)
892892
.withContext(`Expected control to start out untouched.`)

tools/public_api_guard/material/autocomplete.md

+2
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
202202
closePanel(): void;
203203
connectedTo: _MatAutocompleteOriginBase;
204204
// (undocumented)
205+
_handleBlur(): void;
206+
// (undocumented)
205207
_handleFocus(): void;
206208
// (undocumented)
207209
_handleInput(event: KeyboardEvent): void;

0 commit comments

Comments
 (0)