Skip to content

Commit b90d942

Browse files
authored
feat(material/autocomplete): add the ability to auto-select the active option while navigating (#20699)
Adds the `autoSelectActiveOption` input to `mat-autocomplete` which allows the consumer to opt into the behavior where the autocomplete will assign the active option value as the user is navigating through the list. The value is only propagated to the model once the panel is closed. There are a couple of UX differences when the new option is enabled: 1. If the user presses escape while there's a pending auto-selected option, the value is reverted to the last text they typed before they started navigating. 2. If the user clicks away, tabs away or presses enter while there's a pending option, it will be selected. The aforementioned UX differences are based on the Google search autocomplete and one of the examples from the W3C here: https://www.w3.org/TR/wai-aria-practices-1.1/examples/combobox/aria1.1pattern/listbox-combo.html
1 parent 16dea18 commit b90d942

File tree

5 files changed

+509
-38
lines changed

5 files changed

+509
-38
lines changed

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

+211
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,217 @@ describe('MDC-based MatAutocomplete', () => {
27312731
}));
27322732
});
27332733

2734+
describe('automatically selecting the active option', () => {
2735+
let fixture: ComponentFixture<SimpleAutocomplete>;
2736+
2737+
beforeEach(() => {
2738+
fixture = createComponent(SimpleAutocomplete);
2739+
fixture.detectChanges();
2740+
fixture.componentInstance.trigger.autocomplete.autoSelectActiveOption = true;
2741+
});
2742+
2743+
it(
2744+
'should update the input value as the user is navigating, without changing the model ' +
2745+
'value or closing the panel',
2746+
fakeAsync(() => {
2747+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2748+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2749+
2750+
trigger.openPanel();
2751+
fixture.detectChanges();
2752+
zone.simulateZoneExit();
2753+
fixture.detectChanges();
2754+
2755+
expect(stateCtrl.value).toBeFalsy();
2756+
expect(input.value).toBeFalsy();
2757+
expect(trigger.panelOpen).toBe(true);
2758+
expect(closedSpy).not.toHaveBeenCalled();
2759+
2760+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2761+
fixture.detectChanges();
2762+
2763+
expect(stateCtrl.value).toBeFalsy();
2764+
expect(input.value).toBe('Alabama');
2765+
expect(trigger.panelOpen).toBe(true);
2766+
expect(closedSpy).not.toHaveBeenCalled();
2767+
2768+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2769+
fixture.detectChanges();
2770+
2771+
expect(stateCtrl.value).toBeFalsy();
2772+
expect(input.value).toBe('California');
2773+
expect(trigger.panelOpen).toBe(true);
2774+
expect(closedSpy).not.toHaveBeenCalled();
2775+
}),
2776+
);
2777+
2778+
it('should revert back to the last typed value if the user presses escape', fakeAsync(() => {
2779+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2780+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2781+
2782+
trigger.openPanel();
2783+
fixture.detectChanges();
2784+
zone.simulateZoneExit();
2785+
fixture.detectChanges();
2786+
typeInElement(input, 'al');
2787+
fixture.detectChanges();
2788+
tick();
2789+
2790+
expect(stateCtrl.value).toBe('al');
2791+
expect(input.value).toBe('al');
2792+
expect(trigger.panelOpen).toBe(true);
2793+
expect(closedSpy).not.toHaveBeenCalled();
2794+
2795+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2796+
fixture.detectChanges();
2797+
2798+
expect(stateCtrl.value).toBe('al');
2799+
expect(input.value).toBe('Alabama');
2800+
expect(trigger.panelOpen).toBe(true);
2801+
expect(closedSpy).not.toHaveBeenCalled();
2802+
2803+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2804+
fixture.detectChanges();
2805+
2806+
expect(stateCtrl.value).toBe('al');
2807+
expect(input.value).toBe('al');
2808+
expect(trigger.panelOpen).toBe(false);
2809+
expect(closedSpy).toHaveBeenCalledTimes(1);
2810+
}));
2811+
2812+
it(
2813+
'should clear the input if the user presses escape while there was a pending ' +
2814+
'auto selection and there is no previous value',
2815+
fakeAsync(() => {
2816+
const {trigger, stateCtrl} = fixture.componentInstance;
2817+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2818+
2819+
trigger.openPanel();
2820+
fixture.detectChanges();
2821+
zone.simulateZoneExit();
2822+
fixture.detectChanges();
2823+
2824+
expect(stateCtrl.value).toBeFalsy();
2825+
expect(input.value).toBeFalsy();
2826+
2827+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2828+
fixture.detectChanges();
2829+
2830+
expect(stateCtrl.value).toBeFalsy();
2831+
expect(input.value).toBe('Alabama');
2832+
2833+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2834+
fixture.detectChanges();
2835+
2836+
expect(stateCtrl.value).toBeFalsy();
2837+
expect(input.value).toBeFalsy();
2838+
}),
2839+
);
2840+
2841+
it('should propagate the auto-selected value if the user clicks away', fakeAsync(() => {
2842+
const {trigger, stateCtrl} = fixture.componentInstance;
2843+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2844+
2845+
trigger.openPanel();
2846+
fixture.detectChanges();
2847+
zone.simulateZoneExit();
2848+
fixture.detectChanges();
2849+
2850+
expect(stateCtrl.value).toBeFalsy();
2851+
expect(input.value).toBeFalsy();
2852+
2853+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2854+
fixture.detectChanges();
2855+
2856+
expect(stateCtrl.value).toBeFalsy();
2857+
expect(input.value).toBe('Alabama');
2858+
2859+
dispatchFakeEvent(document, 'click');
2860+
fixture.detectChanges();
2861+
2862+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2863+
expect(input.value).toBe('Alabama');
2864+
}));
2865+
2866+
it('should propagate the auto-selected value if the user tabs away', fakeAsync(() => {
2867+
const {trigger, stateCtrl} = fixture.componentInstance;
2868+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2869+
2870+
trigger.openPanel();
2871+
fixture.detectChanges();
2872+
zone.simulateZoneExit();
2873+
fixture.detectChanges();
2874+
2875+
expect(stateCtrl.value).toBeFalsy();
2876+
expect(input.value).toBeFalsy();
2877+
2878+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2879+
fixture.detectChanges();
2880+
2881+
expect(stateCtrl.value).toBeFalsy();
2882+
expect(input.value).toBe('Alabama');
2883+
2884+
dispatchKeyboardEvent(input, 'keydown', TAB);
2885+
fixture.detectChanges();
2886+
2887+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2888+
expect(input.value).toBe('Alabama');
2889+
}));
2890+
2891+
it('should propagate the auto-selected value if the user presses enter on it', fakeAsync(() => {
2892+
const {trigger, stateCtrl} = fixture.componentInstance;
2893+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2894+
2895+
trigger.openPanel();
2896+
fixture.detectChanges();
2897+
zone.simulateZoneExit();
2898+
fixture.detectChanges();
2899+
2900+
expect(stateCtrl.value).toBeFalsy();
2901+
expect(input.value).toBeFalsy();
2902+
2903+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2904+
fixture.detectChanges();
2905+
2906+
expect(stateCtrl.value).toBeFalsy();
2907+
expect(input.value).toBe('Alabama');
2908+
2909+
dispatchKeyboardEvent(input, 'keydown', ENTER);
2910+
fixture.detectChanges();
2911+
2912+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2913+
expect(input.value).toBe('Alabama');
2914+
}));
2915+
2916+
it('should allow the user to click on an option different from the auto-selected one', fakeAsync(() => {
2917+
const {trigger, stateCtrl} = fixture.componentInstance;
2918+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2919+
2920+
trigger.openPanel();
2921+
fixture.detectChanges();
2922+
zone.simulateZoneExit();
2923+
fixture.detectChanges();
2924+
2925+
expect(stateCtrl.value).toBeFalsy();
2926+
expect(input.value).toBeFalsy();
2927+
2928+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2929+
fixture.detectChanges();
2930+
2931+
expect(stateCtrl.value).toBeFalsy();
2932+
expect(input.value).toBe('Alabama');
2933+
2934+
const options = overlayContainerElement.querySelectorAll(
2935+
'mat-option',
2936+
) as NodeListOf<HTMLElement>;
2937+
options[2].click();
2938+
fixture.detectChanges();
2939+
2940+
expect(stateCtrl.value).toEqual({code: 'FL', name: 'Florida'});
2941+
expect(input.value).toBe('Florida');
2942+
}));
2943+
});
2944+
27342945
it('should have correct width when opened', () => {
27352946
const widthFixture = createComponent(SimpleAutocomplete);
27362947
widthFixture.componentInstance.width = 300;

0 commit comments

Comments
 (0)