Skip to content

Commit 2f649ef

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 92863cc commit 2f649ef

File tree

6 files changed

+38
-13
lines changed

6 files changed

+38
-13
lines changed

Diff for: 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 */

Diff for: 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
'(click)': '_handleClick()',

Diff for: src/material-experimental/mdc-autocomplete/autocomplete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ describe('MDC-based MatAutocomplete', () => {
917917
});
918918

919919
it('should mark the autocomplete control as touched on blur', () => {
920-
fixture.componentInstance.trigger.openPanel();
920+
dispatchFakeEvent(input, 'focusin');
921921
fixture.detectChanges();
922922
expect(fixture.componentInstance.stateCtrl.touched)
923923
.withContext(`Expected control to start out untouched.`)

Diff for: 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. */
@@ -465,12 +466,25 @@ export abstract class _MatAutocompleteTriggerBase
465466
}
466467

467468
_handleFocus(): void {
468-
if (!this._canOpenOnNextFocus) {
469-
this._canOpenOnNextFocus = true;
470-
} else if (this._canOpen()) {
471-
this._previousValue = this._element.nativeElement.value;
472-
this._attachOverlay();
473-
this._floatLabel(true);
469+
// Normally the event won't fire again if the input already
470+
// has focus, but it may receive a fake event during tests.
471+
if (!this._hasFocus) {
472+
this._hasFocus = true;
473+
474+
if (!this._canOpenOnNextFocus) {
475+
this._canOpenOnNextFocus = true;
476+
} else if (this._canOpen()) {
477+
this._previousValue = this._element.nativeElement.value;
478+
this._attachOverlay();
479+
this._floatLabel(true);
480+
}
481+
}
482+
}
483+
484+
_handleBlur(): void {
485+
if (this._hasFocus) {
486+
this._hasFocus = false;
487+
this._onTouched();
474488
}
475489
}
476490

@@ -836,7 +850,7 @@ export abstract class _MatAutocompleteTriggerBase
836850
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
837851
// a little earlier. This avoids issues where IE delays the focusing of the input.
838852
'(focusin)': '_handleFocus()',
839-
'(blur)': '_onTouched()',
853+
'(blur)': '_handleBlur()',
840854
'(input)': '_handleInput($event)',
841855
'(keydown)': '_handleKeydown($event)',
842856
'(click)': '_handleClick()',

Diff for: src/material/autocomplete/autocomplete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ describe('MatAutocomplete', () => {
912912
});
913913

914914
it('should mark the autocomplete control as touched on blur', () => {
915-
fixture.componentInstance.trigger.openPanel();
915+
dispatchFakeEvent(input, 'focusin');
916916
fixture.detectChanges();
917917
expect(fixture.componentInstance.stateCtrl.touched)
918918
.withContext(`Expected control to start out untouched.`)

Diff for: tools/public_api_guard/material/autocomplete.md

+2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
201201
closePanel(): void;
202202
connectedTo: _MatAutocompleteOriginBase;
203203
// (undocumented)
204+
_handleBlur(): void;
205+
// (undocumented)
204206
_handleClick(): void;
205207
// (undocumented)
206208
_handleFocus(): void;

0 commit comments

Comments
 (0)