Skip to content

Commit 22534f4

Browse files
committed
feat(material/autocomplete): add the ability to auto-select the active option while navigating
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 03485cd commit 22534f4

File tree

5 files changed

+503
-39
lines changed

5 files changed

+503
-39
lines changed

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

+206
Original file line numberDiff line numberDiff line change
@@ -2625,6 +2625,212 @@ describe('MDC-based MatAutocomplete', () => {
26252625
}));
26262626
});
26272627

2628+
describe('automatically selecting the active option', () => {
2629+
let fixture: ComponentFixture<SimpleAutocomplete>;
2630+
2631+
beforeEach(() => {
2632+
fixture = createComponent(SimpleAutocomplete);
2633+
fixture.detectChanges();
2634+
fixture.componentInstance.trigger.autocomplete.autoSelectActiveOption = true;
2635+
});
2636+
2637+
it('should update the input value as the user is navigating, without changing the model ' +
2638+
'value or closing the panel', fakeAsync(() => {
2639+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2640+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2641+
2642+
trigger.openPanel();
2643+
fixture.detectChanges();
2644+
zone.simulateZoneExit();
2645+
fixture.detectChanges();
2646+
2647+
expect(stateCtrl.value).toBeFalsy();
2648+
expect(input.value).toBeFalsy();
2649+
expect(trigger.panelOpen).toBe(true);
2650+
expect(closedSpy).not.toHaveBeenCalled();
2651+
2652+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2653+
fixture.detectChanges();
2654+
2655+
expect(stateCtrl.value).toBeFalsy();
2656+
expect(input.value).toBe('Alabama');
2657+
expect(trigger.panelOpen).toBe(true);
2658+
expect(closedSpy).not.toHaveBeenCalled();
2659+
2660+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2661+
fixture.detectChanges();
2662+
2663+
expect(stateCtrl.value).toBeFalsy();
2664+
expect(input.value).toBe('California');
2665+
expect(trigger.panelOpen).toBe(true);
2666+
expect(closedSpy).not.toHaveBeenCalled();
2667+
}));
2668+
2669+
it('should revert back to the last typed value if the user presses escape', fakeAsync(() => {
2670+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2671+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2672+
2673+
trigger.openPanel();
2674+
fixture.detectChanges();
2675+
zone.simulateZoneExit();
2676+
fixture.detectChanges();
2677+
typeInElement(input, 'al');
2678+
fixture.detectChanges();
2679+
tick();
2680+
2681+
expect(stateCtrl.value).toBe('al');
2682+
expect(input.value).toBe('al');
2683+
expect(trigger.panelOpen).toBe(true);
2684+
expect(closedSpy).not.toHaveBeenCalled();
2685+
2686+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2687+
fixture.detectChanges();
2688+
2689+
expect(stateCtrl.value).toBe('al');
2690+
expect(input.value).toBe('Alabama');
2691+
expect(trigger.panelOpen).toBe(true);
2692+
expect(closedSpy).not.toHaveBeenCalled();
2693+
2694+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2695+
fixture.detectChanges();
2696+
2697+
expect(stateCtrl.value).toBe('al');
2698+
expect(input.value).toBe('al');
2699+
expect(trigger.panelOpen).toBe(false);
2700+
expect(closedSpy).toHaveBeenCalledTimes(1);
2701+
}));
2702+
2703+
it('should clear the input if the user presses escape while there was a pending ' +
2704+
'auto selection and there is no previous value', fakeAsync(() => {
2705+
const {trigger, stateCtrl} = fixture.componentInstance;
2706+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2707+
2708+
trigger.openPanel();
2709+
fixture.detectChanges();
2710+
zone.simulateZoneExit();
2711+
fixture.detectChanges();
2712+
2713+
expect(stateCtrl.value).toBeFalsy();
2714+
expect(input.value).toBeFalsy();
2715+
2716+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2717+
fixture.detectChanges();
2718+
2719+
expect(stateCtrl.value).toBeFalsy();
2720+
expect(input.value).toBe('Alabama');
2721+
2722+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2723+
fixture.detectChanges();
2724+
2725+
expect(stateCtrl.value).toBeFalsy();
2726+
expect(input.value).toBeFalsy();
2727+
}));
2728+
2729+
it('should propagate the auto-selected value if the user clicks away', fakeAsync(() => {
2730+
const {trigger, stateCtrl} = fixture.componentInstance;
2731+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2732+
2733+
trigger.openPanel();
2734+
fixture.detectChanges();
2735+
zone.simulateZoneExit();
2736+
fixture.detectChanges();
2737+
2738+
expect(stateCtrl.value).toBeFalsy();
2739+
expect(input.value).toBeFalsy();
2740+
2741+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2742+
fixture.detectChanges();
2743+
2744+
expect(stateCtrl.value).toBeFalsy();
2745+
expect(input.value).toBe('Alabama');
2746+
2747+
dispatchFakeEvent(document, 'click');
2748+
fixture.detectChanges();
2749+
2750+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2751+
expect(input.value).toBe('Alabama');
2752+
}));
2753+
2754+
it('should propagate the auto-selected value if the user tabs away', fakeAsync(() => {
2755+
const {trigger, stateCtrl} = fixture.componentInstance;
2756+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2757+
2758+
trigger.openPanel();
2759+
fixture.detectChanges();
2760+
zone.simulateZoneExit();
2761+
fixture.detectChanges();
2762+
2763+
expect(stateCtrl.value).toBeFalsy();
2764+
expect(input.value).toBeFalsy();
2765+
2766+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2767+
fixture.detectChanges();
2768+
2769+
expect(stateCtrl.value).toBeFalsy();
2770+
expect(input.value).toBe('Alabama');
2771+
2772+
dispatchKeyboardEvent(input, 'keydown', TAB);
2773+
fixture.detectChanges();
2774+
2775+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2776+
expect(input.value).toBe('Alabama');
2777+
}));
2778+
2779+
it('should propagate the auto-selected value if the user presses enter on it', fakeAsync(() => {
2780+
const {trigger, stateCtrl} = fixture.componentInstance;
2781+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2782+
2783+
trigger.openPanel();
2784+
fixture.detectChanges();
2785+
zone.simulateZoneExit();
2786+
fixture.detectChanges();
2787+
2788+
expect(stateCtrl.value).toBeFalsy();
2789+
expect(input.value).toBeFalsy();
2790+
2791+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2792+
fixture.detectChanges();
2793+
2794+
expect(stateCtrl.value).toBeFalsy();
2795+
expect(input.value).toBe('Alabama');
2796+
2797+
dispatchKeyboardEvent(input, 'keydown', ENTER);
2798+
fixture.detectChanges();
2799+
2800+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2801+
expect(input.value).toBe('Alabama');
2802+
}));
2803+
2804+
it('should allow the user to click on an option different from the auto-selected one',
2805+
fakeAsync(() => {
2806+
const {trigger, stateCtrl} = fixture.componentInstance;
2807+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2808+
2809+
trigger.openPanel();
2810+
fixture.detectChanges();
2811+
zone.simulateZoneExit();
2812+
fixture.detectChanges();
2813+
2814+
expect(stateCtrl.value).toBeFalsy();
2815+
expect(input.value).toBeFalsy();
2816+
2817+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2818+
fixture.detectChanges();
2819+
2820+
expect(stateCtrl.value).toBeFalsy();
2821+
expect(input.value).toBe('Alabama');
2822+
2823+
const options =
2824+
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
2825+
options[2].click();
2826+
fixture.detectChanges();
2827+
2828+
expect(stateCtrl.value).toEqual({code: 'FL', name: 'Florida'});
2829+
expect(input.value).toBe('Florida');
2830+
}));
2831+
2832+
});
2833+
26282834
it('should have correct width when opened', () => {
26292835
const widthFixture = createComponent(SimpleAutocomplete);
26302836
widthFixture.componentInstance.width = 300;

0 commit comments

Comments
 (0)