Skip to content

Commit 716fdec

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 b17ed9d commit 716fdec

File tree

5 files changed

+502
-37
lines changed

5 files changed

+502
-37
lines changed

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

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

24652465
});
24662466

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

0 commit comments

Comments
 (0)