Skip to content

Commit 1d23c86

Browse files
committed
fix(material/autocomplete): mark control as touched once panel is closed
Currently we mark the autocomplete control as touched on `blur`. The problem is that the `blur` event happens a split second before the panel is closed which can cause the error styling to show up and disappear quickly which looks glitchy. These changes change it so that the control is marked as touched once the panel is closed. Also makes a couple of underscored properties private since they weren't used anywhere in the view. Fixes #18313.
1 parent 8a12da7 commit 1d23c86

File tree

5 files changed

+129
-6
lines changed

5 files changed

+129
-6
lines changed

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

+61-2
Original file line numberDiff line numberDiff line change
@@ -892,8 +892,6 @@ describe('MDC-based MatAutocomplete', () => {
892892
});
893893

894894
it('should mark the autocomplete control as touched on blur', () => {
895-
fixture.componentInstance.trigger.openPanel();
896-
fixture.detectChanges();
897895
expect(fixture.componentInstance.stateCtrl.touched)
898896
.withContext(`Expected control to start out untouched.`)
899897
.toBe(false);
@@ -906,6 +904,67 @@ describe('MDC-based MatAutocomplete', () => {
906904
.toBe(true);
907905
});
908906

907+
it('should mark the autocomplete control as touched when the panel is closed via the keyboard', fakeAsync(() => {
908+
fixture.componentInstance.trigger.openPanel();
909+
fixture.detectChanges();
910+
zone.simulateZoneExit();
911+
912+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
913+
false,
914+
`Expected control to start out untouched.`,
915+
);
916+
917+
dispatchKeyboardEvent(input, 'keydown', TAB);
918+
fixture.detectChanges();
919+
920+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
921+
true,
922+
`Expected control to become touched on blur.`,
923+
);
924+
}));
925+
926+
it('should mark the autocomplete control as touched when the panel is closed by clicking away', fakeAsync(() => {
927+
fixture.componentInstance.trigger.openPanel();
928+
fixture.detectChanges();
929+
zone.simulateZoneExit();
930+
931+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
932+
false,
933+
`Expected control to start out untouched.`,
934+
);
935+
936+
dispatchFakeEvent(document, 'click');
937+
fixture.detectChanges();
938+
939+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
940+
true,
941+
`Expected control to become touched on blur.`,
942+
);
943+
}));
944+
945+
it(
946+
'should not mark the autocomplete control as touched when the panel is closed ' +
947+
'programmatically',
948+
fakeAsync(() => {
949+
fixture.componentInstance.trigger.openPanel();
950+
fixture.detectChanges();
951+
zone.simulateZoneExit();
952+
953+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
954+
false,
955+
`Expected control to start out untouched.`,
956+
);
957+
958+
fixture.componentInstance.trigger.closePanel();
959+
fixture.detectChanges();
960+
961+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
962+
false,
963+
`Expected control to stay untouched.`,
964+
);
965+
}),
966+
);
967+
909968
it('should disable the input when used with a value accessor and without `matInput`', () => {
910969
fixture.destroy();
911970
TestBed.resetTestingModule();

src/material/autocomplete/autocomplete-trigger.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,12 @@ export abstract class _MatAutocompleteTriggerBase
454454
}
455455
}
456456

457+
_handleBlur() {
458+
if (!this.panelOpen) {
459+
this._onTouched();
460+
}
461+
}
462+
457463
/**
458464
* In "auto" mode, the label will animate down as soon as focus is lost.
459465
* This causes the value to jump when selecting an option with the mouse.
@@ -573,6 +579,7 @@ export abstract class _MatAutocompleteTriggerBase
573579
}
574580

575581
this.closePanel();
582+
this._onTouched();
576583
}
577584

578585
/**
@@ -797,8 +804,8 @@ export abstract class _MatAutocompleteTriggerBase
797804
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
798805
// a little earlier. This avoids issues where IE delays the focusing of the input.
799806
'(focusin)': '_handleFocus()',
800-
'(blur)': '_onTouched()',
801807
'(input)': '_handleInput($event)',
808+
'(blur)': '_handleBlur()',
802809
'(keydown)': '_handleKeydown($event)',
803810
},
804811
exportAs: 'matAutocompleteTrigger',

src/material/autocomplete/autocomplete.spec.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -887,8 +887,6 @@ describe('MatAutocomplete', () => {
887887
});
888888

889889
it('should mark the autocomplete control as touched on blur', () => {
890-
fixture.componentInstance.trigger.openPanel();
891-
fixture.detectChanges();
892890
expect(fixture.componentInstance.stateCtrl.touched)
893891
.withContext(`Expected control to start out untouched.`)
894892
.toBe(false);
@@ -901,6 +899,63 @@ describe('MatAutocomplete', () => {
901899
.toBe(true);
902900
});
903901

902+
it('should mark the autocomplete control as touched when the panel is closed via the keyboard', fakeAsync(() => {
903+
fixture.componentInstance.trigger.openPanel();
904+
fixture.detectChanges();
905+
zone.simulateZoneExit();
906+
907+
expect(fixture.componentInstance.stateCtrl.touched)
908+
.withContext(`Expected control to start out untouched.`)
909+
.toBe(false);
910+
911+
dispatchKeyboardEvent(input, 'keydown', TAB);
912+
fixture.detectChanges();
913+
914+
expect(fixture.componentInstance.stateCtrl.touched)
915+
.withContext(`Expected control to become touched on blur.`)
916+
.toBe(true);
917+
}));
918+
919+
it('should mark the autocomplete control as touched when the panel is closed by clicking away', fakeAsync(() => {
920+
fixture.componentInstance.trigger.openPanel();
921+
fixture.detectChanges();
922+
zone.simulateZoneExit();
923+
924+
expect(fixture.componentInstance.stateCtrl.touched)
925+
.withContext(`Expected control to start out untouched.`)
926+
.toBe(false);
927+
928+
dispatchFakeEvent(document, 'click');
929+
fixture.detectChanges();
930+
931+
expect(fixture.componentInstance.stateCtrl.touched)
932+
.withContext(`Expected control to become touched on blur.`)
933+
.toBe(true);
934+
}));
935+
936+
it(
937+
'should not mark the autocomplete control as touched when the panel is closed ' +
938+
'programmatically',
939+
fakeAsync(() => {
940+
fixture.componentInstance.trigger.openPanel();
941+
fixture.detectChanges();
942+
zone.simulateZoneExit();
943+
944+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
945+
false,
946+
`Expected control to start out untouched.`,
947+
);
948+
949+
fixture.componentInstance.trigger.closePanel();
950+
fixture.detectChanges();
951+
952+
expect(fixture.componentInstance.stateCtrl.touched).toBe(
953+
false,
954+
`Expected control to stay untouched.`,
955+
);
956+
}),
957+
);
958+
904959
it('should disable the input when used with a value accessor and without `matInput`', () => {
905960
overlayContainer.ngOnDestroy();
906961
fixture.destroy();

tools/public_api_guard/material/autocomplete.md

+2
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
198198
closePanel(): void;
199199
connectedTo: _MatAutocompleteOriginBase;
200200
// (undocumented)
201+
_handleBlur(): void;
202+
// (undocumented)
201203
_handleFocus(): void;
202204
// (undocumented)
203205
_handleInput(event: KeyboardEvent): void;

0 commit comments

Comments
 (0)