Skip to content

Commit 6d5c3b9

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 7cc42f5 commit 6d5c3b9

File tree

5 files changed

+501
-37
lines changed

5 files changed

+501
-37
lines changed

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

+206
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,212 @@ describe('MDC-based MatAutocomplete', () => {
24632463

24642464
});
24652465

2466+
describe('automatically selecting the active option', () => {
2467+
let fixture: ComponentFixture<SimpleAutocomplete>;
2468+
2469+
beforeEach(() => {
2470+
fixture = createComponent(SimpleAutocomplete);
2471+
fixture.detectChanges();
2472+
fixture.componentInstance.trigger.autocomplete.autoSelectActiveOption = true;
2473+
});
2474+
2475+
it('should update the input value as the user is navigating, without changing the model ' +
2476+
'value or closing the panel', fakeAsync(() => {
2477+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2478+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2479+
2480+
trigger.openPanel();
2481+
fixture.detectChanges();
2482+
zone.simulateZoneExit();
2483+
fixture.detectChanges();
2484+
2485+
expect(stateCtrl.value).toBeFalsy();
2486+
expect(input.value).toBeFalsy();
2487+
expect(trigger.panelOpen).toBe(true);
2488+
expect(closedSpy).not.toHaveBeenCalled();
2489+
2490+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2491+
fixture.detectChanges();
2492+
2493+
expect(stateCtrl.value).toBeFalsy();
2494+
expect(input.value).toBe('Alabama');
2495+
expect(trigger.panelOpen).toBe(true);
2496+
expect(closedSpy).not.toHaveBeenCalled();
2497+
2498+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2499+
fixture.detectChanges();
2500+
2501+
expect(stateCtrl.value).toBeFalsy();
2502+
expect(input.value).toBe('California');
2503+
expect(trigger.panelOpen).toBe(true);
2504+
expect(closedSpy).not.toHaveBeenCalled();
2505+
}));
2506+
2507+
it('should revert back to the last typed value if the user presses escape', fakeAsync(() => {
2508+
const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
2509+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2510+
2511+
trigger.openPanel();
2512+
fixture.detectChanges();
2513+
zone.simulateZoneExit();
2514+
fixture.detectChanges();
2515+
typeInElement(input, 'al');
2516+
fixture.detectChanges();
2517+
tick();
2518+
2519+
expect(stateCtrl.value).toBe('al');
2520+
expect(input.value).toBe('al');
2521+
expect(trigger.panelOpen).toBe(true);
2522+
expect(closedSpy).not.toHaveBeenCalled();
2523+
2524+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2525+
fixture.detectChanges();
2526+
2527+
expect(stateCtrl.value).toBe('al');
2528+
expect(input.value).toBe('Alabama');
2529+
expect(trigger.panelOpen).toBe(true);
2530+
expect(closedSpy).not.toHaveBeenCalled();
2531+
2532+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2533+
fixture.detectChanges();
2534+
2535+
expect(stateCtrl.value).toBe('al');
2536+
expect(input.value).toBe('al');
2537+
expect(trigger.panelOpen).toBe(false);
2538+
expect(closedSpy).toHaveBeenCalledTimes(1);
2539+
}));
2540+
2541+
it('should clear the input if the user presses escape while there was a pending ' +
2542+
'auto selection and there is no previous value', fakeAsync(() => {
2543+
const {trigger, stateCtrl} = fixture.componentInstance;
2544+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2545+
2546+
trigger.openPanel();
2547+
fixture.detectChanges();
2548+
zone.simulateZoneExit();
2549+
fixture.detectChanges();
2550+
2551+
expect(stateCtrl.value).toBeFalsy();
2552+
expect(input.value).toBeFalsy();
2553+
2554+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2555+
fixture.detectChanges();
2556+
2557+
expect(stateCtrl.value).toBeFalsy();
2558+
expect(input.value).toBe('Alabama');
2559+
2560+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
2561+
fixture.detectChanges();
2562+
2563+
expect(stateCtrl.value).toBeFalsy();
2564+
expect(input.value).toBeFalsy();
2565+
}));
2566+
2567+
it('should propagate the auto-selected value if the user clicks away', fakeAsync(() => {
2568+
const {trigger, stateCtrl} = fixture.componentInstance;
2569+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2570+
2571+
trigger.openPanel();
2572+
fixture.detectChanges();
2573+
zone.simulateZoneExit();
2574+
fixture.detectChanges();
2575+
2576+
expect(stateCtrl.value).toBeFalsy();
2577+
expect(input.value).toBeFalsy();
2578+
2579+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2580+
fixture.detectChanges();
2581+
2582+
expect(stateCtrl.value).toBeFalsy();
2583+
expect(input.value).toBe('Alabama');
2584+
2585+
dispatchFakeEvent(document, 'click');
2586+
fixture.detectChanges();
2587+
2588+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2589+
expect(input.value).toBe('Alabama');
2590+
}));
2591+
2592+
it('should propagate the auto-selected value if the user tabs away', fakeAsync(() => {
2593+
const {trigger, stateCtrl} = fixture.componentInstance;
2594+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2595+
2596+
trigger.openPanel();
2597+
fixture.detectChanges();
2598+
zone.simulateZoneExit();
2599+
fixture.detectChanges();
2600+
2601+
expect(stateCtrl.value).toBeFalsy();
2602+
expect(input.value).toBeFalsy();
2603+
2604+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2605+
fixture.detectChanges();
2606+
2607+
expect(stateCtrl.value).toBeFalsy();
2608+
expect(input.value).toBe('Alabama');
2609+
2610+
dispatchKeyboardEvent(input, 'keydown', TAB);
2611+
fixture.detectChanges();
2612+
2613+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2614+
expect(input.value).toBe('Alabama');
2615+
}));
2616+
2617+
it('should propagate the auto-selected value if the user presses enter on it', fakeAsync(() => {
2618+
const {trigger, stateCtrl} = fixture.componentInstance;
2619+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2620+
2621+
trigger.openPanel();
2622+
fixture.detectChanges();
2623+
zone.simulateZoneExit();
2624+
fixture.detectChanges();
2625+
2626+
expect(stateCtrl.value).toBeFalsy();
2627+
expect(input.value).toBeFalsy();
2628+
2629+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2630+
fixture.detectChanges();
2631+
2632+
expect(stateCtrl.value).toBeFalsy();
2633+
expect(input.value).toBe('Alabama');
2634+
2635+
dispatchKeyboardEvent(input, 'keydown', ENTER);
2636+
fixture.detectChanges();
2637+
2638+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2639+
expect(input.value).toBe('Alabama');
2640+
}));
2641+
2642+
it('should allow the user to click on an option different from the auto-selected one',
2643+
fakeAsync(() => {
2644+
const {trigger, stateCtrl} = fixture.componentInstance;
2645+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
2646+
2647+
trigger.openPanel();
2648+
fixture.detectChanges();
2649+
zone.simulateZoneExit();
2650+
fixture.detectChanges();
2651+
2652+
expect(stateCtrl.value).toBeFalsy();
2653+
expect(input.value).toBeFalsy();
2654+
2655+
dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
2656+
fixture.detectChanges();
2657+
2658+
expect(stateCtrl.value).toBeFalsy();
2659+
expect(input.value).toBe('Alabama');
2660+
2661+
const options =
2662+
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
2663+
options[2].click();
2664+
fixture.detectChanges();
2665+
2666+
expect(stateCtrl.value).toEqual({code: 'FL', name: 'Florida'});
2667+
expect(input.value).toBe('Florida');
2668+
}));
2669+
2670+
});
2671+
24662672
it('should have correct width when opened', () => {
24672673
const widthFixture = createComponent(SimpleAutocomplete);
24682674
widthFixture.componentInstance.width = 300;

0 commit comments

Comments
 (0)