import {Directionality} from '@angular/cdk/bidi';
import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
import {Overlay, OverlayContainer, OverlayModule} from '@angular/cdk/overlay';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
  clearElement,
  createKeyboardEvent,
  dispatchEvent,
  dispatchFakeEvent,
  dispatchKeyboardEvent,
  dispatchMouseEvent,
  typeInElement,
} from '@angular/cdk/testing/private';
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  Provider,
  QueryList,
  Type,
  ViewChild,
  ViewChildren,
  ViewEncapsulation,
} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  fakeAsync,
  flush,
  inject,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatOption, MatOptionSelectionChange} from '@angular/material/core';
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {EMPTY, Observable, Subject, Subscription} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {
  MAT_AUTOCOMPLETE_DEFAULT_OPTIONS,
  MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
  MatAutocomplete,
  MatAutocompleteDefaultOptions,
  MatAutocompleteModule,
  MatAutocompleteOrigin,
  MatAutocompleteSelectedEvent,
  MatAutocompleteTrigger,
  getMatAutocompleteMissingPanelError,
} from './index';

describe('MatAutocomplete', () => {
  let overlayContainerElement: HTMLElement;

  // Creates a test component fixture.
  function createComponent<T>(component: Type<T>, providers: Provider[] = []) {
    TestBed.configureTestingModule({
      imports: [
        MatAutocompleteModule,
        MatFormFieldModule,
        MatInputModule,
        FormsModule,
        ReactiveFormsModule,
        NoopAnimationsModule,
        OverlayModule,
      ],
      providers,
      declarations: [component],
    });

    inject([OverlayContainer], (oc: OverlayContainer) => {
      overlayContainerElement = oc.getContainerElement();
    })();

    return TestBed.createComponent<T>(component);
  }

  describe('panel toggling', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;
    let input: HTMLInputElement;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
      input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    });

    it('should open the panel when the input is focused', () => {
      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to start out closed.`)
        .toBe(false);

      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to read open when input is focused.`)
        .toBe(true);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when input is focused.`)
        .toContain('Alabama');
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when input is focused.`)
        .toContain('California');
    });

    it('should not open the panel on focus if the input is readonly', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;
      input.readOnly = true;
      fixture.detectChanges();

      expect(trigger.panelOpen)
        .withContext('Expected panel state to start out closed.')
        .toBe(false);
      dispatchFakeEvent(input, 'focusin');
      flush();

      fixture.detectChanges();
      expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false);
    }));

    it('should not open using the arrow keys when the input is readonly', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;
      input.readOnly = true;
      fixture.detectChanges();

      expect(trigger.panelOpen)
        .withContext('Expected panel state to start out closed.')
        .toBe(false);
      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      flush();

      fixture.detectChanges();
      expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false);
    }));

    it('should open the panel programmatically', () => {
      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to start out closed.`)
        .toBe(false);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to read open when opened programmatically.`)
        .toBe(true);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when opened programmatically.`)
        .toContain('Alabama');
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when opened programmatically.`)
        .toContain('California');
    });

    it('should close the panel when the user clicks away', waitForAsync(async () => {
      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      dispatchFakeEvent(document, 'click');
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected clicking outside the panel to set its state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected clicking outside the panel to close the panel.`)
        .toEqual('');
    }));

    it('should close the panel when the user clicks away via auxiliary button', waitForAsync(async () => {
      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      dispatchFakeEvent(document, 'auxclick');
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected clicking outside the panel to set its state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected clicking outside the panel to close the panel.`)
        .toEqual('');
    }));

    it('should close the panel when the user taps away on a touch device', fakeAsync(() => {
      dispatchFakeEvent(input, 'focus');
      fixture.detectChanges();
      flush();
      dispatchFakeEvent(document, 'touchend');

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected tapping outside the panel to set its state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected tapping outside the panel to close the panel.`)
        .toEqual('');
    }));

    it('should close the panel when an option is clicked', waitForAsync(async () => {
      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
      option.click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected clicking an option to set the panel state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected clicking an option to close the panel.`)
        .toEqual('');
    }));

    it('should close the panel when a newly created option is clicked', waitForAsync(async () => {
      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      // Filter down the option list to a subset of original options ('Alabama', 'California')
      typeInElement(input, 'al');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      let options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[0].click();

      // Changing value from 'Alabama' to 'al' to re-populate the option list,
      // ensuring that 'California' is created new.
      dispatchFakeEvent(input, 'focusin');
      clearElement(input);
      typeInElement(input, 'al');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected clicking a new option to set the panel state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected clicking a new option to close the panel.`)
        .toEqual('');
    }));

    it('should close the panel programmatically', fakeAsync(() => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected closing programmatically to set the panel state to closed.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected closing programmatically to close the panel.`)
        .toEqual('');
    }));

    it('should not throw when attempting to close the panel of a destroyed autocomplete', () => {
      const trigger = fixture.componentInstance.trigger;

      trigger.openPanel();
      fixture.detectChanges();
      fixture.destroy();

      expect(() => trigger.closePanel()).not.toThrow();
    });

    it('should hide the panel when the options list is empty', fakeAsync(() => {
      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();

      const panel = overlayContainerElement.querySelector(
        '.mat-mdc-autocomplete-panel',
      ) as HTMLElement;

      expect(panel.classList)
        .withContext(`Expected panel to start out visible.`)
        .toContain('mat-mdc-autocomplete-visible');

      // Filter down the option list such that no options match the value
      typeInElement(input, 'af');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(panel.classList)
        .withContext(`Expected panel to hide itself when empty.`)
        .toContain('mat-mdc-autocomplete-hidden');
    }));

    it('should keep the label floating until the panel closes', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to float as soon as panel opens.')
        .toEqual('always');

      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to return to auto state after panel closes.')
        .toEqual('auto');
    }));

    it('should not open the panel when the `input` event is invoked on a non-focused input', () => {
      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to start out closed.`)
        .toBe(false);

      input.value = 'Alabama';
      dispatchFakeEvent(input, 'input');
      fixture.detectChanges();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to stay closed.`)
        .toBe(false);
    });

    it('should not mess with label placement if set to never', fakeAsync(() => {
      fixture.componentInstance.floatLabel = 'never';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to stay static.')
        .toEqual('never');
      flush();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to stay in static state after close.')
        .toEqual('never');
    }));

    it('should not mess with label placement if set to always', fakeAsync(() => {
      fixture.componentInstance.floatLabel = 'always';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to stay elevated on open.')
        .toEqual('always');
      flush();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.formField.floatLabel)
        .withContext('Expected label to stay elevated after close.')
        .toEqual('always');
    }));

    it('should toggle the visibility when typing and closing the panel', fakeAsync(() => {
      fixture.componentInstance.trigger.openPanel();
      tick();
      fixture.detectChanges();

      expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList)
        .withContext('Expected panel to be visible.')
        .toContain('mat-mdc-autocomplete-visible');

      typeInElement(input, 'x');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList)
        .withContext('Expected panel to be hidden.')
        .toContain('mat-mdc-autocomplete-hidden');

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      clearElement(input);
      typeInElement(input, 'al');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList)
        .withContext('Expected panel to be visible.')
        .toContain('mat-mdc-autocomplete-visible');
    }));

    it('should animate the label when the input is focused', () => {
      const inputContainer = fixture.componentInstance.formField;

      spyOn(inputContainer, '_animateAndLockLabel');
      expect(inputContainer._animateAndLockLabel).not.toHaveBeenCalled();

      dispatchFakeEvent(fixture.debugElement.query(By.css('input'))!.nativeElement, 'focusin');
      expect(inputContainer._animateAndLockLabel).toHaveBeenCalled();
    });

    it('should provide the open state of the panel', fakeAsync(() => {
      expect(fixture.componentInstance.panel.isOpen).toBeFalsy(
        `Expected the panel to be unopened initially.`,
      );

      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.panel.isOpen).toBeTruthy(
        `Expected the panel to be opened on focus.`,
      );
    }));

    it('should emit an event when the panel is opened', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).toHaveBeenCalled();
    });

    it('should not emit the `opened` event when no options are being shown', () => {
      fixture.componentInstance.filteredStates = fixture.componentInstance.states = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).not.toHaveBeenCalled();
    });

    it('should emit the `opened` event if the options come in after the panel is shown', fakeAsync(() => {
      fixture.componentInstance.filteredStates = fixture.componentInstance.states = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).not.toHaveBeenCalled();

      fixture.componentInstance.filteredStates = fixture.componentInstance.states = [
        {name: 'California', code: 'CA'},
      ];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).toHaveBeenCalled();
    }));

    it('should not emit the opened event multiple times while typing', fakeAsync(() => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).toHaveBeenCalledTimes(1);

      typeInElement(input, 'Alabam');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(fixture.componentInstance.openedSpy).toHaveBeenCalledTimes(1);
    }));

    it('should emit an event when the panel is closed', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.closedSpy).toHaveBeenCalled();
    });

    it('should not emit the `closed` event when no options were shown', () => {
      fixture.componentInstance.filteredStates = fixture.componentInstance.states = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      expect(fixture.componentInstance.closedSpy).not.toHaveBeenCalled();
    });

    it('should not be able to open the panel if the autocomplete is disabled', () => {
      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to start out closed.`)
        .toBe(false);

      fixture.componentInstance.autocompleteDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      dispatchFakeEvent(input, 'focusin');
      fixture.detectChanges();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel to remain closed.`)
        .toBe(false);
    });

    it('should continue to update the model if the autocomplete is disabled', () => {
      fixture.componentInstance.autocompleteDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      typeInElement(input, 'hello');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value).toBe('hello');
    });

    it('should set aria-haspopup depending on whether the autocomplete is disabled', () => {
      expect(input.getAttribute('aria-haspopup')).toBe('listbox');

      fixture.componentInstance.autocompleteDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(input.hasAttribute('aria-haspopup')).toBe(false);
    });

    it('should reopen the panel when clicking on the input', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      input.focus();
      flush();
      fixture.detectChanges();

      expect(document.activeElement).withContext('Expected input to be focused.').toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      trigger.closePanel();
      fixture.detectChanges();

      expect(document.activeElement)
        .withContext('Expected input to continue to be focused.')
        .toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);

      input.click();
      flush();
      fixture.detectChanges();

      expect(trigger.panelOpen).withContext('Expected panel to reopen on click.').toBe(true);
    }));
  });

  it('should not close the panel when clicking on the input', waitForAsync(async () => {
    const fixture = createComponent(SimpleAutocomplete);
    fixture.detectChanges();
    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;

    dispatchFakeEvent(input, 'focusin');
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.trigger.panelOpen)
      .withContext('Expected panel to be opened on focus.')
      .toBe(true);

    input.click();
    fixture.detectChanges();

    expect(fixture.componentInstance.trigger.panelOpen)
      .withContext('Expected panel to remain opened after clicking on the input.')
      .toBe(true);
  }));

  it('should not close the panel when clicking on the input inside shadow DOM', waitForAsync(async () => {
    // This test is only relevant for Shadow DOM-capable browsers.
    if (!_supportsShadowDom()) {
      return;
    }

    const fixture = createComponent(SimpleAutocompleteShadowDom);
    fixture.detectChanges();
    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;

    dispatchFakeEvent(input, 'focusin');
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.trigger.panelOpen)
      .withContext('Expected panel to be opened on focus.')
      .toBe(true);

    input.click();
    fixture.detectChanges();

    expect(fixture.componentInstance.trigger.panelOpen)
      .withContext('Expected panel to remain opened after clicking on the input.')
      .toBe(true);
  }));

  it('should have the correct text direction in RTL', () => {
    const rtlFixture = createComponent(SimpleAutocomplete, [
      {provide: Directionality, useFactory: () => ({value: 'rtl', change: EMPTY})},
    ]);

    rtlFixture.detectChanges();
    rtlFixture.componentInstance.trigger.openPanel();
    rtlFixture.detectChanges();

    const boundingBox = overlayContainerElement.querySelector(
      '.cdk-overlay-connected-position-bounding-box',
    )!;
    expect(boundingBox.getAttribute('dir')).toEqual('rtl');
  });

  it('should update the panel direction if it changes for the trigger', () => {
    const dirProvider = {value: 'rtl', change: EMPTY};
    const rtlFixture = createComponent(SimpleAutocomplete, [
      {provide: Directionality, useFactory: () => dirProvider},
    ]);

    rtlFixture.detectChanges();
    rtlFixture.componentInstance.trigger.openPanel();
    rtlFixture.detectChanges();

    let boundingBox = overlayContainerElement.querySelector(
      '.cdk-overlay-connected-position-bounding-box',
    )!;
    expect(boundingBox.getAttribute('dir')).toEqual('rtl');

    rtlFixture.componentInstance.trigger.closePanel();
    rtlFixture.detectChanges();

    dirProvider.value = 'ltr';
    rtlFixture.componentInstance.trigger.openPanel();
    rtlFixture.detectChanges();

    boundingBox = overlayContainerElement.querySelector(
      '.cdk-overlay-connected-position-bounding-box',
    )!;
    expect(boundingBox.getAttribute('dir')).toEqual('ltr');
  });

  it('should be able to set a custom value for the `autocomplete` attribute', () => {
    const fixture = createComponent(AutocompleteWithNativeAutocompleteAttribute);
    const input = fixture.nativeElement.querySelector('input');

    fixture.detectChanges();

    expect(input.getAttribute('autocomplete')).toBe('changed');
  });

  it('should not throw when typing in an element with a null and disabled autocomplete', () => {
    const fixture = createComponent(InputWithoutAutocompleteAndDisabled);
    fixture.detectChanges();

    expect(() => {
      dispatchKeyboardEvent(fixture.nativeElement.querySelector('input'), 'keydown', SPACE);
      fixture.detectChanges();
    }).not.toThrow();
  });

  it('should clear the selected option if it no longer matches the input text while typing', waitForAsync(async () => {
    const fixture = createComponent(SimpleAutocomplete);
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    // Select an option and reopen the panel.
    (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.options.first.selected).toBe(true);

    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    input.value = '';
    typeInElement(input, 'Ala');
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.options.first.selected).toBe(false);
  }));

  it('should not clear the selected option if it no longer matches the input text while typing with requireSelection', waitForAsync(async () => {
    const fixture = createComponent(SimpleAutocomplete);
    fixture.componentInstance.requireSelection = true;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    // Select an option and reopen the panel.
    (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.options.first.selected).toBe(true);

    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    input.value = '';
    typeInElement(input, 'Ala');
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.options.first.selected).toBe(true);
  }));

  describe('forms integration', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;
    let input: HTMLInputElement;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    });

    it('should update control value as user types with input value', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      typeInElement(input, 'a');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value)
        .withContext('Expected control value to be updated as user types.')
        .toEqual('a');

      typeInElement(input, 'l');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value)
        .withContext('Expected control value to be updated as user types.')
        .toEqual('al');
    }));

    it('should update control value when autofilling', () => {
      // Simulate the browser autofilling the input by setting a value and
      // dispatching an `input` event while the input is out of focus.
      expect(document.activeElement).not.toBe(input, 'Expected input not to have focus.');
      input.value = 'Alabama';
      dispatchFakeEvent(input, 'input');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value)
        .withContext('Expected value to be propagated to the form control.')
        .toBe('Alabama');
    });

    it('should update control value when option is selected with option value', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value)
        .withContext('Expected control value to equal the selected option value.')
        .toEqual({code: 'CA', name: 'California'});
    }));

    it('should update the control back to a string if user types after an option is selected', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      clearElement(input);
      typeInElement(input, 'Californi');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.stateCtrl.value)
        .withContext('Expected control value to revert back to string.')
        .toEqual('Californi');
    }));

    it('should fill the text field with display value when an option is selected', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(input.value)
        .withContext(`Expected text field to fill with selected value.`)
        .toContain('California');
    }));

    it('should fill the text field with value if displayWith is not set', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      fixture.componentInstance.panel.displayWith = null;
      fixture.componentInstance.options.toArray()[1].value = 'test value';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();

      fixture.detectChanges();
      expect(input.value)
        .withContext(`Expected input to fall back to selected option's value.`)
        .toContain('test value');
    }));

    it('should fill the text field correctly if value is set to obj programmatically', fakeAsync(() => {
      fixture.componentInstance.stateCtrl.setValue({code: 'AL', name: 'Alabama'});
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(input.value)
        .withContext(`Expected input to fill with matching option's viewValue.`)
        .toContain('Alabama');
    }));

    it('should clear the text field if value is reset programmatically', fakeAsync(() => {
      typeInElement(input, 'Alabama');
      fixture.detectChanges();
      tick();

      fixture.componentInstance.stateCtrl.reset();
      tick();

      fixture.detectChanges();
      tick();

      expect(input.value).withContext(`Expected input value to be empty after reset.`).toEqual('');
    }));

    it('should clear the previous selection when reactive form field is reset programmatically', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      const clickedOption = options[0];
      const option = fixture.componentInstance.options.first;

      clickedOption.click();
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
      expect(option.selected).toBe(true);

      fixture.componentInstance.stateCtrl.reset();
      await new Promise(r => setTimeout(r));

      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(fixture.componentInstance.stateCtrl.value).toEqual(null);
      expect(option.selected).toBe(false);
    }));

    it('should disable input in view when disabled programmatically', () => {
      const formFieldElement = fixture.debugElement.query(
        By.css('.mat-mdc-form-field'),
      )!.nativeElement;

      expect(input.disabled)
        .withContext(`Expected input to start out enabled in view.`)
        .toBe(false);
      expect(formFieldElement.classList.contains('mat-form-field-disabled'))
        .withContext(`Expected input underline to start out with normal styles.`)
        .toBe(false);

      fixture.componentInstance.stateCtrl.disable();
      fixture.detectChanges();

      expect(input.disabled)
        .withContext(`Expected input to be disabled in view when disabled programmatically.`)
        .toBe(true);
      expect(formFieldElement.classList.contains('mat-form-field-disabled'))
        .withContext(`Expected input underline to display disabled styles.`)
        .toBe(true);
    });

    it('should mark the autocomplete control as dirty as user types', () => {
      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toBe(false);

      typeInElement(input, 'a');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to become dirty when the user types into the input.`)
        .toBe(true);
    });

    it('should mark the autocomplete control as dirty when an option is selected', waitForAsync(async () => {
      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toBe(false);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to become dirty when an option was selected.`)
        .toBe(true);
    }));

    it('should not mark the control dirty when the value is set programmatically', () => {
      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toBe(false);

      fixture.componentInstance.stateCtrl.setValue('AL');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to stay pristine if value is set programmatically.`)
        .toBe(false);
    });

    it('should mark the autocomplete control as touched on blur', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      expect(fixture.componentInstance.stateCtrl.touched)
        .withContext(`Expected control to start out untouched.`)
        .toBe(false);

      dispatchFakeEvent(input, 'blur');
      fixture.detectChanges();

      expect(fixture.componentInstance.stateCtrl.touched)
        .withContext(`Expected control to become touched on blur.`)
        .toBe(true);
    });

    it('should disable the input when used with a value accessor and without `matInput`', () => {
      fixture.destroy();
      TestBed.resetTestingModule();

      const plainFixture = createComponent(PlainAutocompleteInputWithFormControl);
      plainFixture.detectChanges();
      input = plainFixture.nativeElement.querySelector('input');

      expect(input.disabled).toBe(false);

      plainFixture.componentInstance.formControl.disable();
      plainFixture.detectChanges();

      expect(input.disabled).toBe(true);
    });
  });

  describe('with theming', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
    });

    it('should transfer the theme to the autocomplete panel', () => {
      fixture.componentInstance.theme = 'warn';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = overlayContainerElement.querySelector(
        '.mat-mdc-autocomplete-panel',
      )! as HTMLElement;
      expect(panel.classList).toContain('mat-warn');
    });
  });

  describe('keyboard events', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;
    let input: HTMLInputElement;
    let DOWN_ARROW_EVENT: KeyboardEvent;
    let UP_ARROW_EVENT: KeyboardEvent;
    let ENTER_EVENT: KeyboardEvent;

    beforeEach(waitForAsync(async () => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
      UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
      ENTER_EVENT = createKeyboardEvent('keydown', ENTER);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
    }));

    it('should not focus the option when DOWN key is pressed', () => {
      spyOn(fixture.componentInstance.options.first, 'focus');

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      expect(fixture.componentInstance.options.first.focus).not.toHaveBeenCalled();
    });

    it('should not close the panel when DOWN key is pressed', () => {
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to stay open when DOWN key is pressed.`)
        .toBe(true);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to keep displaying when DOWN key is pressed.`)
        .toContain('Alabama');
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to keep displaying when DOWN key is pressed.`)
        .toContain('California');
    });

    it('should set the active item to the first option when DOWN key is pressed', () => {
      const componentInstance = fixture.componentInstance;
      const optionEls = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      expect(componentInstance.trigger.panelOpen)
        .withContext('Expected first down press to open the pane.')
        .toBe(true);

      componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      expect(componentInstance.trigger.activeOption === componentInstance.options.first)
        .withContext('Expected first option to be active.')
        .toBe(true);
      expect(optionEls[0].classList).toContain('mat-mdc-option-active');
      expect(optionEls[1].classList).not.toContain('mat-mdc-option-active');

      componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      expect(componentInstance.trigger.activeOption === componentInstance.options.toArray()[1])
        .withContext('Expected second option to be active.')
        .toBe(true);
      expect(optionEls[0].classList).not.toContain('mat-mdc-option-active');
      expect(optionEls[1].classList).toContain('mat-mdc-option-active');
    });

    it('should set the active item to the last option when UP key is pressed', () => {
      const componentInstance = fixture.componentInstance;
      const optionEls = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      expect(componentInstance.trigger.panelOpen)
        .withContext('Expected first up press to open the pane.')
        .toBe(true);

      componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
      fixture.detectChanges();

      expect(componentInstance.trigger.activeOption === componentInstance.options.last)
        .withContext('Expected last option to be active.')
        .toBe(true);
      expect(optionEls[10].classList).toContain('mat-mdc-option-active');
      expect(optionEls[0].classList).not.toContain('mat-mdc-option-active');

      componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      expect(componentInstance.trigger.activeOption === componentInstance.options.first)
        .withContext('Expected first option to be active.')
        .toBe(true);
      expect(optionEls[0].classList).toContain('mat-mdc-option-active');
    });

    it('should set the active item properly after filtering', fakeAsync(() => {
      const componentInstance = fixture.componentInstance;

      componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      tick();
      fixture.detectChanges();
    }));

    it('should set the active item properly after filtering', () => {
      const componentInstance = fixture.componentInstance;

      typeInElement(input, 'o');
      fixture.detectChanges();

      componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      const optionEls = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      expect(componentInstance.trigger.activeOption === componentInstance.options.first)
        .withContext('Expected first option to be active.')
        .toBe(true);
      expect(optionEls[0].classList).toContain('mat-mdc-option-active');
      expect(optionEls[1].classList).not.toContain('mat-mdc-option-active');
    });

    it('should fill the text field when an option is selected with ENTER', fakeAsync(() => {
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      flush();
      fixture.detectChanges();

      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
      fixture.detectChanges();
      flush();
      expect(input.value)
        .withContext(`Expected text field to fill with selected value on ENTER.`)
        .toContain('Alabama');
    }));

    it('should prevent the default enter key action', fakeAsync(() => {
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      flush();

      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
      flush();

      expect(ENTER_EVENT.defaultPrevented)
        .withContext('Expected the default action to have been prevented.')
        .toBe(true);
    }));

    it('should not prevent the default enter action for a closed panel after a user action', () => {
      fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
      fixture.detectChanges();

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();
      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);

      expect(ENTER_EVENT.defaultPrevented)
        .withContext('Default action should not be prevented.')
        .toBe(false);
    });

    it('should not interfere with the ENTER key when pressing a modifier', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      expect(input.value).withContext('Expected input to start off blank.').toBeFalsy();
      expect(trigger.panelOpen).withContext('Expected panel to start off open.').toBe(true);

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      flush();
      fixture.detectChanges();

      Object.defineProperty(ENTER_EVENT, 'altKey', {get: () => true});
      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
      fixture.detectChanges();

      expect(trigger.panelOpen).withContext('Expected panel to remain open.').toBe(true);
      expect(input.value).withContext('Expected input to remain blank.').toBeFalsy();
      expect(ENTER_EVENT.defaultPrevented)
        .withContext('Expected the default ENTER action not to have been prevented.')
        .toBe(false);
    }));

    it('should fill the text field, not select an option, when SPACE is entered', () => {
      typeInElement(input, 'New');
      fixture.detectChanges();

      const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT);
      fixture.detectChanges();

      expect(input.value)
        .not.withContext(`Expected option not to be selected on SPACE.`)
        .toContain('New York');
    });

    it('should mark the control dirty when selecting an option from the keyboard', fakeAsync(() => {
      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toBe(false);

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      flush();
      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.stateCtrl.dirty)
        .withContext(`Expected control to become dirty when option was selected by ENTER.`)
        .toBe(true);
    }));

    it('should open the panel again when typing after making a selection', fakeAsync(() => {
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      flush();
      fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to read closed after ENTER key.`)
        .toBe(false);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to close after ENTER key.`)
        .toEqual('');

      dispatchFakeEvent(input, 'focusin');
      clearElement(input);
      typeInElement(input, 'Alabama');
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to read open when typing in input.`)
        .toBe(true);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when typing in input.`)
        .toContain('Alabama');
    }));

    it('should not open the panel if the `input` event was dispatched with changing the value', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      dispatchFakeEvent(input, 'focusin');
      typeInElement(input, 'A');
      fixture.detectChanges();
      tick();

      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      trigger.closePanel();
      fixture.detectChanges();

      expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);

      // Dispatch the event without actually changing the value
      // to simulate what happen in some cases on IE.
      dispatchFakeEvent(input, 'input');
      fixture.detectChanges();
      tick();

      expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false);
    }));

    it('should scroll to active options below the fold', () => {
      const trigger = fixture.componentInstance.trigger;
      const scrollContainer = document.querySelector(
        '.cdk-overlay-pane .mat-mdc-autocomplete-panel',
      )!;

      trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();
      expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0);

      // These down arrows will set the 6th option active, below the fold.
      [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));

      // Expect option bottom minus the panel height plus padding (288 - 256 + 8 = 40)
      // Expect option bottom minus the panel height plus padding (288 - 256 + 8 = 40)
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel to reveal the sixth option.`)
        .toEqual(40);
    });

    it('should scroll to active options below if the option height is variable', () => {
      // Make every other option a bit taller than the base of 48.
      fixture.componentInstance.states.forEach((state, index) => {
        if (index % 2 === 0) {
          state.height = 64;
        }
      });
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const trigger = fixture.componentInstance.trigger;
      const scrollContainer = document.querySelector(
        '.cdk-overlay-pane .mat-mdc-autocomplete-panel',
      )!;

      trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();
      expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0);

      // These down arrows will set the 6th option active, below the fold.
      [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));

      // Expect option bottom minus the panel height (336 - 256 + 8 = 88)
      // Expect option bottom minus the panel height (336 - 256 + 8 = 88)
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel to reveal the sixth option.`)
        .toEqual(88);
    });

    it('should scroll to active options on UP arrow', () => {
      const scrollContainer = document.querySelector(
        '.cdk-overlay-pane .mat-mdc-autocomplete-panel',
      )!;
      const initialScrollTop = scrollContainer.scrollTop;

      fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
      fixture.detectChanges();

      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel to reveal last option.`)
        .toBeGreaterThan(initialScrollTop);
    });

    it('should not scroll to active options that are fully in the panel', () => {
      const trigger = fixture.componentInstance.trigger;
      const scrollContainer = document.querySelector(
        '.cdk-overlay-pane .mat-mdc-autocomplete-panel',
      )!;

      trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0);

      // These down arrows will set the 6th option active, below the fold.
      [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));

      // Expect option bottom minus the panel height plus the padding (288 - 256 + 8 = 40)
      // Expect option bottom minus the panel height plus the padding (288 - 256 + 8 = 40)
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel to reveal the sixth option.`)
        .toEqual(40);

      // These up arrows will set the 2nd option active
      [4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT));

      // Expect no scrolling to have occurred. Still showing bottom of 6th option.
      // Expect no scrolling to have occurred. Still showing bottom of 6th option.
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel not to scroll up since sixth option still fully visible.`)
        .toEqual(40);
    });

    it('should scroll to active options that are above the panel', () => {
      const trigger = fixture.componentInstance.trigger;
      const scrollContainer = document.querySelector(
        '.cdk-overlay-pane .mat-mdc-autocomplete-panel',
      )!;

      trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0);

      // These down arrows will set the 7th option active, below the fold.
      [1, 2, 3, 4, 5, 6].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));

      // These up arrows will set the 2nd option active
      [5, 4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT));

      // Expect to show the top of the 2nd option at the top of the panel
      // Expect to show the top of the 2nd option at the top of the panel
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel to scroll up when option is above panel.`)
        .toEqual(56);
    });

    it('should close the panel when pressing escape', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      input.focus();
      flush();
      fixture.detectChanges();

      expect(document.activeElement).withContext('Expected input to be focused.').toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
      fixture.detectChanges();
      flush();

      expect(document.activeElement)
        .withContext('Expected input to continue to be focused.')
        .toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);
    }));

    it('should prevent the default action when pressing escape', () => {
      const escapeEvent = dispatchKeyboardEvent(input, 'keydown', ESCAPE);
      fixture.detectChanges();

      expect(escapeEvent.defaultPrevented).toBe(true);
    });

    it('should not close the panel when pressing escape with a modifier', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      input.focus();
      flush();
      fixture.detectChanges();

      expect(document.activeElement).withContext('Expected input to be focused.').toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, {alt: true});
      fixture.detectChanges();

      expect(document.activeElement)
        .withContext('Expected input to continue to be focused.')
        .toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to stay open.').toBe(true);
      expect(event.defaultPrevented)
        .withContext('Expected default action not to be prevented.')
        .toBe(false);
    }));

    it('should close the panel when pressing ALT + UP_ARROW', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;
      const upArrowEvent = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true});
      spyOn(upArrowEvent, 'stopPropagation').and.callThrough();

      input.focus();
      flush();
      fixture.detectChanges();

      expect(document.activeElement).withContext('Expected input to be focused.').toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      dispatchEvent(document.body, upArrowEvent);
      fixture.detectChanges();
      flush();

      expect(document.activeElement)
        .withContext('Expected input to continue to be focused.')
        .toBe(input);
      expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);
      expect(upArrowEvent.stopPropagation).toHaveBeenCalled();
    }));

    it('should close the panel when tabbing away from a trigger without results', fakeAsync(() => {
      fixture.componentInstance.states = [];
      fixture.componentInstance.filteredStates = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      input.focus();
      flush();

      expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel'))
        .withContext('Expected panel to be rendered.')
        .toBeTruthy();

      dispatchKeyboardEvent(input, 'keydown', TAB);
      fixture.detectChanges();
      tick();

      expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel'))
        .withContext('Expected panel to be removed.')
        .toBeFalsy();
    }));

    it('should not close when a click event occurs on the outside while the panel has focus', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      input.focus();
      flush();
      fixture.detectChanges();

      expect(document.activeElement).toBe(input, 'Expected input to be focused.');
      expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');

      dispatchMouseEvent(document.body, 'click');
      fixture.detectChanges();

      expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
      expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
    }));

    it('should reset the active option when closing with the escape key', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      trigger.openPanel();
      fixture.detectChanges();
      tick();

      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);
      expect(!!trigger.activeOption).withContext('Expected no active option.').toBe(false);

      // Press the down arrow a few times.
      [1, 2, 3].forEach(() => {
        trigger._handleKeydown(DOWN_ARROW_EVENT);
        tick();
        fixture.detectChanges();
      });

      // Note that this casts to a boolean, in order to prevent Jasmine
      // from crashing when trying to stringify the option if the test fails.
      // Note that this casts to a boolean, in order to prevent Jasmine
      // from crashing when trying to stringify the option if the test fails.
      expect(!!trigger.activeOption).withContext('Expected to find an active option.').toBe(true);

      dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
      tick();

      expect(!!trigger.activeOption).withContext('Expected no active options.').toBe(false);
    }));

    it('should reset the active option when closing by selecting with enter', fakeAsync(() => {
      const trigger = fixture.componentInstance.trigger;

      trigger.openPanel();
      fixture.detectChanges();
      tick();

      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);
      expect(!!trigger.activeOption).withContext('Expected no active option.').toBe(false);

      // Press the down arrow a few times.
      [1, 2, 3].forEach(() => {
        trigger._handleKeydown(DOWN_ARROW_EVENT);
        tick();
        fixture.detectChanges();
      });

      // Note that this casts to a boolean, in order to prevent Jasmine
      // from crashing when trying to stringify the option if the test fails.
      // Note that this casts to a boolean, in order to prevent Jasmine
      // from crashing when trying to stringify the option if the test fails.
      expect(!!trigger.activeOption).withContext('Expected to find an active option.').toBe(true);

      trigger._handleKeydown(ENTER_EVENT);
      tick();

      expect(!!trigger.activeOption).withContext('Expected no active options.').toBe(false);
    }));

    it('should not prevent the default action when a modifier key is pressed', () => {
      ['metaKey', 'ctrlKey', 'altKey', 'shiftKey'].forEach(name => {
        const event = createKeyboardEvent('keydown', DOWN_ARROW);
        Object.defineProperty(event, name, {get: () => true});

        fixture.componentInstance.trigger._handleKeydown(event);
        fixture.detectChanges();

        expect(event.defaultPrevented)
          .withContext(`Expected autocomplete not to block ${name} key`)
          .toBe(false);
      });
    });
  });

  describe('option groups', () => {
    let DOWN_ARROW_EVENT: KeyboardEvent;
    let UP_ARROW_EVENT: KeyboardEvent;

    beforeEach(() => {
      DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
      UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
    });

    it('should scroll to active options below the fold', waitForAsync(async () => {
      const fixture = createComponent(AutocompleteWithGroups);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement;

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      expect(container.scrollTop).withContext('Expected the panel not to scroll.').toBe(0);

      // Press the down arrow five times.
      for (const _unused of [1, 2, 3, 4, 5]) {
        fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
        await new Promise(r => setTimeout(r));
      }

      // <option bottom> - <panel height> + <2x group labels> + panel padding = 136
      // 288 - 256 + 96 + 8 = 128
      // <option bottom> - <panel height> + <2x group labels> + panel padding = 136
      // 288 - 256 + 96 + 8 = 128
      expect(container.scrollTop)
        .withContext('Expected panel to reveal the sixth option.')
        .toBe(136);
    }));

    it('should scroll to active options on UP arrow', waitForAsync(async () => {
      const fixture = createComponent(AutocompleteWithGroups);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement;

      fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      // <option bottom> - <panel height> + <3x group label> + panel padding = 472
      // 576 - 256 + 144 + 8 = 472
      // <option bottom> - <panel height> + <3x group label> + panel padding = 472
      // 576 - 256 + 144 + 8 = 472
      expect(container.scrollTop).withContext('Expected panel to reveal last option.').toBe(472);
    }));

    it('should scroll to active options that are above the panel', fakeAsync(() => {
      const fixture = createComponent(AutocompleteWithGroups);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement;

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      tick();
      fixture.detectChanges();
      expect(container.scrollTop).withContext('Expected panel not to scroll.').toBe(0);

      // These down arrows will set the 7th option active, below the fold.
      [1, 2, 3, 4, 5, 6].forEach(() => {
        fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
        tick();
      });

      // These up arrows will set the 2nd option active
      [5, 4, 3, 2, 1].forEach(() => {
        fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
        tick();
      });

      // Expect to show the top of the 2nd option at the top of the panel.
      // It is offset by 56, because there's a group label above it plus the panel padding.
      // Expect to show the top of the 2nd option at the top of the panel.
      // It is offset by 56, because there's a group label above it plus the panel padding.
      expect(container.scrollTop)
        .withContext('Expected panel to scroll up when option is above panel.')
        .toBe(104);
    }));

    it('should scroll back to the top when reaching the first option with preceding group label', fakeAsync(() => {
      const fixture = createComponent(AutocompleteWithGroups);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement;

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      tick();
      fixture.detectChanges();
      expect(container.scrollTop).withContext('Expected the panel not to scroll.').toBe(0);

      // Press the down arrow five times.
      [1, 2, 3, 4, 5].forEach(() => {
        fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
        tick();
      });

      // Press the up arrow five times.
      [1, 2, 3, 4, 5].forEach(() => {
        fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
        tick();
      });

      expect(container.scrollTop).withContext('Expected panel to be scrolled to the top.').toBe(0);
    }));

    it('should scroll to active option when group is indirect descendant', waitForAsync(async () => {
      const fixture = createComponent(AutocompleteWithIndirectGroups);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement;

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      expect(container.scrollTop).withContext('Expected the panel not to scroll.').toBe(0);

      // Press the down arrow five times.
      for (const _unused of [1, 2, 3, 4, 5]) {
        fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
        await new Promise(r => setTimeout(r));
      }

      // <option bottom> - <panel height> + <2x group labels> + panel padding = 128
      // 288 - 256 + 96 + 8 = 136
      // <option bottom> - <panel height> + <2x group labels> + panel padding = 128
      // 288 - 256 + 96 + 8 = 136
      expect(container.scrollTop)
        .withContext('Expected panel to reveal the sixth option.')
        .toBe(136);
    }));
  });

  describe('aria', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;
    let input: HTMLInputElement;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    });

    it('should set role of input to combobox', () => {
      expect(input.getAttribute('role'))
        .withContext('Expected role of input to be combobox.')
        .toEqual('combobox');
    });

    it('should set role of autocomplete panel to listbox', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;

      expect(panel.getAttribute('role'))
        .withContext('Expected role of the panel to be listbox.')
        .toEqual('listbox');
    });

    it('should point the aria-labelledby of the panel to the field label', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;
      const labelId = fixture.nativeElement.querySelector('label').id;
      expect(panel.getAttribute('aria-labelledby')).toBe(labelId);
      expect(panel.hasAttribute('aria-label')).toBe(false);
    });

    it('should add a custom aria-labelledby to the panel', () => {
      fixture.componentInstance.ariaLabelledby = 'myLabelId';
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;
      const labelId = fixture.nativeElement.querySelector('label').id;
      expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`);
      expect(panel.hasAttribute('aria-label')).toBe(false);
    });

    it('should trim aria-labelledby if the input does not have a label', () => {
      fixture.componentInstance.hasLabel = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      fixture.componentInstance.ariaLabelledby = 'myLabelId';
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;
      expect(panel.getAttribute('aria-labelledby')).toBe(`myLabelId`);
    });

    it('should clear aria-labelledby from the panel if an aria-label is set', () => {
      fixture.componentInstance.ariaLabel = 'My label';
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;
      expect(panel.getAttribute('aria-label')).toBe('My label');
      expect(panel.hasAttribute('aria-labelledby')).toBe(false);
    });

    it('should clear aria-labelledby if the form field does not have a label', () => {
      fixture.componentInstance.hasLabel = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;
      expect(panel.hasAttribute('aria-labelledby')).toBe(false);
    });

    it('should support setting a custom aria-label', () => {
      fixture.componentInstance.ariaLabel = 'Custom Label';
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;

      expect(panel.getAttribute('aria-label')).toEqual('Custom Label');
      expect(panel.hasAttribute('aria-labelledby')).toBe(false);
    });

    it('should set aria-autocomplete to list', () => {
      expect(input.getAttribute('aria-autocomplete'))
        .withContext('Expected aria-autocomplete attribute to equal list.')
        .toEqual('list');
    });

    it('should set aria-activedescendant based on the active option', fakeAsync(() => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(input.hasAttribute('aria-activedescendant'))
        .withContext('Expected aria-activedescendant to be absent if no active item.')
        .toBe(false);

      const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      tick();
      fixture.detectChanges();

      expect(input.getAttribute('aria-activedescendant'))
        .withContext('Expected aria-activedescendant to match the active item after 1 down arrow.')
        .toEqual(fixture.componentInstance.options.first.id);

      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      tick();
      fixture.detectChanges();

      expect(input.getAttribute('aria-activedescendant'))
        .withContext('Expected aria-activedescendant to match the active item after 2 down arrows.')
        .toEqual(fixture.componentInstance.options.toArray()[1].id);
    }));

    it('should set aria-expanded based on whether the panel is open', () => {
      expect(input.getAttribute('aria-expanded'))
        .withContext('Expected aria-expanded to be false while panel is closed.')
        .toBe('false');

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(input.getAttribute('aria-expanded'))
        .withContext('Expected aria-expanded to be true while panel is open.')
        .toBe('true');

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      expect(input.getAttribute('aria-expanded'))
        .withContext('Expected aria-expanded to be false when panel closes again.')
        .toBe('false');
    });

    it('should set aria-expanded properly when the panel is hidden', fakeAsync(() => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      expect(input.getAttribute('aria-expanded'))
        .withContext('Expected aria-expanded to be true while panel is open.')
        .toBe('true');

      typeInElement(input, 'zz');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(input.getAttribute('aria-expanded'))
        .withContext('Expected aria-expanded to be false when panel hides itself.')
        .toBe('false');
    }));

    it('should set aria-controls based on the attached autocomplete', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panel = fixture.debugElement.query(
        By.css('.mat-mdc-autocomplete-panel'),
      )!.nativeElement;

      expect(input.getAttribute('aria-controls'))
        .withContext('Expected aria-controls to match attached autocomplete.')
        .toBe(panel.getAttribute('id'));
    });

    it('should not set aria-controls while the autocomplete is closed', () => {
      expect(input.getAttribute('aria-controls')).toBeFalsy();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      expect(input.getAttribute('aria-controls')).toBeTruthy();
    });

    it('should restore focus to the input when clicking to select a value', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;

      // Focus the option manually since the synthetic click may not do it.
      option.focus();
      option.click();
      fixture.detectChanges();

      expect(document.activeElement)
        .withContext('Expected focus to be restored to the input.')
        .toBe(input);
    }));

    it('should remove autocomplete-specific aria attributes when autocomplete is disabled', () => {
      fixture.componentInstance.autocompleteDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(input.getAttribute('role')).toBeFalsy();
      expect(input.getAttribute('aria-autocomplete')).toBeFalsy();
      expect(input.getAttribute('aria-expanded')).toBeFalsy();
      expect(input.getAttribute('aria-owns')).toBeFalsy();
    });
  });

  describe('Fallback positions', () => {
    it('should use below positioning by default', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const inputBottom = inputReference.getBoundingClientRect().bottom;
      const panel = overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!;
      const panelTop = panel.getBoundingClientRect().top;

      expect(Math.floor(inputBottom))
        .withContext(`Expected panel top to match input bottom by default.`)
        .toEqual(Math.floor(panelTop));
      expect(panel.classList).not.toContain('mat-mdc-autocomplete-panel-above');
    }));

    it('should reposition the panel on scroll', () => {
      let scrolledSubject = new Subject();
      let spacer = document.createElement('div');
      let fixture = createComponent(SimpleAutocomplete, [
        {
          provide: ScrollDispatcher,
          useValue: {scrolled: () => scrolledSubject},
        },
      ]);

      fixture.detectChanges();

      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;
      spacer.style.height = '1000px';
      document.body.appendChild(spacer);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      window.scroll(0, 100);
      scrolledSubject.next();
      fixture.detectChanges();

      const inputBottom = inputReference.getBoundingClientRect().bottom;
      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const panelTop = panel.getBoundingClientRect().top;

      expect(Math.floor(inputBottom))
        .withContext('Expected panel top to match input bottom after scrolling.')
        .toEqual(Math.floor(panelTop));

      spacer.remove();
      window.scroll(0, 0);
    });

    it('should fall back to above position if panel cannot fit below', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      // Push the autocomplete trigger down so it won't have room to open "below"
      inputReference.style.bottom = '0';
      inputReference.style.position = 'fixed';

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const inputTop = inputReference.getBoundingClientRect().top;
      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const panelBottom = panel.getBoundingClientRect().bottom;

      expect(Math.floor(inputTop))
        .withContext(`Expected panel to fall back to above position.`)
        .toEqual(Math.floor(panelBottom));

      expect(panel.classList).toContain('mat-mdc-autocomplete-panel-above');
    }));

    it('should allow the panel to expand when the number of results increases', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      // Push the element down so it has a little bit of space, but not enough to render.
      inputReference.style.bottom = '10px';
      inputReference.style.position = 'fixed';

      // Type enough to only show one option.
      typeInElement(inputEl, 'California');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      let panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      let initialPanelHeight = panel.getBoundingClientRect().height;

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      // Change the text so we get more than one result.
      clearElement(inputEl);
      typeInElement(inputEl, 'C');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;

      expect(panel.getBoundingClientRect().height).toBeGreaterThan(initialPanelHeight);
    }));

    it('should align panel properly when filtering in "above" position', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      // Push the autocomplete trigger down so it won't have room to open "below"
      inputReference.style.bottom = '0';
      inputReference.style.position = 'fixed';

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      typeInElement(input, 'f');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const inputTop = inputReference.getBoundingClientRect().top;
      const panel = overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!;
      const panelBottom = panel.getBoundingClientRect().bottom;

      expect(Math.floor(inputTop))
        .withContext(`Expected panel to stay aligned after filtering.`)
        .toEqual(Math.floor(panelBottom));
    }));

    it(
      'should fall back to above position when requested if options are added while ' +
        'the panel is open',
      waitForAsync(async () => {
        let fixture = createComponent(SimpleAutocomplete);
        fixture.componentInstance.states = fixture.componentInstance.states.slice(0, 1);
        fixture.componentInstance.filteredStates = fixture.componentInstance.states.slice();
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
        let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

        // Push the element down so it has a little bit of space, but not enough to render.
        inputReference.style.bottom = '100px';
        inputReference.style.position = 'fixed';

        dispatchFakeEvent(inputEl, 'focusin');
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();

        let panel = overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!;
        let inputRect = inputReference.getBoundingClientRect();
        let panelRect = panel.getBoundingClientRect();

        expect(Math.floor(panelRect.top))
          .withContext(`Expected panel top to be below input before repositioning.`)
          .toBe(Math.floor(inputRect.bottom));

        for (let i = 0; i < 20; i++) {
          fixture.componentInstance.filteredStates.push({code: 'FK', name: 'Fake State'});
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();
        }

        // Request a position update now that there are too many suggestions to fit in the viewport.
        fixture.componentInstance.trigger.updatePosition();

        inputRect = inputReference.getBoundingClientRect();
        panelRect = panel.getBoundingClientRect();

        expect(Math.floor(panelRect.bottom))
          .withContext(`Expected panel to fall back to above position after repositioning.`)
          .toBe(Math.floor(inputRect.top));
        await new Promise(r => setTimeout(r));
      }),
    );

    it('should not throw if a panel reposition is requested while the panel is closed', () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      expect(() => fixture.componentInstance.trigger.updatePosition()).not.toThrow();
    });

    it('should be able to force below position even if there is not enough space', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.componentInstance.position = 'below';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      // Push the autocomplete trigger down so it won't have room to open below.
      inputReference.style.bottom = '0';
      inputReference.style.position = 'fixed';

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const inputBottom = inputReference.getBoundingClientRect().bottom;
      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const panelTop = panel.getBoundingClientRect().top;

      expect(Math.floor(inputBottom))
        .withContext('Expected panel to be below the input.')
        .toEqual(Math.floor(panelTop));

      expect(panel.classList).not.toContain('mat-mdc-autocomplete-panel-above');
    }));

    it('should be able to force above position even if there is not enough space', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.componentInstance.position = 'above';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;

      // Push the autocomplete trigger up so it won't have room to open above.
      inputReference.style.top = '0';
      inputReference.style.position = 'fixed';

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const inputTop = inputReference.getBoundingClientRect().top;
      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const panelBottom = panel.getBoundingClientRect().bottom;

      expect(Math.floor(inputTop))
        .withContext('Expected panel to be above the input.')
        .toEqual(Math.floor(panelBottom));

      expect(panel.classList).toContain('mat-mdc-autocomplete-panel-above');
    }));

    it('should handle the position being changed after the first open', waitForAsync(async () => {
      let fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
      let inputReference = fixture.debugElement.query(By.css('.mdc-text-field'))!.nativeElement;
      let openPanel = async () => {
        fixture.componentInstance.trigger.openPanel();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
      };

      // Push the autocomplete trigger down so it won't have room to open below.
      inputReference.style.bottom = '0';
      inputReference.style.position = 'fixed';
      await openPanel();

      let inputRect = inputReference.getBoundingClientRect();
      let panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      let panelRect = panel.getBoundingClientRect();

      expect(Math.floor(inputRect.top))
        .withContext('Expected panel to be above the input.')
        .toEqual(Math.floor(panelRect.bottom));
      expect(panel.classList).toContain('mat-mdc-autocomplete-panel-above');

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      fixture.componentInstance.position = 'below';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      await openPanel();

      inputRect = inputReference.getBoundingClientRect();
      panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      panelRect = panel.getBoundingClientRect();

      expect(Math.floor(inputRect.bottom))
        .withContext('Expected panel to be below the input.')
        .toEqual(Math.floor(panelRect.top));
      expect(panel.classList).not.toContain('mat-mdc-autocomplete-panel-above');
    }));
  });

  describe('Option selection', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
    });

    it('should deselect any other selected option', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      let options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[0].click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      let componentOptions = fixture.componentInstance.options.toArray();
      expect(componentOptions[0].selected)
        .withContext(`Clicked option should be selected.`)
        .toBe(true);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(componentOptions[0].selected)
        .withContext(`Previous option should not be selected.`)
        .toBe(false);
      expect(componentOptions[1].selected)
        .withContext(`New Clicked option should be selected.`)
        .toBe(true);
    }));

    it('should call deselect only on the previous selected option', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      let options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[0].click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      let componentOptions = fixture.componentInstance.options.toArray();
      componentOptions.forEach(option => spyOn(option, 'deselect'));

      expect(componentOptions[0].selected)
        .withContext(`Clicked option should be selected.`)
        .toBe(true);

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
      options[1].click();
      fixture.detectChanges();

      expect(componentOptions[0].deselect).toHaveBeenCalled();
      componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled());
    }));

    it('should be able to preselect the first option', waitForAsync(async () => {
      fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
        .withContext('Expected first option to be highlighted.')
        .toContain('mat-mdc-option-active');
    }));

    it(
      'should skip to the next enabled option if the first one is disabled ' +
        'when using `autoActiveFirstOption`',
      waitForAsync(async () => {
        const testComponent = fixture.componentInstance;
        testComponent.trigger.autocomplete.autoActiveFirstOption = true;
        testComponent.states[0].disabled = true;
        testComponent.states[1].disabled = true;
        testComponent.trigger.openPanel();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();

        expect(overlayContainerElement.querySelectorAll('mat-option')[2].classList)
          .withContext('Expected third option to be highlighted.')
          .toContain('mat-mdc-option-active');
      }),
    );

    it('should not activate any option if all options are disabled', waitForAsync(async () => {
      const testComponent = fixture.componentInstance;
      testComponent.trigger.autocomplete.autoActiveFirstOption = true;
      for (const state of testComponent.states) {
        state.disabled = true;
      }
      testComponent.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      const selectedOptions = overlayContainerElement.querySelectorAll(
        'mat-option.mat-mdc-option-active',
      );
      expect(selectedOptions.length).withContext('expected no options to be active').toBe(0);
    }));

    it('should remove aria-activedescendant when panel is closed with autoActiveFirstOption', waitForAsync(async () => {
      const input: HTMLElement = fixture.nativeElement.querySelector('input');

      expect(input.hasAttribute('aria-activedescendant'))
        .withContext('Expected no active descendant on init.')
        .toBe(false);

      fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(input.getAttribute('aria-activedescendant'))
        .withContext('Expected active descendant while open.')
        .toBeTruthy();

      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      expect(input.hasAttribute('aria-activedescendant'))
        .withContext('Expected no active descendant when closed.')
        .toBe(false);
    }));

    it('should be able to preselect the first option when the floating label is disabled', waitForAsync(async () => {
      fixture.componentInstance.floatLabel = 'never';
      fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      // Note: should not have a detectChanges call here
      // in order for the test to fail when it's supposed to.

      expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
        .withContext('Expected first option to be highlighted.')
        .toContain('mat-mdc-option-active');
    }));

    it('should be able to configure preselecting the first option globally', waitForAsync(async () => {
      fixture.destroy();
      TestBed.resetTestingModule();
      fixture = createComponent(SimpleAutocomplete, [
        {provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: {autoActiveFirstOption: true}},
      ]);

      fixture.detectChanges();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
        .withContext('Expected first option to be highlighted.')
        .toContain('mat-mdc-option-active');
    }));

    it('should handle `optionSelections` being accessed too early', waitForAsync(async () => {
      fixture.destroy();
      fixture = TestBed.createComponent(SimpleAutocomplete);

      let spy = jasmine.createSpy('option selection spy');
      let subscription: Subscription;

      expect(fixture.componentInstance.trigger.autocomplete).toBeFalsy();
      expect(() => {
        subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
      }).not.toThrow();

      fixture.detectChanges();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;

      option.click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
      subscription!.unsubscribe();
    }));

    it('should emit to `optionSelections` if the list of options changes', waitForAsync(async () => {
      const spy = jasmine.createSpy('option selection spy');
      const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
      const openAndSelectFirstOption = async () => {
        fixture.detectChanges();
        fixture.componentInstance.trigger.openPanel();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
      };

      fixture.componentInstance.states = [{code: 'OR', name: 'Oregon'}];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      await openAndSelectFirstOption();
      expect(spy).toHaveBeenCalledTimes(1);

      fixture.componentInstance.states = [{code: 'WV', name: 'West Virginia'}];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      await openAndSelectFirstOption();
      expect(spy).toHaveBeenCalledTimes(2);

      subscription!.unsubscribe();
    }));

    it('should reposition the panel when the amount of options changes', waitForAsync(async () => {
      const flushPosition = async () => {
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
        // Safari seems to require an extra round that other browsers don't.
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();
      };

      await flushPosition();

      let formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
      let inputReference = formField.querySelector('.mdc-text-field');
      let input = inputReference.querySelector('input');

      formField.style.bottom = '100px';
      formField.style.position = 'fixed';

      typeInElement(input, 'Cali');
      await flushPosition();

      const inputBottom = inputReference.getBoundingClientRect().bottom;
      const panel = overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!;
      const panelTop = panel.getBoundingClientRect().top;

      expect(Math.floor(inputBottom))
        .withContext(`Expected panel top to match input bottom when there is only one option.`)
        .toBe(Math.floor(panelTop));

      clearElement(input);
      await flushPosition();

      const inputTop = inputReference.getBoundingClientRect().top;
      const panelBottom = panel.getBoundingClientRect().bottom;

      expect(Math.floor(inputTop))
        .withContext(`Expected panel switch to the above position if the options no longer fit.`)
        .toBe(Math.floor(panelBottom));
    }));

    it('should clear the selected option when the input value is cleared', waitForAsync(async () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const input = fixture.nativeElement.querySelector('input');
      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
      const optionInstance = fixture.componentInstance.options.first;
      const spy = jasmine.createSpy('selectionChange spy');

      option.click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('Alabama');
      expect(optionInstance.selected).toBe(true);

      const subscription = optionInstance.onSelectionChange.subscribe(spy);

      clearElement(input);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('');
      expect(optionInstance.selected).toBe(false);
      expect(spy).not.toHaveBeenCalled();

      subscription.unsubscribe();
    }));

    it('should accept the user selection if they click on an option while selection is required', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      const spy = jasmine.createSpy('optionSelected spy');
      const subscription = trigger.optionSelections.subscribe(spy);

      options[5].click();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('Oregon');
      expect(stateCtrl.value).toEqual({code: 'OR', name: 'Oregon'});
      expect(spy).toHaveBeenCalledTimes(1);

      subscription.unsubscribe();
    }));

    it('should accept the user selection if they press enter on an option while selection is required', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      const spy = jasmine.createSpy('optionSelected spy');
      const subscription = trigger.optionSelections.subscribe(spy);

      dispatchKeyboardEvent(options[5], 'keydown', ENTER);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('Oregon');
      expect(stateCtrl.value).toEqual({code: 'OR', name: 'Oregon'});
      expect(spy).toHaveBeenCalledTimes(1);

      subscription.unsubscribe();
    }));

    it('should accept the user selection if autoSelectActiveOption is enabled', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      trigger.autocomplete.autoSelectActiveOption = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      for (let i = 0; i < 5; i++) {
        dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
        fixture.detectChanges();
      }

      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('New York');
      expect(stateCtrl.value).toEqual({code: 'NY', name: 'New York'});
    }));

    it('should clear the value if selection is required and the user interacted with the panel without selecting anything', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const spy = jasmine.createSpy('optionSelected spy');
      const subscription = trigger.optionSelections.subscribe(spy);

      input.value = 'Cali';
      dispatchKeyboardEvent(input, 'input');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('Cali');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
      expect(spy).not.toHaveBeenCalled();

      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('');
      expect(stateCtrl.value).toBe(null);
      expect(spy).not.toHaveBeenCalled();

      subscription.unsubscribe();
    }));

    it('should preserve the value if a selection is required, but the user opened and closed the panel without interacting with it', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const spy = jasmine.createSpy('optionSelected spy');
      const subscription = trigger.optionSelections.subscribe(spy);

      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
      expect(spy).not.toHaveBeenCalled();
      subscription.unsubscribe();
    }));

    it('should preserve the value if a selection is required, and there are no options', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger, states} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      stateCtrl.setValue(states[1]);
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

      fixture.componentInstance.states = fixture.componentInstance.filteredStates = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const spy = jasmine.createSpy('optionSelected spy');
      const subscription = trigger.optionSelections.subscribe(spy);

      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(input.value).toBe('California');
      expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
      expect(spy).not.toHaveBeenCalled();
      subscription.unsubscribe();
    }));

    it('should clear the value if requireSelection is enabled and the user edits the input before clicking away', waitForAsync(async () => {
      const input = fixture.nativeElement.querySelector('input');
      const {stateCtrl, trigger} = fixture.componentInstance;
      fixture.componentInstance.requireSelection = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      // Simulate opening the input and clicking the first option.
      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(trigger.panelOpen).toBe(false);
      expect(input.value).toBe('Alabama');
      expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});

      // Simulate pressing backspace while focus is still on the input.
      dispatchFakeEvent(input, 'keydown');
      input.value = 'Alabam';
      fixture.detectChanges();
      dispatchFakeEvent(input, 'input');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(trigger.panelOpen).toBe(true);
      expect(input.value).toBe('Alabam');
      expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});

      // Simulate clicking away.
      input.blur();
      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(trigger.panelOpen).toBe(false);
      expect(input.value).toBe('');
      expect(stateCtrl.value).toBe(null);
    }));
  });

  describe('panel closing', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;
    let input: HTMLInputElement;
    let trigger: MatAutocompleteTrigger;
    let closingActionSpy: jasmine.Spy;
    let closingActionsSub: Subscription;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      trigger = fixture.componentInstance.trigger;
      closingActionSpy = jasmine.createSpy('closing action listener');
      closingActionsSub = trigger.panelClosingActions.subscribe(closingActionSpy);

      input = fixture.debugElement.query(By.css('input'))!.nativeElement;

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();
      flush();
    }));

    afterEach(() => {
      closingActionsSub.unsubscribe();
    });

    it('should emit panel close event when clicking away', () => {
      expect(closingActionSpy).not.toHaveBeenCalled();
      dispatchFakeEvent(document, 'click');
      expect(closingActionSpy).toHaveBeenCalledWith(null);
    });

    it('should emit panel close event when tabbing out', () => {
      const tabEvent = createKeyboardEvent('keydown', TAB);
      input.focus();

      expect(closingActionSpy).not.toHaveBeenCalled();
      trigger._handleKeydown(tabEvent);
      expect(closingActionSpy).toHaveBeenCalledWith(null);
    });

    it('should not emit when tabbing away from a closed panel', waitForAsync(async () => {
      const tabEvent = createKeyboardEvent('keydown', TAB);

      input.focus();
      await new Promise(r => setTimeout(r));

      trigger._handleKeydown(tabEvent);

      // Ensure that it emitted once while the panel was open.
      expect(closingActionSpy).toHaveBeenCalledTimes(1);

      trigger._handleKeydown(tabEvent);

      // Ensure that it didn't emit again when tabbing out again.
      expect(closingActionSpy).toHaveBeenCalledTimes(1);
    }));

    it('should emit panel close event when selecting an option', () => {
      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;

      expect(closingActionSpy).not.toHaveBeenCalled();
      option.click();
      expect(closingActionSpy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
    });

    it('should close the panel when pressing escape', () => {
      expect(closingActionSpy).not.toHaveBeenCalled();
      dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
      expect(closingActionSpy).toHaveBeenCalledWith(null);
    });

    // TODO(mmalerba): This test previously only passed because it wasn't properly flushed.
    //  We should figure out if this is indeed the desired behavior, and if so fix the
    //  implementation.
    // tslint:disable-next-line:ban
    xit('should not prevent escape key propagation when there are no options', waitForAsync(async () => {
      fixture.componentInstance.filteredStates = fixture.componentInstance.states = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      const event = createKeyboardEvent('keydown', ESCAPE);
      spyOn(event, 'stopPropagation').and.callThrough();
      dispatchEvent(document.body, event);
      fixture.detectChanges();

      expect(event.stopPropagation).not.toHaveBeenCalled();
    }));
  });

  describe('without matInput', () => {
    let fixture: ComponentFixture<AutocompleteWithNativeInput>;

    beforeEach(() => {
      fixture = createComponent(AutocompleteWithNativeInput);
      fixture.detectChanges();
    });

    it('should not throw when clicking outside', fakeAsync(() => {
      dispatchFakeEvent(fixture.debugElement.query(By.css('input'))!.nativeElement, 'focus');
      fixture.detectChanges();
      flush();

      expect(() => dispatchFakeEvent(document, 'click')).not.toThrow();
    }));
  });

  describe('with panel classes in the default options', () => {
    it('should apply them if provided as string', () => {
      const fixture = createComponent(SimpleAutocomplete, [
        {provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: {overlayPanelClass: 'default1'}},
      ]);

      fixture.detectChanges();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panelClassList = overlayContainerElement.querySelector('.cdk-overlay-pane')!.classList;
      expect(panelClassList).toContain('default1');
    });

    it('should apply them if provided as array', () => {
      const fixture = createComponent(SimpleAutocomplete, [
        {
          provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS,
          useValue: {overlayPanelClass: ['default1', 'default2']},
        },
      ]);

      fixture.detectChanges();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panelClassList = overlayContainerElement.querySelector('.cdk-overlay-pane')!.classList;
      expect(panelClassList).toContain('default1');
      expect(panelClassList).toContain('default2');
    });
  });

  describe('misc', () => {
    it('should allow basic use without any forms directives', () => {
      expect(() => {
        const fixture = createComponent(AutocompleteWithoutForms);
        fixture.detectChanges();

        const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
        typeInElement(input, 'd');
        fixture.detectChanges();

        const options = overlayContainerElement.querySelectorAll(
          'mat-option',
        ) as NodeListOf<HTMLElement>;
        expect(options.length).toBe(1);
      }).not.toThrowError();
    });

    it('should display an empty input when the value is undefined with ngModel', () => {
      const fixture = createComponent(AutocompleteWithNgModel);
      fixture.detectChanges();

      expect(fixture.debugElement.query(By.css('input'))!.nativeElement.value).toBe('');
    });

    it('should display the number when the selected option is the number zero', fakeAsync(() => {
      const fixture = createComponent(AutocompleteWithNumbers);

      fixture.componentInstance.selectedNumber = 0;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(fixture.debugElement.query(By.css('input'))!.nativeElement.value).toBe('0');
    }));

    it('should work when input is wrapped in ngIf', () => {
      const fixture = createComponent(NgIfAutocomplete);
      fixture.detectChanges();

      dispatchFakeEvent(fixture.debugElement.query(By.css('input'))!.nativeElement, 'focusin');
      fixture.detectChanges();

      expect(fixture.componentInstance.trigger.panelOpen)
        .withContext(`Expected panel state to read open when input is focused.`)
        .toBe(true);
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when input is focused.`)
        .toContain('One');
      expect(overlayContainerElement.textContent)
        .withContext(`Expected panel to display when input is focused.`)
        .toContain('Two');
    });

    it('should filter properly with ngIf after setting the active item', () => {
      const fixture = createComponent(NgIfAutocomplete);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
      fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
      fixture.detectChanges();

      const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      typeInElement(input, 'o');
      fixture.detectChanges();

      expect(fixture.componentInstance.matOptions.length).toBe(2);
    });

    it('should throw if the user attempts to open the panel too early', () => {
      const fixture = createComponent(AutocompleteWithoutPanel);
      fixture.detectChanges();

      expect(() => {
        fixture.componentInstance.trigger.openPanel();
      }).toThrow(getMatAutocompleteMissingPanelError());
    });

    it('should not throw on init, even if the panel is not defined', fakeAsync(() => {
      expect(() => {
        const fixture = createComponent(AutocompleteWithoutPanel);
        fixture.componentInstance.control.setValue('Something');
        fixture.detectChanges();
        tick();
      }).not.toThrow();
    }));

    it('should transfer the mat-autocomplete classes to the panel element', fakeAsync(() => {
      const fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      tick();
      fixture.detectChanges();

      const autocomplete = fixture.debugElement.nativeElement.querySelector('mat-autocomplete');
      const panel = overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!;

      expect(autocomplete.classList).not.toContain('class-one');
      expect(autocomplete.classList).not.toContain('class-two');

      expect(panel.classList).toContain('class-one');
      expect(panel.classList).toContain('class-two');
    }));

    it('should remove old classes when the panel class changes', fakeAsync(() => {
      const fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      tick();
      fixture.detectChanges();

      const classList = overlayContainerElement.querySelector(
        '.mat-mdc-autocomplete-panel',
      )!.classList;

      expect(classList).toContain('mat-mdc-autocomplete-visible');
      expect(classList).toContain('class-one');
      expect(classList).toContain('class-two');

      fixture.componentInstance.panelClass = 'class-three class-four';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(classList).not.toContain('class-one');
      expect(classList).not.toContain('class-two');
      expect(classList).toContain('mat-mdc-autocomplete-visible');
      expect(classList).toContain('class-three');
      expect(classList).toContain('class-four');
    }));

    it('should reset correctly when closed programmatically', waitForAsync(async () => {
      const scrolledSubject = new Subject();
      const fixture = createComponent(SimpleAutocomplete, [
        {
          provide: ScrollDispatcher,
          useValue: {scrolled: () => scrolledSubject},
        },
        {
          provide: MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
          useFactory: (overlay: Overlay) => () => overlay.scrollStrategies.close(),
          deps: [Overlay],
        },
      ]);

      fixture.detectChanges();
      const trigger = fixture.componentInstance.trigger;

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

      scrolledSubject.next();
      fixture.detectChanges();

      expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);
    }));

    it('should handle autocomplete being attached to number inputs', () => {
      const fixture = createComponent(AutocompleteWithNumberInputAndNgModel);
      fixture.detectChanges();
      const input = fixture.debugElement.query(By.css('input'))!.nativeElement;

      typeInElement(input, '1337');
      fixture.detectChanges();

      expect(fixture.componentInstance.selectedValue).toBe(1337);
    });

    it('should not focus the option when DOWN key is pressed', () => {
      const fixture = createComponent(SimpleAutocomplete);
      const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      fixture.detectChanges();
      const spy = spyOn(console, 'error');

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      // Note: for some reason the error here gets logged using console.error, rather than being
      // thrown, hence why we use a spy to assert against it, rather than `.not.toThrow`.
      expect(spy).not.toHaveBeenCalled();
    });
  });

  describe('automatically selecting the active option', () => {
    let fixture: ComponentFixture<SimpleAutocomplete>;

    beforeEach(() => {
      fixture = createComponent(SimpleAutocomplete);
      fixture.detectChanges();
      fixture.componentInstance.trigger.autocomplete.autoSelectActiveOption = true;
      fixture.changeDetectorRef.markForCheck();
    });

    it(
      'should update the input value as the user is navigating, without changing the model ' +
        'value or closing the panel',
      waitForAsync(async () => {
        const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
        const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

        trigger.openPanel();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBeFalsy();
        expect(trigger.panelOpen).toBe(true);
        expect(closedSpy).not.toHaveBeenCalled();

        dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBe('Alabama');
        expect(trigger.panelOpen).toBe(true);
        expect(closedSpy).not.toHaveBeenCalled();

        dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBe('California');
        expect(trigger.panelOpen).toBe(true);
        expect(closedSpy).not.toHaveBeenCalled();
      }),
    );

    it('should revert back to the last typed value if the user presses escape', waitForAsync(async () => {
      const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();
      typeInElement(input, 'al');
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));

      expect(stateCtrl.value).toBe('al');
      expect(input.value).toBe('al');
      expect(trigger.panelOpen).toBe(true);
      expect(closedSpy).not.toHaveBeenCalled();

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(stateCtrl.value).toBe('al');
      expect(input.value).toBe('Alabama');
      expect(trigger.panelOpen).toBe(true);
      expect(closedSpy).not.toHaveBeenCalled();

      dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
      fixture.detectChanges();

      expect(stateCtrl.value).toBe('al');
      expect(input.value).toBe('al');
      expect(trigger.panelOpen).toBe(false);
      expect(closedSpy).toHaveBeenCalledTimes(1);
    }));

    it('should emit a closed event if no option is displayed', fakeAsync(() => {
      const {openedSpy, closedSpy} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      typeInElement(input, 'Alabama'); // Valid option
      fixture.detectChanges();
      tick();

      expect(openedSpy).toHaveBeenCalledTimes(1);
      expect(closedSpy).toHaveBeenCalledTimes(0);

      typeInElement(input, '_x'); // Invalidate option to 'Alabama_x'
      fixture.detectChanges();
      tick();

      expect(openedSpy).toHaveBeenCalledTimes(1);
      expect(closedSpy).toHaveBeenCalledTimes(1);
    }));

    it(
      'should clear the input if the user presses escape while there was a pending ' +
        'auto selection and there is no previous value',
      waitForAsync(async () => {
        const {trigger, stateCtrl} = fixture.componentInstance;
        const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

        trigger.openPanel();
        fixture.detectChanges();
        await new Promise(r => setTimeout(r));
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBeFalsy();

        dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBe('Alabama');

        dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
        fixture.detectChanges();

        expect(stateCtrl.value).toBeFalsy();
        expect(input.value).toBeFalsy();
      }),
    );

    it('should propagate the auto-selected value if the user clicks away', waitForAsync(async () => {
      const {trigger, stateCtrl} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBeFalsy();

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBe('Alabama');

      dispatchFakeEvent(document, 'click');
      fixture.detectChanges();

      expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
      expect(input.value).toBe('Alabama');
    }));

    it('should propagate the auto-selected value if the user tabs away', waitForAsync(async () => {
      const {trigger, stateCtrl} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBeFalsy();

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBe('Alabama');

      dispatchKeyboardEvent(input, 'keydown', TAB);
      fixture.detectChanges();

      expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
      expect(input.value).toBe('Alabama');
    }));

    it('should propagate the auto-selected value if the user presses enter on it', waitForAsync(async () => {
      const {trigger, stateCtrl} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBeFalsy();

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBe('Alabama');

      dispatchKeyboardEvent(input, 'keydown', ENTER);
      fixture.detectChanges();

      expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
      expect(input.value).toBe('Alabama');
    }));

    it('should allow the user to click on an option different from the auto-selected one', waitForAsync(async () => {
      const {trigger, stateCtrl} = fixture.componentInstance;
      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      trigger.openPanel();
      fixture.detectChanges();
      await new Promise(r => setTimeout(r));
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBeFalsy();

      dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(stateCtrl.value).toBeFalsy();
      expect(input.value).toBe('Alabama');

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;
      options[2].click();
      fixture.detectChanges();

      expect(stateCtrl.value).toEqual({code: 'FL', name: 'Florida'});
      expect(input.value).toBe('Florida');
    }));
  });

  it('should have correct width when opened', () => {
    const widthFixture = createComponent(SimpleAutocomplete);
    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
    // Firefox, Edge return a decimal value for width, so we need to parse and round it to verify
    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(300);

    widthFixture.componentInstance.trigger.closePanel();
    widthFixture.detectChanges();

    widthFixture.componentInstance.width = 500;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    // Firefox, Edge return a decimal value for width, so we need to parse and round it to verify
    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500);
  });

  it('should update the width while the panel is open', () => {
    const widthFixture = createComponent(SimpleAutocomplete);

    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
    const input = widthFixture.debugElement.query(By.css('input'))!.nativeElement;

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(300);

    widthFixture.componentInstance.width = 500;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    input.focus();
    dispatchFakeEvent(input, 'input');
    widthFixture.detectChanges();

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500);
  });

  it('should not reopen a closed autocomplete when returning to a blurred tab', () => {
    const fixture = createComponent(SimpleAutocomplete);
    fixture.detectChanges();

    const trigger = fixture.componentInstance.trigger;
    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;

    input.focus();
    fixture.detectChanges();

    expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);

    trigger.closePanel();
    fixture.detectChanges();

    expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);

    // Simulate the user going to a different tab.
    dispatchFakeEvent(window, 'blur');
    input.blur();
    fixture.detectChanges();

    // Simulate the user coming back.
    dispatchFakeEvent(window, 'focus');
    input.focus();
    fixture.detectChanges();

    expect(trigger.panelOpen).withContext('Expected panel to remain closed.').toBe(false);
  });

  it('should update the panel width if the window is resized', fakeAsync(() => {
    const widthFixture = createComponent(SimpleAutocomplete);

    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(300);

    widthFixture.componentInstance.width = 400;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    dispatchFakeEvent(window, 'resize');
    tick(20);

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(400);
  }));

  it('should have panel width match host width by default', () => {
    const widthFixture = createComponent(SimpleAutocomplete);

    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(300);
  });

  it('should have panel width set to string value', () => {
    const widthFixture = createComponent(SimpleAutocomplete);

    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.autocomplete.panelWidth = 'auto';
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

    expect(overlayPane.style.width).toBe('auto');
  });

  it('should have panel width set to number value', () => {
    const widthFixture = createComponent(SimpleAutocomplete);

    widthFixture.componentInstance.width = 300;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.detectChanges();

    widthFixture.componentInstance.trigger.autocomplete.panelWidth = 400;
    widthFixture.changeDetectorRef.markForCheck();
    widthFixture.componentInstance.trigger.openPanel();
    widthFixture.detectChanges();

    const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

    expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(400);
  });

  it('should show the panel when the options are initialized later within a component with OnPush change detection', fakeAsync(() => {
    let fixture = createComponent(AutocompleteWithOnPushDelay);
    fixture.detectChanges();
    flush();
    dispatchFakeEvent(fixture.debugElement.query(By.css('input'))!.nativeElement, 'focusin');
    tick(1000);

    fixture.detectChanges();
    tick();

    Promise.resolve().then(() => {
      let panel = overlayContainerElement.querySelector(
        '.mat-mdc-autocomplete-panel',
      ) as HTMLElement;
      let visibleClass = 'mat-mdc-autocomplete-visible';

      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(panel.classList).withContext(`Expected panel to be visible.`).toContain(visibleClass);
    });
  }));

  it('should emit an event when an option is selected', waitForAsync(async () => {
    let fixture = createComponent(AutocompleteWithSelectEvent);

    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
    let spy = fixture.componentInstance.optionSelected;

    options[1].click();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    expect(spy).toHaveBeenCalledTimes(1);

    let event = spy.calls.mostRecent().args[0] as MatAutocompleteSelectedEvent;

    expect(event.source).toBe(fixture.componentInstance.autocomplete);
    expect(event.option.value).toBe('Washington');
  }));

  it('should refocus the input after the selection event is emitted', waitForAsync(async () => {
    const events: string[] = [];
    const fixture = createComponent(AutocompleteWithSelectEvent);
    fixture.detectChanges();
    const input = fixture.nativeElement.querySelector('input');

    fixture.componentInstance.trigger.openPanel();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    const options = overlayContainerElement.querySelectorAll(
      'mat-option',
    ) as NodeListOf<HTMLElement>;
    spyOn(input, 'focus').and.callFake(() => events.push('focus'));
    fixture.componentInstance.optionSelected.and.callFake(() => events.push('select'));

    options[1].click();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    expect(events).toEqual(['select', 'focus']);
  }));

  it('should emit an event when a newly-added option is selected', fakeAsync(() => {
    let fixture = createComponent(AutocompleteWithSelectEvent);

    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    tick();
    fixture.detectChanges();

    fixture.componentInstance.states.push('Puerto Rico');
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
    let spy = fixture.componentInstance.optionSelected;

    options[3].click();
    tick();
    fixture.detectChanges();

    expect(spy).toHaveBeenCalledTimes(1);

    let event = spy.calls.mostRecent().args[0] as MatAutocompleteSelectedEvent;

    expect(event.source).toBe(fixture.componentInstance.autocomplete);
    expect(event.option.value).toBe('Puerto Rico');
  }));

  it('should emit an event when an option is activated', waitForAsync(async () => {
    const fixture = createComponent(AutocompleteWithActivatedEvent);

    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    const input = fixture.nativeElement.querySelector('input');
    const spy = fixture.componentInstance.optionActivated;
    const autocomplete = fixture.componentInstance.autocomplete;
    const options = fixture.componentInstance.options.toArray();

    expect(spy).not.toHaveBeenCalled();

    dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
    fixture.detectChanges();
    expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[0]});

    dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
    fixture.detectChanges();
    expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[1]});

    dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
    fixture.detectChanges();
    expect(spy.calls.mostRecent().args[0]).toEqual({source: autocomplete, option: options[2]});
  }));

  it('should not emit the optionActivated event when the active option is reset', waitForAsync(async () => {
    const fixture = createComponent(AutocompleteWithActivatedEvent);

    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    await new Promise(r => setTimeout(r));
    fixture.detectChanges();

    const input = fixture.nativeElement.querySelector('input');
    const spy = fixture.componentInstance.optionActivated;

    expect(spy).not.toHaveBeenCalled();

    dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
    fixture.detectChanges();
    expect(spy).toHaveBeenCalledTimes(1);

    dispatchKeyboardEvent(input, 'keydown', ESCAPE);
    fixture.detectChanges();
    expect(spy).toHaveBeenCalledTimes(1);
  }));

  it('should be able to set a custom panel connection element', waitForAsync(async () => {
    const fixture = createComponent(AutocompleteWithDifferentOrigin);

    fixture.detectChanges();
    fixture.componentInstance.connectedTo = fixture.componentInstance.alternateOrigin;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    const overlayRect = overlayContainerElement
      .querySelector('.cdk-overlay-pane')!
      .getBoundingClientRect();
    const originRect = fixture.nativeElement.querySelector('.origin').getBoundingClientRect();

    expect(Math.floor(overlayRect.top))
      .withContext('Expected autocomplete panel to align with the bottom of the new origin.')
      .toBe(Math.floor(originRect.bottom));
  }));

  it('should be able to change the origin after the panel has been opened', waitForAsync(async () => {
    const fixture = createComponent(AutocompleteWithDifferentOrigin);

    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    fixture.componentInstance.trigger.closePanel();
    fixture.detectChanges();

    fixture.componentInstance.connectedTo = fixture.componentInstance.alternateOrigin;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    const overlayRect = overlayContainerElement
      .querySelector('.cdk-overlay-pane')!
      .getBoundingClientRect();
    const originRect = fixture.nativeElement.querySelector('.origin').getBoundingClientRect();

    expect(Math.floor(overlayRect.top))
      .withContext('Expected autocomplete panel to align with the bottom of the new origin.')
      .toBe(Math.floor(originRect.bottom));
  }));

  it('should be able to re-type the same value when it is reset while open', fakeAsync(() => {
    const fixture = createComponent(SimpleAutocomplete);
    fixture.detectChanges();
    const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
    const formControl = fixture.componentInstance.stateCtrl;

    typeInElement(input, 'Cal');
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    expect(formControl.value)
      .withContext('Expected initial value to be propagated to model')
      .toBe('Cal');

    formControl.setValue('');
    fixture.detectChanges();

    expect(input.value).withContext('Expected input value to reset when model is reset').toBe('');

    typeInElement(input, 'Cal');
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    expect(formControl.value)
      .withContext('Expected new value to be propagated to model')
      .toBe('Cal');
  }));

  it('should not close when clicking inside alternate origin', waitForAsync(async () => {
    const fixture = createComponent(AutocompleteWithDifferentOrigin);
    fixture.detectChanges();
    fixture.componentInstance.connectedTo = fixture.componentInstance.alternateOrigin;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    fixture.componentInstance.trigger.openPanel();
    fixture.detectChanges();
    await new Promise(r => setTimeout(r));

    expect(fixture.componentInstance.trigger.panelOpen).toBe(true);

    const origin = fixture.nativeElement.querySelector('.origin');
    origin.click();
    fixture.detectChanges();

    expect(fixture.componentInstance.trigger.panelOpen).toBe(true);
  }));

  describe('a11y', () => {
    it('should display checkmark for selection by default', () => {
      const fixture = createComponent(AutocompleteWithNgModel);
      fixture.componentInstance.selectedState = 'New York';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
      fixture.detectChanges();

      const selectedOption = document.querySelector('mat-option[aria-selected="true"]');
      expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
      expect(selectedOption?.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-minimal'))
        .withContext(
          'Expected selection option to have a pseudo-checkbox with "minimal" appearance.',
        )
        .toBeTruthy();
    });
  });

  describe('with token to hide single selection indicator', () => {
    it('should not display checkmark', () => {
      const defaultOptions: MatAutocompleteDefaultOptions = {
        hideSingleSelectionIndicator: true,
      };
      const fixture = createComponent(AutocompleteWithNgModel, [
        {provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: defaultOptions},
      ]);
      fixture.detectChanges();

      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
      fixture.detectChanges();

      const selectedOption = document.querySelector('mat-option[aria-selected="true"]');
      expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
      expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
    });
  });

  describe('when used inside a modal', () => {
    let fixture: ComponentFixture<AutocompleteInsideAModal>;

    beforeEach(() => {
      fixture = createComponent(AutocompleteInsideAModal);
      fixture.detectChanges();
    });

    it('should add the id of the autocomplete panel to the aria-owns of the modal', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panelId = fixture.componentInstance.autocomplete.id;
      const modalElement = fixture.componentInstance.modal.nativeElement;

      expect(modalElement.getAttribute('aria-owns')?.split(' '))
        .withContext('expecting modal to own the autocommplete panel')
        .toContain(panelId);
    });

    it('should remove the aria-owns attribute of the modal when the autocomplete panel closes', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.componentInstance.trigger.closePanel();
      fixture.detectChanges();

      const modalElement = fixture.componentInstance.modal.nativeElement;

      expect(modalElement.getAttribute('aria-owns')).toBeFalsy();
    });

    it('should readd the aria-owns attribute of the modal when the autocomplete panel opens again', () => {
      fixture.componentInstance.trigger.openPanel();
      fixture.componentInstance.trigger.closePanel();
      fixture.componentInstance.trigger.openPanel();
      fixture.detectChanges();

      const panelId = fixture.componentInstance.autocomplete.id;
      const modalElement = fixture.componentInstance.modal.nativeElement;

      expect(modalElement.getAttribute('aria-owns')?.split(' '))
        .withContext('expecting modal to own the autocommplete panel')
        .toContain(panelId);
    });
  });
});

const SIMPLE_AUTOCOMPLETE_TEMPLATE = `
  <mat-form-field [floatLabel]="floatLabel" [style.width.px]="width" [color]="theme">
    @if (hasLabel) {
      <mat-label>State</mat-label>
    }
    <input
      matInput
      placeholder="State"
      [matAutocomplete]="auto"
      [matAutocompletePosition]="position"
      [matAutocompleteDisabled]="autocompleteDisabled"
      [formControl]="stateCtrl">
  </mat-form-field>
  <mat-autocomplete
    #auto="matAutocomplete"
    [class]="panelClass"
    [displayWith]="displayFn"
    [disableRipple]="disableRipple"
    [requireSelection]="requireSelection"
    [aria-label]="ariaLabel"
    [aria-labelledby]="ariaLabelledby"
    (opened)="openedSpy()"
    (closed)="closedSpy()">
    @for (state of filteredStates; track state) {
      <mat-option
        [value]="state"
        [style.height.px]="state.height"
        [disabled]="state.disabled">
        <span>{{ state.code }}: {{ state.name }}</span>
      </mat-option>
    }
  </mat-autocomplete>
`;

@Component({template: SIMPLE_AUTOCOMPLETE_TEMPLATE, standalone: false})
class SimpleAutocomplete implements OnDestroy {
  stateCtrl = new FormControl<{name: string; code: string} | string | null>(null);
  filteredStates: any[];
  valueSub: Subscription;
  floatLabel = 'auto';
  position = 'auto';
  width: number;
  disableRipple = false;
  autocompleteDisabled = false;
  hasLabel = true;
  requireSelection = false;
  ariaLabel: string;
  ariaLabelledby: string;
  panelClass = 'class-one class-two';
  theme: string;
  openedSpy = jasmine.createSpy('autocomplete opened spy');
  closedSpy = jasmine.createSpy('autocomplete closed spy');

  @ViewChild(MatAutocompleteTrigger, {static: true}) trigger: MatAutocompleteTrigger;
  @ViewChild(MatAutocomplete) panel: MatAutocomplete;
  @ViewChild(MatFormField) formField: MatFormField;
  @ViewChildren(MatOption) options: QueryList<MatOption>;

  states: {code: string; name: string; height?: number; disabled?: boolean}[] = [
    {code: 'AL', name: 'Alabama'},
    {code: 'CA', name: 'California'},
    {code: 'FL', name: 'Florida'},
    {code: 'KS', name: 'Kansas'},
    {code: 'MA', name: 'Massachusetts'},
    {code: 'NY', name: 'New York'},
    {code: 'OR', name: 'Oregon'},
    {code: 'PA', name: 'Pennsylvania'},
    {code: 'TN', name: 'Tennessee'},
    {code: 'VA', name: 'Virginia'},
    {code: 'WY', name: 'Wyoming'},
  ];

  constructor() {
    this.filteredStates = this.states;
    this.valueSub = this.stateCtrl.valueChanges.subscribe(val => {
      this.filteredStates = val
        ? this.states.filter(s => s.name.match(new RegExp(val as string, 'gi')))
        : this.states;
    });
  }

  displayFn(value: any): string {
    return value ? value.name : value;
  }

  ngOnDestroy() {
    this.valueSub.unsubscribe();
  }
}

@Component({
  template: SIMPLE_AUTOCOMPLETE_TEMPLATE,
  encapsulation: ViewEncapsulation.ShadowDom,
  standalone: false,
})
class SimpleAutocompleteShadowDom extends SimpleAutocomplete {}

@Component({
  template: `
    @if (isVisible) {
<mat-form-field>
      <input matInput placeholder="Choose" [matAutocomplete]="auto" [formControl]="optionCtrl">
    </mat-form-field>
}

    <mat-autocomplete #auto="matAutocomplete">
      @for (option of filteredOptions | async; track option) {
  <mat-option [value]="option">
         {{option}}
      </mat-option>
}
    </mat-autocomplete>
  `,
  standalone: false,
})
class NgIfAutocomplete {
  optionCtrl = new FormControl('');
  filteredOptions: Observable<any>;
  isVisible = true;
  options = ['One', 'Two', 'Three'];

  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChildren(MatOption) matOptions: QueryList<MatOption>;

  constructor() {
    this.filteredOptions = this.optionCtrl.valueChanges.pipe(
      startWith(null),
      map(val => {
        return val
          ? this.options.filter(option => new RegExp(val, 'gi').test(option))
          : this.options.slice();
      }),
    );
  }
}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="State" [matAutocomplete]="auto"
      (input)="onInput($event.target?.value)">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (state of filteredStates; track state) {
        <mat-option [value]="state">
          <span> {{ state }}  </span>
        </mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithoutForms {
  filteredStates: any[];
  states = ['Alabama', 'California', 'Florida'];

  constructor() {
    this.filteredStates = this.states.slice();
  }

  onInput(value: any) {
    this.filteredStates = this.states.filter(s => new RegExp(value, 'gi').test(s));
  }
}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="State" [matAutocomplete]="auto" [(ngModel)]="selectedState"
      (ngModelChange)="onInput($event)">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (state of filteredStates; track state) {
        <mat-option [value]="state">
          <span>{{ state }}</span>
        </mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithNgModel {
  filteredStates: any[];
  selectedState: string;
  states = ['New York', 'Washington', 'Oregon'];

  @ViewChild(MatAutocompleteTrigger, {static: true}) trigger: MatAutocompleteTrigger;

  constructor() {
    this.filteredStates = this.states.slice();
  }

  onInput(value: any) {
    this.filteredStates = this.states.filter(s => new RegExp(value, 'gi').test(s));
  }
}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="Number" [matAutocomplete]="auto" [(ngModel)]="selectedNumber">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (number of numbers; track number) {
        <mat-option [value]="number">
          <span>{{ number }}</span>
        </mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithNumbers {
  selectedNumber: number;
  numbers = [0, 1, 2];
}

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <mat-form-field>
      <input type="text" matInput [matAutocomplete]="auto">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (option of options; track option) {
        <mat-option [value]="option">{{ option }}</mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithOnPushDelay implements OnInit {
  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  options: string[];

  ngOnInit() {
    setTimeout(() => {
      this.options = ['One'];
    }, 1000);
  }
}

@Component({
  template: `
    <input placeholder="Choose" [matAutocomplete]="auto" [formControl]="optionCtrl">

    <mat-autocomplete #auto="matAutocomplete">
      @for (option of filteredOptions | async; track option) {
        <mat-option [value]="option">{{option}}</mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithNativeInput {
  optionCtrl = new FormControl('');
  filteredOptions: Observable<any>;
  options = ['En', 'To', 'Tre', 'Fire', 'Fem'];

  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChildren(MatOption) matOptions: QueryList<MatOption>;

  constructor() {
    this.filteredOptions = this.optionCtrl.valueChanges.pipe(
      startWith(null),
      map(val => {
        return val
          ? this.options.filter(option => new RegExp(val, 'gi').test(option))
          : this.options.slice();
      }),
    );
  }
}

@Component({
  template: `<input placeholder="Choose" [matAutocomplete]="auto" [formControl]="control">`,
  standalone: false,
})
class AutocompleteWithoutPanel {
  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  control = new FormControl('');
}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="State" [matAutocomplete]="auto" [(ngModel)]="selectedState">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (group of stateGroups; track group) {
        <mat-optgroup [label]="group.title">
          @for (state of group.states; track state) {
            <mat-option [value]="state">
              <span>{{ state }}</span>
            </mat-option>
          }
        </mat-optgroup>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithGroups {
  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  selectedState: string;
  stateGroups = [
    {
      title: 'One',
      states: ['Alabama', 'California', 'Florida', 'Oregon'],
    },
    {
      title: 'Two',
      states: ['Kansas', 'Massachusetts', 'New York', 'Pennsylvania'],
    },
    {
      title: 'Three',
      states: ['Tennessee', 'Virginia', 'Wyoming', 'Alaska'],
    },
  ];
}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="State" [matAutocomplete]="auto" [(ngModel)]="selectedState">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @if (true) {
        @for (group of stateGroups; track group) {
          <mat-optgroup [label]="group.title">
            @for (state of group.states; track state) {
              <mat-option [value]="state">
                <span>{{ state }}</span>
              </mat-option>
            }
          </mat-optgroup>
        }
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithIndirectGroups extends AutocompleteWithGroups {}

@Component({
  template: `
    <mat-form-field>
      <input matInput placeholder="State" [matAutocomplete]="auto" [(ngModel)]="selectedState">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected($event)">
      @for (state of states; track state) {
        <mat-option [value]="state">
          <span>{{ state }}</span>
        </mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithSelectEvent {
  selectedState: string;
  states = ['New York', 'Washington', 'Oregon'];
  optionSelected = jasmine.createSpy('optionSelected callback');

  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete;
}

@Component({
  template: `
    <input [formControl]="formControl" [matAutocomplete]="auto"/>
    <mat-autocomplete #auto="matAutocomplete"></mat-autocomplete>
  `,
  standalone: false,
})
class PlainAutocompleteInputWithFormControl {
  formControl = new FormControl('');
}

@Component({
  template: `
    <mat-form-field>
      <input type="number" matInput [matAutocomplete]="auto" [(ngModel)]="selectedValue">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      @for (value of values; track value) {
        <mat-option [value]="value">{{value}}</mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithNumberInputAndNgModel {
  selectedValue: number;
  values = [1, 2, 3];
}

@Component({
  template: `
    <div>
      <mat-form-field>
        <input
          matInput
          [matAutocomplete]="auto"
          [matAutocompleteConnectedTo]="connectedTo"
          [(ngModel)]="selectedValue">
      </mat-form-field>
    </div>

    <div
      class="origin"
      matAutocompleteOrigin
      #origin="matAutocompleteOrigin"
      style="margin-top: 50px">
      Connection element
    </div>

    <mat-autocomplete #auto="matAutocomplete">
      @for (value of values; track value) {
        <mat-option [value]="value">{{value}}</mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithDifferentOrigin {
  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChild(MatAutocompleteOrigin) alternateOrigin: MatAutocompleteOrigin;
  selectedValue: string;
  values = ['one', 'two', 'three'];
  connectedTo?: MatAutocompleteOrigin;
}

@Component({
  template: `
    <input autocomplete="changed" [(ngModel)]="value" [matAutocomplete]="auto"/>
    <mat-autocomplete #auto="matAutocomplete"></mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithNativeAutocompleteAttribute {
  value: string;
}

@Component({
  template: '<input [matAutocomplete]="null" matAutocompleteDisabled>',
  standalone: false,
})
class InputWithoutAutocompleteAndDisabled {}

@Component({
  template: `
    <mat-form-field>
      <input matInput [matAutocomplete]="auto">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete" (optionActivated)="optionActivated($event)">
      @for (state of states; track state) {
        <mat-option [value]="state">{{ state }}</mat-option>
      }
    </mat-autocomplete>
  `,
  standalone: false,
})
class AutocompleteWithActivatedEvent {
  states = ['California', 'West Virginia', 'Florida'];
  optionActivated = jasmine.createSpy('optionActivated callback');

  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'autocomplete-inside-a-modal',
  template: `
    <button cdkOverlayOrigin #trigger="cdkOverlayOrigin">open dialog</button>
    <ng-template cdkConnectedOverlay [cdkConnectedOverlayOpen]="true"
      [cdkConnectedOverlayOrigin]="trigger">
      <div role="dialog" [attr.aria-modal]="'true'" #modal>
        <mat-form-field>
          <mat-label>Food</mat-label>
          <input matInput [matAutocomplete]="reactiveAuto" [formControl]="formControl">
        </mat-form-field>
        <mat-autocomplete #reactiveAuto="matAutocomplete">
          @for (food of foods; track food; let index = $index) {
            <mat-option [value]="food">{{food.viewValue}}</mat-option>
          }
        </mat-autocomplete>
      </div>
    </ng-template>
  `,
  standalone: false,
})
class AutocompleteInsideAModal {
  foods = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];

  formControl = new FormControl();

  @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete;
  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
  @ViewChild('modal') modal: ElementRef;
}
