sass-references/angular-material/material/select/select.spec.ts

5666 lines
195 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import {LiveAnnouncer} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {
A,
DOWN_ARROW,
END,
ENTER,
ESCAPE,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
SPACE,
TAB,
UP_ARROW,
} from '@angular/cdk/keycodes';
import {OverlayContainer, OverlayModule} from '@angular/cdk/overlay';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
wrappedErrorMessage,
} from '@angular/cdk/testing/private';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DebugElement,
ElementRef,
OnInit,
Provider,
QueryList,
ViewChild,
ViewChildren,
inject,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
fakeAsync,
flush,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {ErrorStateMatcher, MatOption, MatOptionSelectionChange} from '@angular/material/core';
import {
FloatLabelType,
MAT_FORM_FIELD_DEFAULT_OPTIONS,
MatFormFieldModule,
} from '@angular/material/form-field';
import {MAT_SELECT_CONFIG, MatSelectConfig} from '@angular/material/select';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {EMPTY, Observable, Subject, Subscription} from 'rxjs';
import {map} from 'rxjs/operators';
import {MatSelectModule} from './index';
import {MatSelect} from './select';
import {
getMatSelectDynamicMultipleError,
getMatSelectNonArrayValueError,
getMatSelectNonFunctionValueError,
} from './select-errors';
/** Default debounce interval when typing letters to select an option. */
const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL = 200;
describe('MatSelect', () => {
let overlayContainerElement: HTMLElement;
let dir: {value: 'ltr' | 'rtl'; change: Observable<string>};
let scrolledSubject = new Subject();
/**
* Configures the test module for MatSelect with the given declarations. This is broken out so
* that we're only compiling the necessary test components for each test in order to speed up
* overall test time.
* @param declarations Components to declare for this block
* @param providers Additional providers for this block
*/
function configureMatSelectTestingModule(declarations: any[], providers: Provider[] = []) {
TestBed.configureTestingModule({
imports: [
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
FormsModule,
NoopAnimationsModule,
OverlayModule,
],
providers: [
{provide: Directionality, useFactory: () => (dir = {value: 'ltr', change: EMPTY})},
{
provide: ScrollDispatcher,
useFactory: () => ({
scrolled: () => scrolledSubject,
}),
},
...providers,
],
declarations: declarations,
});
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
}
describe('core', () => {
beforeEach(waitForAsync(() => {
configureMatSelectTestingModule([
BasicSelect,
SelectInsideAModal,
MultiSelect,
SelectWithGroups,
SelectWithGroupsAndNgContainer,
SelectWithFormFieldLabel,
SelectWithChangeEvent,
SelectInsideDynamicFormGroup,
]);
}));
describe('accessibility', () => {
describe('for select', () => {
let fixture: ComponentFixture<BasicSelect>;
let select: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
});
it('should set the role of the select to combobox', () => {
expect(select.getAttribute('role')).toEqual('combobox');
expect(select.getAttribute('aria-haspopup')).toBe('listbox');
});
it('should point the aria-controls attribute to the listbox', fakeAsync(() => {
expect(select.hasAttribute('aria-controls')).toBe(false);
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const ariaControls = select.getAttribute('aria-controls');
expect(ariaControls).toBeTruthy();
expect(ariaControls).toBe(document.querySelector('.mat-mdc-select-panel')!.id);
}));
it('should set aria-expanded based on the select open state', fakeAsync(() => {
expect(select.getAttribute('aria-expanded')).toBe('false');
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
expect(select.getAttribute('aria-expanded')).toBe('true');
}));
it('should support setting a custom aria-label', () => {
fixture.componentInstance.ariaLabel = 'Custom Label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.getAttribute('aria-label')).toEqual('Custom Label');
expect(select.hasAttribute('aria-labelledby')).toBeFalsy();
});
it('should be able to add an extra aria-labelledby on top of the default', () => {
fixture.componentInstance.ariaLabelledby = 'myLabelId';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const labelId = fixture.nativeElement.querySelector('label').id;
const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id;
expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId} myLabelId`);
});
it('should set aria-labelledby to the value and label IDs', () => {
fixture.detectChanges();
const labelId = fixture.nativeElement.querySelector('label').id;
const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id;
expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId}`);
});
it('should trim the trigger aria-labelledby when there is no label', fakeAsync(() => {
fixture.componentInstance.hasLabel = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
fixture.detectChanges();
// Note that we assert that there are no spaces around the value.
const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id;
expect(select.getAttribute('aria-labelledby')).toBe(`${valueId}`);
}));
it('should set the tabindex of the select to 0 by default', () => {
expect(select.getAttribute('tabindex')).toEqual('0');
});
it('should set `aria-describedby` to the id of the mat-hint', () => {
expect(select.getAttribute('aria-describedby')).toBeNull();
fixture.componentInstance.hint = 'test';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\w+\d+$/);
});
it('should support user binding to `aria-describedby`', () => {
fixture.componentInstance.ariaDescribedBy = 'test';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.getAttribute('aria-describedby')).toBe('test');
});
it('should be able to override the tabindex', () => {
fixture.componentInstance.tabIndexOverride = 3;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toBe('3');
});
it('should set aria-required for required selects', () => {
expect(select.getAttribute('aria-required'))
.withContext(`Expected aria-required attr to be false for normal selects.`)
.toEqual('false');
fixture.componentInstance.isRequired = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.getAttribute('aria-required'))
.withContext(`Expected aria-required attr to be true for required selects.`)
.toEqual('true');
});
it('should set the mat-select-required class for required selects', () => {
expect(select.classList).not.toContain(
'mat-mdc-select-required',
`Expected the mat-mdc-select-required class not to be set.`,
);
fixture.componentInstance.isRequired = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.classList)
.withContext(`Expected the mat-mdc-select-required class to be set.`)
.toContain('mat-mdc-select-required');
});
it('should set aria-invalid for selects that are invalid and touched', () => {
expect(select.getAttribute('aria-invalid'))
.withContext(`Expected aria-invalid attr to be false for valid selects.`)
.toEqual('false');
fixture.componentInstance.isRequired = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.componentInstance.control.markAsTouched();
fixture.detectChanges();
expect(select.getAttribute('aria-invalid'))
.withContext(`Expected aria-invalid attr to be true for invalid selects.`)
.toEqual('true');
});
it('should set aria-disabled for disabled selects', () => {
expect(select.getAttribute('aria-disabled')).toEqual('false');
fixture.componentInstance.control.disable();
fixture.detectChanges();
expect(select.getAttribute('aria-disabled')).toEqual('true');
});
it('should set the tabindex of the select to -1 if disabled', () => {
fixture.componentInstance.control.disable();
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toEqual('-1');
fixture.componentInstance.control.enable();
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toEqual('0');
});
it('should set `aria-labelledby` to the value ID if there is no form field', () => {
fixture.destroy();
const labelFixture = TestBed.createComponent(SelectWithChangeEvent);
labelFixture.detectChanges();
select = labelFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
const valueId = labelFixture.nativeElement.querySelector('.mat-mdc-select-value').id;
expect(select.getAttribute('aria-labelledby')?.trim()).toBe(valueId);
});
it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(options[0].selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model.')
.toBe(options[0].value);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
// Note that the third option is skipped, because it is disabled.
// Note that the third option is skipped, because it is disabled.
expect(options[3].selected)
.withContext('Expected fourth option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from fourth option to have been set on the model.')
.toBe(options[3].value);
dispatchKeyboardEvent(select, 'keydown', UP_ARROW);
expect(options[1].selected)
.withContext('Expected second option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from second option to have been set on the model.')
.toBe(options[1].value);
flush();
}));
it('should go back to first option if value is reset after interacting using the arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
flush();
expect(options[0].selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model.')
.toBe(options[0].value);
formControl.reset();
fixture.detectChanges();
expect(options[0].selected)
.withContext('Expected first option to be deselected.')
.toBe(false);
expect(formControl.value).withContext('Expected value to be reset.').toBeFalsy();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
flush();
expect(options[0].selected)
.withContext('Expected first option to be selected again.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model again.')
.toBe(options[0].value);
}));
it('should select first/last options via the HOME/END keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const firstOption = fixture.componentInstance.options.first;
const lastOption = fixture.componentInstance.options.last;
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
const endEvent = dispatchKeyboardEvent(select, 'keydown', END);
expect(endEvent.defaultPrevented).toBe(true);
expect(lastOption.selected)
.withContext('Expected last option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from last option to have been set on the model.')
.toBe(lastOption.value);
const homeEvent = dispatchKeyboardEvent(select, 'keydown', HOME);
expect(homeEvent.defaultPrevented).toBe(true);
expect(firstOption.selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model.')
.toBe(firstOption.value);
flush();
}));
it('should select first/last options via the PAGE_DOWN/PAGE_UP keys on a closed select with less than 10 options', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const firstOption = fixture.componentInstance.options.first;
const lastOption = fixture.componentInstance.options.last;
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(-1);
const endEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_DOWN);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
expect(endEvent.defaultPrevented).toBe(true);
expect(lastOption.selected)
.withContext('Expected last option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from last option to have been set on the model.')
.toBe(lastOption.value);
const homeEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_UP);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
expect(homeEvent.defaultPrevented).toBe(true);
expect(firstOption.selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model.')
.toBe(firstOption.value);
flush();
}));
it('should resume focus from selected item after selecting via click', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelectorAll('mat-option')[3] as HTMLElement).click();
fixture.detectChanges();
flush();
expect(formControl.value).toBe(options[3].value);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(formControl.value).toBe(options[4].value);
flush();
}));
it('should select options via LEFT/RIGHT arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
expect(options[0].selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from first option to have been set on the model.')
.toBe(options[0].value);
dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
// Note that the third option is skipped, because it is disabled.
// Note that the third option is skipped, because it is disabled.
expect(options[3].selected)
.withContext('Expected fourth option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from fourth option to have been set on the model.')
.toBe(options[3].value);
dispatchKeyboardEvent(select, 'keydown', LEFT_ARROW);
expect(options[1].selected)
.withContext('Expected second option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from second option to have been set on the model.')
.toBe(options[1].value);
flush();
}));
it('should announce changes via the keyboard on a closed select', fakeAsync(() => {
const liveAnnouncer = TestBed.inject(LiveAnnouncer);
spyOn(liveAnnouncer, 'announce');
dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
expect(liveAnnouncer.announce).toHaveBeenCalledWith('Steak', jasmine.any(Number));
flush();
}));
it('should not throw when reaching a reset option using the arrow keys on a closed select', fakeAsync(() => {
fixture.componentInstance.foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: null, viewValue: 'None'},
];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.componentInstance.control.setValue('steak-0');
expect(() => {
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
}).not.toThrow();
flush();
}));
it('should open a single-selection select using ALT + DOWN_ARROW', () => {
const {control: formControl, select: selectInstance} = fixture.componentInstance;
expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false);
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
dispatchEvent(select, event);
expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true);
expect(formControl.value).withContext('Expected value not to have changed.').toBeFalsy();
});
it('should open a single-selection select using ALT + UP_ARROW', () => {
const {control: formControl, select: selectInstance} = fixture.componentInstance;
expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false);
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true});
dispatchEvent(select, event);
expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true);
expect(formControl.value).withContext('Expected value not to have changed.').toBeFalsy();
});
it('should close when pressing ALT + DOWN_ARROW', () => {
const {select: selectInstance} = fixture.componentInstance;
selectInstance.open();
fixture.detectChanges();
expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true);
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
dispatchEvent(select, event);
expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false);
expect(event.defaultPrevented)
.withContext('Expected default action to be prevented.')
.toBe(true);
});
it('should close when pressing ALT + UP_ARROW', () => {
const {select: selectInstance} = fixture.componentInstance;
selectInstance.open();
fixture.detectChanges();
expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true);
const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true});
dispatchEvent(select, event);
expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false);
expect(event.defaultPrevented)
.withContext('Expected default action to be prevented.')
.toBe(true);
});
it('should be able to select options by typing on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
expect(options[1].selected)
.withContext('Expected second option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from second option to have been set on the model.')
.toBe(options[1].value);
dispatchEvent(select, createKeyboardEvent('keydown', 69, 'e'));
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
expect(options[5].selected)
.withContext('Expected sixth option to be selected.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from sixth option to have been set on the model.')
.toBe(options[5].value);
}));
it('should not open the select when pressing space while typing', fakeAsync(() => {
const selectInstance = fixture.componentInstance.select;
fixture.componentInstance.typeaheadDebounceInterval = DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(selectInstance.panelOpen)
.withContext('Expected select to be closed on init.')
.toBe(false);
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', SPACE);
fixture.detectChanges();
expect(selectInstance.panelOpen)
.withContext('Expected select to remain closed after space was pressed.')
.toBe(false);
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2);
fixture.detectChanges();
expect(selectInstance.panelOpen)
.withContext('Expected select to be closed when the timer runs out.')
.toBe(false);
}));
it('should be able to customize the typeahead debounce interval', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
fixture.componentInstance.typeaheadDebounceInterval = 1337;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
expect(formControl.value)
.withContext('Expected no value after a bit of time has passed.')
.toBeFalsy();
tick(1337);
expect(options[1].selected)
.withContext('Expected second option to be selected after all the time has passed.')
.toBe(true);
expect(formControl.value)
.withContext('Expected value from second option to have been set on the model.')
.toBe(options[1].value);
}));
it('should cancel the typeahead selection on blur', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).withContext('Expected no initial value.').toBeFalsy();
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
dispatchFakeEvent(select, 'blur');
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
expect(options.some(o => o.selected))
.withContext('Expected no options to be selected.')
.toBe(false);
expect(formControl.value).withContext('Expected no value to be assigned.').toBeFalsy();
}));
it('should open the panel when pressing a vertical arrow key on a closed multiple select', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).withContext('Expected panel to be closed.').toBe(false);
const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(instance.select.panelOpen).withContext('Expected panel to be open.').toBe(true);
expect(instance.control.value)
.withContext('Expected value to stay the same.')
.toBe(initialValue);
expect(event.defaultPrevented)
.withContext('Expected default to be prevented.')
.toBe(true);
});
it('should open the panel when pressing a horizontal arrow key on closed multiple select', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).withContext('Expected panel to be closed.').toBe(false);
const event = dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
expect(instance.select.panelOpen).withContext('Expected panel to be open.').toBe(true);
expect(instance.control.value)
.withContext('Expected value to stay the same.')
.toBe(initialValue);
expect(event.defaultPrevented)
.withContext('Expected default to be prevented.')
.toBe(true);
});
it('should do nothing when typing on a closed multi-select', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).withContext('Expected panel to be closed.').toBe(false);
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
expect(instance.select.panelOpen)
.withContext('Expected panel to stay closed.')
.toBe(false);
expect(instance.control.value)
.withContext('Expected value to stay the same.')
.toBe(initialValue);
});
it('should do nothing if the key manager did not change the active item', () => {
const formControl = fixture.componentInstance.control;
expect(formControl.value)
.withContext('Expected form control value to be empty.')
.toBeNull();
expect(formControl.pristine).withContext('Expected form control to be clean.').toBe(true);
dispatchKeyboardEvent(select, 'keydown', 16); // Press a random key.
expect(formControl.value)
.withContext('Expected form control value to stay empty.')
.toBeNull();
expect(formControl.pristine)
.withContext('Expected form control to stay clean.')
.toBe(true);
});
it('should continue from the selected option when the value is set programmatically', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
formControl.setValue('eggs-5');
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(formControl.value).toBe('pasta-6');
expect(fixture.componentInstance.options.toArray()[6].selected).toBe(true);
flush();
}));
it('should not shift focus when the selected options are updated programmatically in a multi select', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
multiFixture.componentInstance.select.open();
multiFixture.detectChanges();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
select.focus();
multiFixture.detectChanges();
multiFixture.componentInstance.select._keyManager.setActiveItem(3);
multiFixture.detectChanges();
expect(document.activeElement)
.withContext('Expected select to have DOM focus.')
.toBe(select);
expect(select.getAttribute('aria-activedescendant'))
.withContext('Expected fourth option to be activated.')
.toBe(options[3].id);
multiFixture.componentInstance.control.setValue(['steak-0', 'sushi-7']);
multiFixture.detectChanges();
expect(document.activeElement)
.withContext('Expected select to have DOM focus.')
.toBe(select);
expect(select.getAttribute('aria-activedescendant'))
.withContext('Expected fourth optino to remain activated.')
.toBe(options[3].id);
});
it('should not cycle through the options if the control is disabled', () => {
const formControl = fixture.componentInstance.control;
formControl.setValue('eggs-5');
formControl.disable();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(formControl.value)
.withContext('Expected value to remain unchaged.')
.toBe('eggs-5');
});
it('should not wrap selection after reaching the end of the options', () => {
const lastOption = fixture.componentInstance.options.last;
fixture.componentInstance.options.forEach(() => {
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
});
expect(lastOption.selected)
.withContext('Expected last option to be selected.')
.toBe(true);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(lastOption.selected)
.withContext('Expected last option to stay selected.')
.toBe(true);
});
it('should not open a multiple select when tabbing through', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
expect(multiFixture.componentInstance.select.panelOpen)
.withContext('Expected panel to be closed initially.')
.toBe(false);
dispatchKeyboardEvent(select, 'keydown', TAB);
expect(multiFixture.componentInstance.select.panelOpen)
.withContext('Expected panel to stay closed.')
.toBe(false);
});
it('should toggle the next option when pressing shift + DOWN_ARROW on a multi-select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {shift: true});
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
multiFixture.componentInstance.select.open();
multiFixture.detectChanges();
flush();
expect(multiFixture.componentInstance.select.value).toBeFalsy();
dispatchEvent(select, event);
multiFixture.detectChanges();
expect(multiFixture.componentInstance.select.value).toEqual(['pizza-1']);
dispatchEvent(select, event);
multiFixture.detectChanges();
expect(multiFixture.componentInstance.select.value).toEqual(['pizza-1', 'tacos-2']);
}));
it('should toggle the previous option when pressing shift + UP_ARROW on a multi-select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {shift: true});
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
multiFixture.componentInstance.select.open();
multiFixture.detectChanges();
flush();
// Move focus down first.
for (let i = 0; i < 5; i++) {
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
multiFixture.detectChanges();
}
expect(multiFixture.componentInstance.select.value).toBeFalsy();
dispatchEvent(select, event);
multiFixture.detectChanges();
expect(multiFixture.componentInstance.select.value).toEqual(['chips-4']);
dispatchEvent(select, event);
multiFixture.detectChanges();
expect(multiFixture.componentInstance.select.value).toEqual(['sandwich-3', 'chips-4']);
}));
it('should prevent the default action when pressing space', () => {
const event = dispatchKeyboardEvent(select, 'keydown', SPACE);
expect(event.defaultPrevented).toBe(true);
});
it('should prevent the default action when pressing enter', () => {
const event = dispatchKeyboardEvent(select, 'keydown', ENTER);
expect(event.defaultPrevented).toBe(true);
});
it('should not prevent the default actions on selection keys when pressing a modifier', () => {
[ENTER, SPACE].forEach(key => {
const event = createKeyboardEvent('keydown', key, undefined, {shift: true});
expect(event.defaultPrevented).toBe(false);
});
});
it('should consider the selection a result of a user action when closed', fakeAsync(() => {
const option = fixture.componentInstance.options.first;
const spy = jasmine.createSpy('option selection spy');
const subscription = option.onSelectionChange
.pipe(map(e => e.isUserInput))
.subscribe(spy);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(spy).toHaveBeenCalledWith(true);
subscription.unsubscribe();
flush();
}));
it('should be able to focus the select trigger', () => {
document.body.focus(); // ensure that focus isn't on the trigger already
fixture.componentInstance.select.focus();
expect(document.activeElement)
.withContext('Expected select element to be focused.')
.toBe(select);
});
it('should set `aria-multiselectable` to true on the listbox inside multi select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
multiFixture.componentInstance.select.open();
multiFixture.detectChanges();
flush();
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-multiselectable')).toBe('true');
}));
it('should set aria-multiselectable false on single-selection instances', fakeAsync(() => {
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-multiselectable')).toBe('false');
}));
it('should set aria-activedescendant only while the panel is open', fakeAsync(() => {
fixture.componentInstance.control.setValue('chips-4');
fixture.detectChanges();
const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
expect(host.hasAttribute('aria-activedescendant'))
.withContext('Expected no aria-activedescendant on init.')
.toBe(false);
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll('mat-option');
expect(host.getAttribute('aria-activedescendant'))
.withContext('Expected aria-activedescendant to match the active option.')
.toBe(options[4].id);
fixture.componentInstance.select.close();
fixture.detectChanges();
flush();
expect(host.hasAttribute('aria-activedescendant'))
.withContext('Expected no aria-activedescendant when closed.')
.toBe(false);
}));
it('should set aria-activedescendant based on the focused option', fakeAsync(() => {
const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll('mat-option');
expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);
[1, 2, 3].forEach(() => {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
fixture.detectChanges();
});
expect(host.getAttribute('aria-activedescendant')).toBe(options[3].id);
dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(host.getAttribute('aria-activedescendant')).toBe(options[2].id);
}));
it('should not change the aria-activedescendant using the horizontal arrow keys', fakeAsync(() => {
const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll('mat-option');
expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);
[1, 2, 3].forEach(() => {
dispatchKeyboardEvent(host, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
});
expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);
}));
it('should restore focus to the trigger after selecting an option in multi-select mode', () => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
instance.select.open();
multiFixture.detectChanges();
// Ensure that the select isn't focused to begin with.
select.blur();
expect(document.activeElement).not.toBe(select, 'Expected trigger not to be focused.');
const option = overlayContainerElement.querySelector('mat-option')! as HTMLElement;
option.click();
multiFixture.detectChanges();
expect(document.activeElement)
.withContext('Expected trigger to be focused.')
.toBe(select);
});
it('should set a role of listbox on the select panel', fakeAsync(() => {
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('role')).toBe('listbox');
}));
it('should point the aria-labelledby of the panel to the field label', fakeAsync(() => {
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const labelId = fixture.nativeElement.querySelector('label').id;
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-labelledby')).toBe(labelId);
}));
it('should add a custom aria-labelledby to the panel', fakeAsync(() => {
fixture.componentInstance.ariaLabelledby = 'myLabelId';
fixture.changeDetectorRef.markForCheck();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const labelId = fixture.nativeElement.querySelector('label').id;
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`);
}));
it('should trim the custom panel aria-labelledby when there is no label', fakeAsync(() => {
fixture.componentInstance.hasLabel = false;
fixture.componentInstance.ariaLabelledby = 'myLabelId';
fixture.changeDetectorRef.markForCheck();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
// Note that we assert that there are no spaces around the value.
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-labelledby')).toBe(`myLabelId`);
}));
it('should clear aria-labelledby from the panel if an aria-label is set', fakeAsync(() => {
fixture.componentInstance.ariaLabel = 'My label';
fixture.changeDetectorRef.markForCheck();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const panel = document.querySelector('.mat-mdc-select-panel')!;
expect(panel.getAttribute('aria-label')).toBe('My label');
expect(panel.hasAttribute('aria-labelledby')).toBe(false);
}));
});
describe('for select inside a modal', () => {
let fixture: ComponentFixture<SelectInsideAModal>;
beforeEach(() => {
fixture = TestBed.createComponent(SelectInsideAModal);
fixture.detectChanges();
});
it('should add the id of the select panel to the aria-owns of the modal', () => {
fixture.componentInstance.select.open();
fixture.detectChanges();
const panelId = `${fixture.componentInstance.select.id}-panel`;
const modalElement = fixture.componentInstance.modal.nativeElement;
expect(modalElement.getAttribute('aria-owns')?.split(' '))
.withContext('expecting modal to own the select panel')
.toContain(panelId);
});
});
describe('for options', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let options: HTMLElement[];
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
});
it('should set the role of mat-option to option', fakeAsync(() => {
expect(options[0].getAttribute('role')).toEqual('option');
expect(options[1].getAttribute('role')).toEqual('option');
expect(options[2].getAttribute('role')).toEqual('option');
}));
it('should set aria-selected on each option for single select', fakeAsync(() => {
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options to have ' + 'aria-selected="false".',
)
.toBe(true);
options[1].click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
expect(options[1].getAttribute('aria-selected'))
.withContext(
'Expected selected single-select option to have ' + 'aria-selected="true".',
)
.toEqual('true');
options.splice(1, 1);
expect(options.every(option => option.getAttribute('aria-selected') === 'false'))
.withContext(
'Expected all unselected single-select options to have ' + 'aria-selected="false".',
)
.toBe(true);
}));
it('should set aria-selected on each option for multi-select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
trigger = multiFixture.debugElement.query(
By.css('.mat-mdc-select-trigger'),
)!.nativeElement;
trigger.click();
multiFixture.detectChanges();
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
expect(
options.every(
option =>
option.hasAttribute('aria-selected') &&
option.getAttribute('aria-selected') === 'false',
),
)
.withContext(
'Expected all unselected multi-select options to have ' + 'aria-selected="false".',
)
.toBe(true);
options[1].click();
multiFixture.detectChanges();
trigger.click();
multiFixture.detectChanges();
flush();
expect(options[1].getAttribute('aria-selected'))
.withContext('Expected selected multi-select option to have aria-selected="true".')
.toEqual('true');
options.splice(1, 1);
expect(
options.every(
option =>
option.hasAttribute('aria-selected') &&
option.getAttribute('aria-selected') === 'false',
),
)
.withContext(
'Expected all unselected multi-select options to have ' + 'aria-selected="false".',
)
.toBe(true);
}));
it('should omit the tabindex attribute on each option', fakeAsync(() => {
expect(options[0].hasAttribute('tabindex')).toBeFalse();
expect(options[1].hasAttribute('tabindex')).toBeFalse();
expect(options[2].hasAttribute('tabindex')).toBeFalse();
}));
it('should set aria-disabled for disabled options', fakeAsync(() => {
expect(options[0].getAttribute('aria-disabled')).toEqual('false');
expect(options[1].getAttribute('aria-disabled')).toEqual('false');
expect(options[2].getAttribute('aria-disabled')).toEqual('true');
fixture.componentInstance.foods[2]['disabled'] = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(options[0].getAttribute('aria-disabled')).toEqual('false');
expect(options[1].getAttribute('aria-disabled')).toEqual('false');
expect(options[2].getAttribute('aria-disabled')).toEqual('false');
}));
it('should remove the active state from options that have been deselected while closed', fakeAsync(() => {
let activeOptions = options.filter(option => {
return option.classList.contains('mat-mdc-option-active');
});
expect(activeOptions)
.withContext('Expected first option to have active styles.')
.toEqual([options[0]]);
options[1].click();
fixture.detectChanges();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
activeOptions = options.filter(option => {
return option.classList.contains('mat-mdc-option-active');
});
expect(activeOptions)
.withContext(
'Expected only selected option to be marked as active after it is ' + 'clicked.',
)
.toEqual([options[1]]);
fixture.componentInstance.control.setValue(fixture.componentInstance.foods[7].value);
fixture.detectChanges();
fixture.componentInstance.select.close();
fixture.detectChanges();
flush();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
activeOptions = options.filter(option => {
return option.classList.contains('mat-mdc-option-active');
});
expect(activeOptions)
.withContext(
'Expected only selected option to be marked as active after the ' +
'value has changed.',
)
.toEqual([options[7]]);
}));
it('should render a checkmark on selected option', fakeAsync(() => {
fixture.componentInstance.control.setValue(fixture.componentInstance.foods[2].value);
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const pseudoCheckboxes = options
.map(option => option.querySelector('.mat-pseudo-checkbox-minimal'))
.filter((x): x is HTMLElement => !!x);
const selectedOption = options[2];
expect(selectedOption.querySelector('.mat-pseudo-checkbox-minimal')).not.toBeNull();
expect(pseudoCheckboxes.length).toBe(1);
}));
it('should render checkboxes for multi-select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
multiFixture.componentInstance.control.setValue([
multiFixture.componentInstance.foods[2].value,
]);
multiFixture.detectChanges();
trigger = multiFixture.debugElement.query(
By.css('.mat-mdc-select-trigger'),
)!.nativeElement;
trigger.click();
multiFixture.detectChanges();
flush();
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
const pseudoCheckboxes = options
.map(option => option.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-full'))
.filter((x): x is HTMLElement => !!x);
const selectedPseudoCheckbox = pseudoCheckboxes[2];
expect(pseudoCheckboxes.length)
.withContext('expecting each option to have a pseudo-checkbox with "full" appearance')
.toEqual(options.length);
expect(selectedPseudoCheckbox.classList)
.withContext('expecting selected pseudo-checkbox to be checked')
.toContain('mat-pseudo-checkbox-checked');
}));
});
describe('for option groups', () => {
let fixture: ComponentFixture<SelectWithGroups>;
let trigger: HTMLElement;
let groups: NodeListOf<HTMLElement>;
beforeEach(() => {
fixture = TestBed.createComponent(SelectWithGroups);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
groups = overlayContainerElement.querySelectorAll(
'mat-optgroup',
) as NodeListOf<HTMLElement>;
});
it('should set the appropriate role', fakeAsync(() => {
expect(groups[0].getAttribute('role')).toBe('group');
}));
it('should set the `aria-labelledby` attribute', fakeAsync(() => {
let group = groups[0];
let label = group.querySelector('.mat-mdc-optgroup-label') as HTMLElement;
expect(label.getAttribute('id'))
.withContext('Expected label to have an id.')
.toBeTruthy();
expect(group.getAttribute('aria-labelledby'))
.withContext('Expected `aria-labelledby` to match the label id.')
.toBe(label.getAttribute('id'));
}));
it('should set the `aria-disabled` attribute if the group is disabled', fakeAsync(() => {
expect(groups[1].getAttribute('aria-disabled')).toBe('true');
}));
});
});
describe('overlay panel', () => {
let fixture: ComponentFixture<BasicSelect>;
let formField: HTMLElement;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
trigger = formField.querySelector('.mat-mdc-select-trigger') as HTMLElement;
});
it('should not throw when attempting to open too early', () => {
// Create component and then immediately open without running change detection
fixture = TestBed.createComponent(BasicSelect);
expect(() => fixture.componentInstance.select.open()).not.toThrow();
});
it('should open the panel when trigger is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(overlayContainerElement.textContent).toContain('Steak');
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(overlayContainerElement.textContent).toContain('Tacos');
}));
it('should close the panel when an item is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should close the panel when a click occurs outside the panel', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
) as HTMLElement;
backdrop.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should set the width of the overlay based on the trigger', fakeAsync(() => {
formField.style.width = '200px';
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toBe('200px');
}));
it('should update the width of the panel on resize', fakeAsync(() => {
formField.style.width = '300px';
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
const initialWidth = parseInt(pane.style.width || '0');
expect(initialWidth).toBeGreaterThan(0);
formField.style.width = '400px';
dispatchFakeEvent(window, 'resize');
fixture.detectChanges();
tick(1000);
fixture.detectChanges();
expect(parseInt(pane.style.width || '0')).toBeGreaterThan(initialWidth);
}));
it('should be able to set a custom width on the select panel', fakeAsync(() => {
fixture.componentInstance.panelWidth = '42px';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toBe('42px');
}));
it('should not set a width on the panel if panelWidth is null', fakeAsync(() => {
fixture.componentInstance.panelWidth = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toBeFalsy();
}));
it('should not set a width on the panel if panelWidth is an empty string', fakeAsync(() => {
fixture.componentInstance.panelWidth = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toBeFalsy();
}));
it('should not attempt to open a select that does not have any options', () => {
fixture.componentInstance.foods = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select.panelOpen).toBe(false);
});
it('should close the panel when tabbing out', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
dispatchKeyboardEvent(trigger, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should restore focus to the host before tabbing away', fakeAsync(() => {
const select = fixture.nativeElement.querySelector('.mat-mdc-select');
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
// Use a spy since focus can be flaky in unit tests.
spyOn(select, 'focus').and.callThrough();
dispatchKeyboardEvent(trigger, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(select.focus).toHaveBeenCalled();
}));
it('should close when tabbing out from inside the panel', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
const panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')!;
dispatchKeyboardEvent(panel, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should focus the first option when pressing HOME', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const event = dispatchKeyboardEvent(trigger, 'keydown', HOME);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
expect(event.defaultPrevented).toBe(true);
}));
it('should focus the last option when pressing END', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const event = dispatchKeyboardEvent(trigger, 'keydown', END);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
expect(event.defaultPrevented).toBe(true);
}));
it('should focus the last option when pressing PAGE_DOWN with less than 10 options', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
expect(event.defaultPrevented).toBe(true);
}));
it('should focus the first option when pressing PAGE_UP with index < 10', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBeLessThan(10);
const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
expect(event.defaultPrevented).toBe(true);
}));
it('should be able to set extra classes on the panel', () => {
trigger.click();
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-mdc-select-panel') as HTMLElement;
expect(panel.classList).toContain('custom-one');
expect(panel.classList).toContain('custom-two');
});
it('should update disableRipple properly on each option', () => {
const options = fixture.componentInstance.options.toArray();
expect(options.every(option => option.disableRipple === false))
.withContext('Expected all options to have disableRipple set to false initially.')
.toBeTruthy();
fixture.componentInstance.disableRipple = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(options.every(option => option.disableRipple === true))
.withContext('Expected all options to have disableRipple set to true.')
.toBeTruthy();
});
it('should not show ripples if they were disabled', fakeAsync(() => {
fixture.componentInstance.disableRipple = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option')!;
dispatchFakeEvent(option, 'mousedown');
dispatchFakeEvent(option, 'mouseup');
expect(option.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
it('should be able to render options inside groups with an ng-container', () => {
fixture.destroy();
const groupFixture = TestBed.createComponent(SelectWithGroupsAndNgContainer);
groupFixture.detectChanges();
trigger = groupFixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
groupFixture.detectChanges();
expect(document.querySelectorAll('.cdk-overlay-container mat-option').length)
.withContext('Expected at least one option to be rendered.')
.toBeGreaterThan(0);
});
it('should not consider itself as blurred if the trigger loses focus while the panel is still open', fakeAsync(() => {
const selectElement = fixture.nativeElement.querySelector('.mat-mdc-select');
const selectInstance = fixture.componentInstance.select;
dispatchFakeEvent(selectElement, 'focus');
fixture.detectChanges();
expect(selectInstance.focused).withContext('Expected select to be focused.').toBe(true);
selectInstance.open();
fixture.detectChanges();
flush();
dispatchFakeEvent(selectElement, 'blur');
fixture.detectChanges();
expect(selectInstance.focused)
.withContext('Expected select element to remain focused.')
.toBe(true);
}));
});
describe('selection logic', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let formField: HTMLElement;
let label: HTMLLabelElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
label = formField.querySelector('label')!;
});
it('should not float label if no option is selected', () => {
expect(label.classList.contains('mat-form-field-should-float'))
.withContext('Label should not be floating')
.toBe(false);
});
it('should focus the first option if no option is selected', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(0);
}));
it('should select an option when it is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
let option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
trigger.click();
fixture.detectChanges();
flush();
option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
expect(option.classList).toContain('mdc-list-item--selected');
expect(fixture.componentInstance.options.first.selected).toBe(true);
expect(fixture.componentInstance.select.selected).toBe(
fixture.componentInstance.options.first,
);
}));
it('should be able to select an option using the MatOption API', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const optionInstances = fixture.componentInstance.options.toArray();
const optionNodes: NodeListOf<HTMLElement> =
overlayContainerElement.querySelectorAll('mat-option');
optionInstances[1].select();
fixture.detectChanges();
expect(optionNodes[1].classList).toContain('mdc-list-item--selected');
expect(optionInstances[1].selected).toBe(true);
expect(fixture.componentInstance.select.selected).toBe(optionInstances[1]);
}));
it('should deselect other options when one is selected', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
let options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
flush();
trigger.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList).not.toContain('mdc-list-item--selected');
expect(options[2].classList).not.toContain('mdc-list-item--selected');
const optionInstances = fixture.componentInstance.options.toArray();
expect(optionInstances[1].selected).toBe(false);
expect(optionInstances[2].selected).toBe(false);
}));
it('should deselect other options when one is programmatically selected', fakeAsync(() => {
let control = fixture.componentInstance.control;
let foods = fixture.componentInstance.foods;
trigger.click();
fixture.detectChanges();
flush();
let options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
flush();
control.setValue(foods[1].value);
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[0].classList).not.toContain(
'mdc-list-item--selected',
'Expected first option to no longer be selected',
);
expect(options[1].classList)
.withContext('Expected second option to be selected')
.toContain('mdc-list-item--selected');
const optionInstances = fixture.componentInstance.options.toArray();
expect(optionInstances[0].selected)
.withContext('Expected first option to no longer be selected')
.toBe(false);
expect(optionInstances[1].selected)
.withContext('Expected second option to be selected')
.toBe(true);
}));
it('should remove selection if option has been removed', fakeAsync(() => {
let select = fixture.componentInstance.select;
trigger.click();
fixture.detectChanges();
flush();
let firstOption = overlayContainerElement.querySelectorAll('mat-option')[0] as HTMLElement;
firstOption.click();
fixture.detectChanges();
expect(select.selected)
.withContext('Expected first option to be selected.')
.toBe(select.options.first);
fixture.componentInstance.foods = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
expect(select.selected)
.withContext('Expected selection to be removed when option no longer exists.')
.toBeUndefined();
}));
it('should display the selected option in the trigger', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!.nativeElement;
expect(label.classList.contains('mdc-floating-label--float-above'))
.withContext('Label should be floating')
.toBe(true);
expect(value.textContent).toContain('Steak');
}));
it('should focus the selected option if an option is selected', fakeAsync(() => {
// must wait for initial writeValue promise to finish
flush();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
// must wait for animation to finish
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(1);
}));
it('should select an option that was added after initialization', fakeAsync(() => {
fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[8].click();
fixture.detectChanges();
flush();
expect(trigger.textContent).toContain('Potatoes');
expect(fixture.componentInstance.select.selected).toBe(
fixture.componentInstance.options.last,
);
}));
it('should update the trigger when the selected option label is changed', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
expect(trigger.textContent!.trim()).toBe('Pizza');
fixture.componentInstance.foods[1].viewValue = 'Calzone';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(trigger.textContent!.trim()).toBe('Calzone');
}));
it('should update the trigger value if the text as a result of an expression change', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
expect(trigger.textContent!.trim()).toBe('Pizza');
fixture.componentInstance.capitalize = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.checkNoChanges();
expect(trigger.textContent!.trim()).toBe('PIZZA');
}));
it('should not select disabled options', () => {
trigger.click();
fixture.detectChanges();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[2].click();
fixture.detectChanges();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(options[2].classList).not.toContain('mdc-list-item--selected');
expect(fixture.componentInstance.select.selected).toBeUndefined();
});
it('should not select options inside a disabled group', () => {
fixture.destroy();
const groupFixture = TestBed.createComponent(SelectWithGroups);
groupFixture.detectChanges();
groupFixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
groupFixture.detectChanges();
const disabledGroup = overlayContainerElement.querySelectorAll('mat-optgroup')[1];
const options = disabledGroup.querySelectorAll('mat-option');
(options[0] as HTMLElement).click();
groupFixture.detectChanges();
expect(groupFixture.componentInstance.select.panelOpen).toBe(true);
expect(options[0].classList).not.toContain('mdc-list-item--selected');
expect(groupFixture.componentInstance.select.selected).toBeUndefined();
});
it('should not throw if triggerValue accessed with no selected value', fakeAsync(() => {
expect(() => fixture.componentInstance.select.triggerValue).not.toThrow();
}));
it('should emit to `optionSelectionChanges` when an option is selected', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const spy = jasmine.createSpy('option selection spy');
const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
subscription.unsubscribe();
}));
it('should handle accessing `optionSelectionChanges` before the options are initialized', fakeAsync(() => {
fixture.destroy();
fixture = TestBed.createComponent(BasicSelect);
let spy = jasmine.createSpy('option selection spy');
let subscription: Subscription;
expect(fixture.componentInstance.select.options).toBeFalsy();
expect(() => {
subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
}).not.toThrow();
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
subscription!.unsubscribe();
}));
it('should emit to `optionSelectionChanges` after the list of options has changed', fakeAsync(() => {
let spy = jasmine.createSpy('option selection spy');
let subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
let selectFirstOption = () => {
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
};
fixture.componentInstance.foods = [{value: 'salad-8', viewValue: 'Salad'}];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
selectFirstOption();
expect(spy).toHaveBeenCalledTimes(1);
fixture.componentInstance.foods = [{value: 'fruit-9', viewValue: 'Fruit'}];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
selectFirstOption();
expect(spy).toHaveBeenCalledTimes(2);
subscription!.unsubscribe();
}));
it('should not indicate programmatic value changes as user interactions', () => {
const events: MatOptionSelectionChange[] = [];
const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(
(event: MatOptionSelectionChange) => events.push(event),
);
fixture.componentInstance.control.setValue('eggs-5');
fixture.detectChanges();
expect(events.map(event => event.isUserInput)).toEqual([false]);
subscription.unsubscribe();
});
});
describe('forms integration', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
});
it('should take an initial view value with reactive forms', fakeAsync(() => {
fixture.componentInstance.control = new FormControl('pizza-1');
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent)
.withContext(`Expected trigger to be populated by the control's initial value.`)
.toContain('Pizza');
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
expect(options[1].classList)
.withContext(`Expected option with the control's initial value to be selected.`)
.toContain('mdc-list-item--selected');
}));
it('should set the view value from the form', fakeAsync(() => {
let value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent.trim()).toBe('Food');
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent)
.withContext(`Expected trigger to be populated by the control's new value.`)
.toContain('Pizza');
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
expect(options[1].classList)
.withContext(`Expected option with the control's new value to be selected.`)
.toContain('mdc-list-item--selected');
}));
it('should update the form value when the view changes', fakeAsync(() => {
expect(fixture.componentInstance.control.value)
.withContext(`Expected the control's value to be empty initially.`)
.toEqual(null);
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value)
.withContext(`Expected control's value to be set to the new option.`)
.toEqual('steak-0');
}));
it('should clear the selection when a nonexistent option value is selected', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
fixture.componentInstance.control.setValue('gibberish');
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent.trim())
.withContext(`Expected trigger to show the placeholder.`)
.toBe('Food');
expect(trigger.textContent).not.toContain(
'Pizza',
`Expected trigger is cleared when option value is not found.`,
);
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
expect(options[1].classList).not.toContain(
'mdc-list-item--selected',
`Expected option w/ the old value not to be selected.`,
);
}));
it('should clear the selection when the control is reset', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
fixture.componentInstance.control.reset();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent.trim())
.withContext(`Expected trigger to show the placeholder.`)
.toBe('Food');
expect(trigger.textContent).not.toContain(
'Pizza',
`Expected trigger is cleared when option value is not found.`,
);
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
expect(options[1].classList).not.toContain(
'mdc-list-item--selected',
`Expected option w/ the old value not to be selected.`,
);
}));
it('should set the control to touched when the select is blurred', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.withContext(`Expected the control to start off as untouched.`)
.toEqual(false);
trigger.click();
dispatchFakeEvent(trigger, 'blur');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.withContext(`Expected the control to stay untouched when menu opened.`)
.toEqual(false);
const backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
) as HTMLElement;
backdrop.click();
dispatchFakeEvent(trigger, 'blur');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.withContext(`Expected the control to be touched as soon as focus left the select.`)
.toEqual(true);
}));
it('should set the control to touched when the panel is closed', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to start off as untouched.')
.toBe(false);
trigger.click();
dispatchFakeEvent(trigger, 'blur');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to stay untouched when menu opened.')
.toBe(false);
fixture.componentInstance.select.close();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to be touched when the panel was closed.')
.toBe(true);
}));
it('should not set touched when a disabled select is touched', () => {
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to start off as untouched.')
.toBe(false);
fixture.componentInstance.control.disable();
dispatchFakeEvent(trigger, 'blur');
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to stay untouched.')
.toBe(false);
});
it('should set the control to dirty when the select value changes in DOM', fakeAsync(() => {
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to start out pristine.`)
.toEqual(false);
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to be dirty after value was changed by user.`)
.toEqual(true);
}));
it('should not set the control to dirty when the value changes programmatically', () => {
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to start out pristine.`)
.toEqual(false);
fixture.componentInstance.control.setValue('pizza-1');
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to stay pristine after programmatic change.`)
.toEqual(false);
});
it('should set an asterisk after the label if control is required', fakeAsync(() => {
const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
expect(label.querySelector('.mat-mdc-form-field-required-marker'))
.withContext(`Expected label not to have an asterisk, as control was not required.`)
.toBeFalsy();
fixture.componentInstance.isRequired = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(label.querySelector('.mat-mdc-form-field-required-marker'))
.withContext(`Expected label to have an asterisk, as control was required.`)
.toBeTruthy();
}));
it('should propagate the value set through the `value` property to the form field', fakeAsync(() => {
const control = fixture.componentInstance.control;
expect(control.value).toBeFalsy();
fixture.componentInstance.select.value = 'pizza-1';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(control.value).toBe('pizza-1');
}));
});
describe('disabled behavior', () => {
it('should disable itself when control is disabled programmatically', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
fixture.componentInstance.control.disable();
fixture.detectChanges();
let trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.withContext(`Expected cursor to be default arrow on disabled control.`)
.toEqual('default');
trigger.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent)
.withContext(`Expected select panel to stay closed.`)
.toEqual('');
expect(fixture.componentInstance.select.panelOpen)
.withContext(`Expected select panelOpen property to stay false.`)
.toBe(false);
fixture.componentInstance.control.enable();
fixture.detectChanges();
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.withContext(`Expected cursor to be a pointer on enabled control.`)
.toEqual('pointer');
trigger.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent)
.withContext(`Expected select panel to open normally on re-enabled control`)
.toContain('Steak');
expect(fixture.componentInstance.select.panelOpen)
.withContext(`Expected select panelOpen property to become true.`)
.toBe(true);
}));
it('should keep the disabled state in sync if the form group is swapped and disabled at the same time', () => {
const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup);
fixture.detectChanges();
const instance = fixture.componentInstance;
expect(instance.select.disabled).toBe(false);
instance.assignGroup(true);
fixture.detectChanges();
expect(instance.select.disabled).toBe(true);
});
});
describe('keyboard scrolling', () => {
let fixture: ComponentFixture<BasicSelect>;
let host: HTMLElement;
let panel: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.componentInstance.foods = [];
for (let i = 0; i < 30; i++) {
fixture.componentInstance.foods.push({value: `value-${i}`, viewValue: `Option ${i}`});
}
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
fixture.detectChanges();
host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;
}));
it('should not scroll to options that are completely in the view', () => {
const initialScrollPosition = panel.scrollTop;
[1, 2, 3].forEach(() => {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
});
expect(panel.scrollTop)
.withContext('Expected scroll position not to change')
.toBe(initialScrollPosition);
});
it('should scroll down to the active option', () => {
for (let i = 0; i < 15; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
}
// <top padding> + <option index * height> - <panel height> = 8 + 16 * 48 - 275 = 501
expect(panel.scrollTop).withContext('Expected scroll to be at the 16th option.').toBe(501);
});
it('should scroll up to the active option', () => {
// Scroll to the bottom.
for (let i = 0; i < fixture.componentInstance.foods.length; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
}
for (let i = 0; i < 20; i++) {
dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
}
// <top padding> + <option index * height> = 8 + 9 * 48 = 440
expect(panel.scrollTop).withContext('Expected scroll to be at the 9th option.').toBe(440);
});
it('should skip option group labels', fakeAsync(() => {
fixture.destroy();
const groupFixture = TestBed.createComponent(SelectWithGroups);
groupFixture.detectChanges();
groupFixture.componentInstance.select.open();
groupFixture.detectChanges();
flush();
host = groupFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;
for (let i = 0; i < 8; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
flush();
}
// <top padding> + <(option index + group labels) * height> - <panel height> =
// 8 + (8 + 3) * 48 - 275 = 309
expect(panel.scrollTop).withContext('Expected scroll to be at the 9th option.').toBe(309);
}));
it('should scroll to the top when pressing HOME', () => {
for (let i = 0; i < 20; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
fixture.detectChanges();
}
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled down.')
.toBeGreaterThan(0);
dispatchKeyboardEvent(host, 'keydown', HOME);
fixture.detectChanges();
// 8px is the top padding of the panel.
expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(8);
});
it('should scroll to the bottom of the panel when pressing END', () => {
dispatchKeyboardEvent(host, 'keydown', END);
fixture.detectChanges();
// <top padding> + <option amount> * <option height> - <panel height> =
// 8 + 30 * 48 - 275 = 1173
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled to the bottom')
.toBe(1173);
});
it('should scroll 10 to the top or to first element when pressing PAGE_UP', () => {
for (let i = 0; i < 18; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
fixture.detectChanges();
}
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled down.')
.toBeGreaterThan(0);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(18);
dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
fixture.detectChanges();
// <top padding> + <option amount> * <option height>
// 8 + 8 × 48
expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(392);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(8);
dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
fixture.detectChanges();
// 8px is the top padding of the panel.
expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(8);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
});
it('should scroll 10 to the bottom of the panel when pressing PAGE_DOWN', () => {
dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
fixture.detectChanges();
// <top padding> + <option amount> * <option height> - <panel height> =
// 8 + 11 * 48 - 275 = 261
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled 10 to the bottom')
.toBe(261);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(10);
dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
fixture.detectChanges();
// <top padding> + <option amount> * <option height> - <panel height> =
// 8 + 21 * 48 - 275 = 741
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled 10 to the bottom')
.toBe(741);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(20);
dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
fixture.detectChanges();
// <top padding> + <option amount> * <option height> - <panel height> =
// 8 + 30 * 48 - 275 = 1173
expect(panel.scrollTop)
.withContext('Expected panel to be scrolled 10 to the bottom')
.toBe(1173);
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(29);
});
it('should scroll to the active option when typing', fakeAsync(() => {
for (let i = 0; i < 15; i++) {
// Press the letter 'o' 15 times since all the options are named 'Option <index>'
dispatchEvent(host, createKeyboardEvent('keydown', 79, 'o'));
fixture.detectChanges();
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
}
flush();
// <top padding> + <option index * height> - <panel height> = 8 + 16 * 48 - 275 = 501
expect(panel.scrollTop).withContext('Expected scroll to be at the 16th option.').toBe(501);
}));
it('should scroll to top when going to first option in top group', fakeAsync(() => {
fixture.destroy();
const groupFixture = TestBed.createComponent(SelectWithGroups);
groupFixture.detectChanges();
groupFixture.componentInstance.select.open();
groupFixture.detectChanges();
flush();
host = groupFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;
for (let i = 0; i < 5; i++) {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
}
expect(panel.scrollTop).toBeGreaterThan(0);
for (let i = 0; i < 5; i++) {
dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
flush();
}
expect(panel.scrollTop).toBe(0);
}));
});
});
describe('when initialized without options', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectInitWithoutOptions])));
it('should select the proper option when option list is initialized later', fakeAsync(() => {
const fixture = TestBed.createComponent(SelectInitWithoutOptions);
const instance = fixture.componentInstance;
fixture.detectChanges();
flush();
// Wait for the initial writeValue promise.
expect(instance.select.selected).toBeFalsy();
instance.addOptions();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
// Wait for the next writeValue promise.
expect(instance.select.selected).toBe(instance.options.toArray()[1]);
}));
});
describe('with a selectionChange event handler', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithChangeEvent])));
let fixture: ComponentFixture<SelectWithChangeEvent>;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SelectWithChangeEvent);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
});
it('should emit an event when the selected option has changed', () => {
trigger.click();
fixture.detectChanges();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
expect(fixture.componentInstance.changeListener).toHaveBeenCalled();
});
it('should not emit multiple change events for the same option', () => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
option.click();
expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);
});
it('should only emit one event when pressing arrow keys on closed select', fakeAsync(() => {
const select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);
flush();
}));
});
describe('with ngModel', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgModelSelect])));
it('should disable itself when control is disabled using the property', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelect);
fixture.detectChanges();
fixture.componentInstance.isDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.withContext(`Expected cursor to be default arrow on disabled control.`)
.toEqual('default');
trigger.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent)
.withContext(`Expected select panel to stay closed.`)
.toEqual('');
expect(fixture.componentInstance.select.panelOpen)
.withContext(`Expected select panelOpen property to stay false.`)
.toBe(false);
fixture.componentInstance.isDisabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
fixture.detectChanges();
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.withContext(`Expected cursor to be a pointer on enabled control.`)
.toEqual('pointer');
trigger.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent)
.withContext(`Expected select panel to open normally on re-enabled control`)
.toContain('Steak');
expect(fixture.componentInstance.select.panelOpen)
.withContext(`Expected select panelOpen property to become true.`)
.toBe(true);
}));
});
describe('with ngIf', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgIfSelect])));
it('should handle nesting in an ngIf', fakeAsync(() => {
const fixture = TestBed.createComponent(NgIfSelect);
fixture.detectChanges();
fixture.componentInstance.isShowing = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
const trigger = formField.querySelector('.mat-mdc-select-trigger');
formField.style.width = '300px';
trigger.click();
flush();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!;
expect(value.nativeElement.textContent)
.withContext(`Expected trigger to be populated by the control's initial value.`)
.toContain('Pizza');
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toEqual('300px');
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(overlayContainerElement.textContent).toContain('Steak');
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(overlayContainerElement.textContent).toContain('Tacos');
}));
});
describe('with multiple mat-select elements in one view', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([ManySelects])));
let fixture: ComponentFixture<ManySelects>;
let triggers: DebugElement[];
let options: NodeListOf<HTMLElement>;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(ManySelects);
fixture.detectChanges();
triggers = fixture.debugElement.queryAll(By.css('.mat-mdc-select-trigger'));
triggers[0].nativeElement.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
}));
it('should set the option id', fakeAsync(() => {
let firstOptionID = options[0].id;
expect(options[0].id)
.withContext(`Expected option ID to have the correct prefix.`)
.toContain('mat-option');
expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);
const backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
) as HTMLElement;
backdrop.click();
fixture.detectChanges();
flush();
triggers[1].nativeElement.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[0].id)
.withContext(`Expected option ID to have the correct prefix.`)
.toContain('mat-option');
expect(options[0].id).not.toEqual(firstOptionID, `Expected option IDs to be unique.`);
expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);
}));
});
describe('with floatLabel', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([FloatLabelSelect])));
it('should be able to always float the label', () => {
const fixture = TestBed.createComponent(FloatLabelSelect);
fixture.detectChanges();
const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
expect(fixture.componentInstance.control.value).toBeFalsy();
fixture.componentInstance.floatLabel = 'always';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(label.classList.contains('mdc-floating-label--float-above'))
.withContext('Label should be floating')
.toBe(true);
});
it('should default to global floating label type', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
FormsModule,
NoopAnimationsModule,
],
providers: [{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {floatLabel: 'always'}}],
declarations: [FloatLabelSelect],
});
const fixture = TestBed.createComponent(FloatLabelSelect);
fixture.componentInstance.floatLabel = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
expect(label.classList.contains('mdc-floating-label--float-above'))
.withContext('Label should be floating')
.toBe(true);
});
it('should float the label on focus if it has a placeholder', () => {
const fixture = TestBed.createComponent(FloatLabelSelect);
fixture.detectChanges();
expect(fixture.componentInstance.placeholder).toBeTruthy();
fixture.componentInstance.floatLabel = 'auto';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchFakeEvent(fixture.nativeElement.querySelector('.mat-mdc-select'), 'focus');
fixture.detectChanges();
const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
expect(label.classList.contains('mdc-floating-label--float-above'))
.withContext('Label should be floating')
.toBe(true);
});
});
describe('with a sibling component that throws an error', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([SelectWithErrorSibling, ThrowsErrorOnInit])));
it('should not crash the browser when a sibling throws an error on init', () => {
// Note that this test can be considered successful if the error being thrown didn't
// end up crashing the testing setup altogether.
expect(() => {
TestBed.createComponent(SelectWithErrorSibling).detectChanges();
}).toThrowError(new RegExp('Oh no!', 'g'));
});
});
describe('with tabindex', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithPlainTabindex])));
it('should be able to set the tabindex via the native attribute', () => {
const fixture = TestBed.createComponent(SelectWithPlainTabindex);
fixture.detectChanges();
const select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
expect(select.getAttribute('tabindex')).toBe('5');
});
});
describe('change events', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithPlainTabindex])));
it('should complete the stateChanges stream on destroy', () => {
const fixture = TestBed.createComponent(SelectWithPlainTabindex);
fixture.detectChanges();
const debugElement = fixture.debugElement.query(By.directive(MatSelect))!;
const select = debugElement.componentInstance;
const spy = jasmine.createSpy('stateChanges complete');
const subscription = select.stateChanges.subscribe(undefined, undefined, spy);
fixture.destroy();
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
});
describe('when initially hidden', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectInitiallyHidden])));
it('should set the width of the overlay if the element was hidden initially', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectInitiallyHidden);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
const trigger = formField.querySelector('.mat-mdc-select-trigger');
formField.style.width = '300px';
fixture.componentInstance.isVisible = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toBe('300px');
}));
});
describe('with no placeholder', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectNoPlaceholder])));
it('should set the width of the overlay if there is no placeholder', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectNoPlaceholder);
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(parseInt(pane.style.width as string)).toBeGreaterThan(0);
}));
});
describe('with theming', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectWithTheming])));
let fixture: ComponentFixture<BasicSelectWithTheming>;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelectWithTheming);
fixture.detectChanges();
});
it('should transfer the theme to the select panel', () => {
fixture.componentInstance.theme = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.componentInstance.select.open();
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;
expect(panel.classList).toContain('mat-warn');
});
});
describe('when invalid inside a form', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([InvalidSelectInForm])));
it('should not throw SelectionModel errors in addition to ngModel errors', () => {
const fixture = TestBed.createComponent(InvalidSelectInForm);
// The first change detection run will throw the "ngModel is missing a name" error.
expect(() => fixture.detectChanges()).toThrowError(/the name attribute must be set/g);
fixture.changeDetectorRef.markForCheck();
// The second run shouldn't throw selection-model related errors.
expect(() => fixture.detectChanges()).not.toThrow();
});
});
describe('with ngModel using compareWith', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgModelCompareWithSelect])));
let fixture: ComponentFixture<NgModelCompareWithSelect>;
let instance: NgModelCompareWithSelect;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(NgModelCompareWithSelect);
instance = fixture.componentInstance;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
}));
describe('comparing by value', () => {
it('should have a selection', fakeAsync(() => {
const selectedOption = instance.select.selected as MatOption;
expect(selectedOption.value.value).toEqual('pizza-1');
}));
it('should update when making a new selection', fakeAsync(() => {
instance.options.last._selectViaInteraction();
fixture.detectChanges();
flush();
const selectedOption = instance.select.selected as MatOption;
expect(instance.selectedFood.value).toEqual('tacos-2');
expect(selectedOption.value.value).toEqual('tacos-2');
}));
});
describe('comparing by reference', () => {
beforeEach(fakeAsync(() => {
spyOn(instance, 'compareByReference').and.callThrough();
instance.useCompareByReference();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
}));
it('should use the comparator', () => {
expect(instance.compareByReference).toHaveBeenCalled();
});
it('should initialize with no selection despite having a value', () => {
expect(instance.selectedFood.value).toBe('pizza-1');
expect(instance.select.selected).toBeUndefined();
});
it('should not update the selection if value is copied on change', fakeAsync(() => {
instance.options.first._selectViaInteraction();
fixture.detectChanges();
flush();
expect(instance.selectedFood.value).toEqual('steak-0');
expect(instance.select.selected).toBeUndefined();
}));
it('should throw an error when using a non-function comparator', () => {
instance.useNullComparator();
expect(() => {
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
}).toThrowError(wrappedErrorMessage(getMatSelectNonFunctionValueError()));
});
});
});
describe(`when the select's value is accessed on initialization`, () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectEarlyAccessSibling])));
it('should not throw when trying to access the selected value on init in the view', () => {
expect(() => {
TestBed.createComponent(SelectEarlyAccessSibling).detectChanges();
}).not.toThrow();
});
it('should not throw when reading selected value programmatically in single selection mode', () => {
expect(() => {
const fixture = TestBed.createComponent(SelectEarlyAccessSibling);
const select = fixture.debugElement.query(By.directive(MatSelect)).componentInstance;
// We're checking that accessing the getter won't throw.
select.multiple = false;
return select.selected;
}).not.toThrow();
});
it('should not throw when reading selected value programmatically in multi selection mode', () => {
expect(() => {
const fixture = TestBed.createComponent(SelectEarlyAccessSibling);
const select = fixture.debugElement.query(By.directive(MatSelect)).componentInstance;
// We're checking that accessing the getter won't throw.
select.multiple = true;
return select.selected;
}).not.toThrow();
});
});
describe('with ngIf and mat-label', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithNgIfAndLabel])));
it('should not throw when using ngIf on a select with an associated label', () => {
expect(() => {
const fixture = TestBed.createComponent(SelectWithNgIfAndLabel);
fixture.detectChanges();
}).not.toThrow();
});
});
describe('inside of a form group', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectInsideFormGroup])));
let fixture: ComponentFixture<SelectInsideFormGroup>;
let testComponent: SelectInsideFormGroup;
let select: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SelectInsideFormGroup);
fixture.detectChanges();
testComponent = fixture.componentInstance;
select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
});
it('should not set the invalid class on a clean select', fakeAsync(() => {
expect(testComponent.formGroup.untouched)
.withContext('Expected the form to be untouched.')
.toBe(true);
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid.')
.toBe(true);
expect(select.classList).not.toContain(
'mat-mdc-select-invalid',
'Expected select not to appear invalid.',
);
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to false.')
.toBe('false');
}));
it('should appear as invalid if it becomes touched', fakeAsync(() => {
expect(select.classList).not.toContain(
'mat-mdc-select-invalid',
'Expected select not to appear invalid.',
);
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to false.')
.toBe('false');
testComponent.formControl.markAsTouched();
fixture.detectChanges();
expect(select.classList)
.withContext('Expected select to appear invalid.')
.toContain('mat-mdc-select-invalid');
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to true.')
.toBe('true');
}));
it('should not have the invalid class when the select becomes valid', fakeAsync(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();
expect(select.classList)
.withContext('Expected select to appear invalid.')
.toContain('mat-mdc-select-invalid');
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to true.')
.toBe('true');
testComponent.formControl.setValue('pizza-1');
fixture.detectChanges();
expect(select.classList).not.toContain(
'mat-mdc-select-invalid',
'Expected select not to appear invalid.',
);
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to false.')
.toBe('false');
}));
it('should appear as invalid when the parent form group is submitted', fakeAsync(() => {
expect(select.classList).not.toContain(
'mat-mdc-select-invalid',
'Expected select not to appear invalid.',
);
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to false.')
.toBe('false');
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
fixture.detectChanges();
expect(select.classList)
.withContext('Expected select to appear invalid.')
.toContain('mat-mdc-select-invalid');
expect(select.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to true.')
.toBe('true');
}));
it('should render the error messages when the parent form is submitted', fakeAsync(() => {
const debugEl = fixture.debugElement.nativeElement;
expect(debugEl.querySelectorAll('mat-error').length)
.withContext('Expected no error messages')
.toBe(0);
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
fixture.detectChanges();
expect(debugEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message')
.toBe(1);
}));
it('should override error matching behavior via injection token', () => {
const errorStateMatcher: ErrorStateMatcher = {
isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true),
};
fixture.destroy();
TestBed.resetTestingModule().configureTestingModule({
imports: [MatSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule],
declarations: [SelectInsideFormGroup],
providers: [{provide: ErrorStateMatcher, useValue: errorStateMatcher}],
});
const errorFixture = TestBed.createComponent(SelectInsideFormGroup);
const component = errorFixture.componentInstance;
errorFixture.detectChanges();
expect(component.select.errorState).toBe(true);
expect(errorStateMatcher.isErrorState).toHaveBeenCalled();
});
it('should notify that the state changed when the options have changed', fakeAsync(() => {
testComponent.formControl.setValue('pizza-1');
fixture.detectChanges();
const spy = jasmine.createSpy('stateChanges spy');
const subscription = testComponent.select.stateChanges.subscribe(spy);
testComponent.options = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
}));
it('should set an asterisk after the label if the FormControl is required', fakeAsync(() => {
const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
expect(label.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
}));
});
describe('with custom error behavior', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([CustomErrorBehaviorSelect])));
it('should be able to override the error matching behavior via an @Input', () => {
const fixture = TestBed.createComponent(CustomErrorBehaviorSelect);
const component = fixture.componentInstance;
const matcher = jasmine.createSpy('error state matcher').and.returnValue(true);
fixture.detectChanges();
expect(component.control.invalid).toBe(false);
expect(component.select.errorState).toBe(false);
fixture.componentInstance.errorStateMatcher = {isErrorState: matcher};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(component.select.errorState).toBe(true);
expect(matcher).toHaveBeenCalled();
});
});
describe('with preselected array values', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([SingleSelectWithPreselectedArrayValues])));
it('should be able to preselect an array value in single-selection mode', fakeAsync(() => {
const fixture = TestBed.createComponent(SingleSelectWithPreselectedArrayValues);
fixture.detectChanges();
flush();
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
expect(trigger.textContent).toContain('Pizza');
expect(fixture.componentInstance.options.toArray()[1].selected).toBe(true);
}));
});
describe('with custom value accessor', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([CompWithCustomSelect, CustomSelectAccessor])));
it('should support use inside a custom value accessor', () => {
const fixture = TestBed.createComponent(CompWithCustomSelect);
spyOn(fixture.componentInstance.customAccessor, 'writeValue');
fixture.detectChanges();
expect(fixture.componentInstance.customAccessor.select.ngControl)
.withContext('Expected mat-select NOT to inherit control from parent value accessor.')
.toBeFalsy();
expect(fixture.componentInstance.customAccessor.writeValue).toHaveBeenCalled();
});
});
describe('with a falsy value', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([FalsyValueSelect])));
it('should be able to programmatically select a falsy option', fakeAsync(() => {
const fixture = TestBed.createComponent(FalsyValueSelect);
fixture.detectChanges();
fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
fixture.componentInstance.control.setValue(0);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.options.first.selected)
.withContext('Expected first option to be selected')
.toBe(true);
expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
.withContext('Expected first option to be selected')
.toContain('mdc-list-item--selected');
}));
});
describe('with OnPush', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([BasicSelectOnPush, BasicSelectOnPushPreselected])));
it('should set the trigger text based on the value when initialized', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectOnPushPreselected);
fixture.detectChanges();
flush();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
fixture.detectChanges();
expect(trigger.textContent).toContain('Pizza');
}));
it('should update the trigger based on the value', () => {
const fixture = TestBed.createComponent(BasicSelectOnPush);
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
expect(trigger.textContent).toContain('Pizza');
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(trigger.textContent).not.toContain('Pizza');
});
it('should sync up the form control value with the component value', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectOnPushPreselected);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBe('pizza-1');
expect(fixture.componentInstance.select.value).toBe('pizza-1');
}));
});
describe('with custom trigger', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithCustomTrigger])));
it('should allow the user to customize the label', () => {
const fixture = TestBed.createComponent(SelectWithCustomTrigger);
fixture.detectChanges();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
const label = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!.nativeElement;
expect(label.textContent)
.withContext('Expected the displayed text to be "Pizza" in reverse.')
.toContain('azziP');
});
});
describe('when reseting the value by setting null or undefined', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([ResetValuesSelect])));
let fixture: ComponentFixture<ResetValuesSelect>;
let trigger: HTMLElement;
let formField: HTMLElement;
let options: NodeListOf<HTMLElement>;
let label: HTMLLabelElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(ResetValuesSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
label = formField.querySelector('label')!;
trigger.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
flush();
}));
it('should reset when an option with an undefined value is selected', fakeAsync(() => {
options[4].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(label.classList).not.toContain('mdc-floating-label--float-above');
expect(trigger.textContent).not.toContain('Undefined');
}));
it('should reset when an option with a null value is selected', fakeAsync(() => {
options[5].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(label.classList).not.toContain('mdc-floating-label--float-above');
expect(trigger.textContent).not.toContain('Null');
}));
it('should reset when a blank option is selected', fakeAsync(() => {
options[6].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(label.classList).not.toContain('mdc-floating-label--float-above');
expect(trigger.textContent).not.toContain('None');
}));
it('should not mark the reset option as selected ', fakeAsync(() => {
options[5].click();
fixture.detectChanges();
flush();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
expect(options[5].classList).not.toContain('mdc-list-item--selected');
}));
it('should not reset when any other falsy option is selected', fakeAsync(() => {
options[3].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBe(false);
expect(fixture.componentInstance.select.selected).toBeTruthy();
expect(label.classList).toContain('mdc-floating-label--float-above');
expect(trigger.textContent).toContain('Falsy');
}));
it('should not consider the reset values as selected when resetting the form control', () => {
expect(label.classList).toContain('mdc-floating-label--float-above');
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(label.classList).not.toContain('mdc-floating-label--float-above');
expect(trigger.textContent).not.toContain('Null');
expect(trigger.textContent).not.toContain('Undefined');
});
});
describe('with reset option and a form control', () => {
let fixture: ComponentFixture<SelectWithResetOptionAndFormControl>;
let options: HTMLElement[];
let trigger: HTMLElement;
beforeEach(() => {
configureMatSelectTestingModule([SelectWithResetOptionAndFormControl]);
fixture = TestBed.createComponent(SelectWithResetOptionAndFormControl);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
});
it('should set the select value', fakeAsync(() => {
fixture.componentInstance.control.setValue('a');
fixture.detectChanges();
expect(fixture.componentInstance.select.value).toBe('a');
}));
it('should reset the control value', fakeAsync(() => {
fixture.componentInstance.control.setValue('a');
fixture.detectChanges();
options[0].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value).toBeUndefined();
}));
it('should reflect the value in the form control', fakeAsync(() => {
options[1].click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.value).toBe('a');
expect(fixture.componentInstance.control.value).toBe('a');
}));
it('should deselect the reset option when a value is assigned through the form control', fakeAsync(() => {
expect(options[0].classList).toContain('mat-mdc-option-active');
options[0].click();
fixture.detectChanges();
flush();
fixture.componentInstance.control.setValue('c');
fixture.detectChanges();
trigger.click();
flush();
fixture.detectChanges();
expect(options[0].classList).not.toContain('mat-mdc-option-active');
expect(options[3].classList).toContain('mat-mdc-option-active');
}));
});
describe('without Angular forms', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([
BasicSelectWithoutForms,
BasicSelectWithoutFormsPreselected,
BasicSelectWithoutFormsMultiple,
])));
it('should set the value when options are clicked', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
expect(fixture.componentInstance.selectedFood).toBeFalsy();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFood).toBe('steak-0');
expect(fixture.componentInstance.select.value).toBe('steak-0');
expect(trigger.textContent).toContain('Steak');
trigger.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelectorAll('mat-option')[2] as HTMLElement).click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFood).toBe('sandwich-2');
expect(fixture.componentInstance.select.value).toBe('sandwich-2');
expect(trigger.textContent).toContain('Sandwich');
}));
it('should mark options as selected when the value is set', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
fixture.componentInstance.selectedFood = 'sandwich-2';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
expect(trigger.textContent).toContain('Sandwich');
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelectorAll('mat-option')[2];
expect(option.classList).toContain('mdc-list-item--selected');
expect(fixture.componentInstance.select.value).toBe('sandwich-2');
}));
it('should reset the label when a null value is set', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
expect(fixture.componentInstance.selectedFood).toBeFalsy();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFood).toBe('steak-0');
expect(fixture.componentInstance.select.value).toBe('steak-0');
expect(trigger.textContent).toContain('Steak');
fixture.componentInstance.selectedFood = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.select.value).toBeNull();
expect(trigger.textContent).not.toContain('Steak');
}));
it('should reflect the preselected value', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutFormsPreselected);
fixture.detectChanges();
flush();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
fixture.detectChanges();
expect(trigger.textContent).toContain('Pizza');
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelectorAll('mat-option')[1];
expect(option.classList).toContain('mdc-list-item--selected');
expect(fixture.componentInstance.select.value).toBe('pizza-1');
}));
it('should be able to select multiple values', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple);
fixture.detectChanges();
expect(fixture.componentInstance.selectedFoods).toBeFalsy();
const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0']);
expect(fixture.componentInstance.select.value).toEqual(['steak-0']);
expect(trigger.textContent).toContain('Steak');
options[2].click();
fixture.detectChanges();
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'sandwich-2']);
expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'sandwich-2']);
expect(trigger.textContent).toContain('Steak, Sandwich');
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
expect(trigger.textContent).toContain('Steak, Pizza, Sandwich');
}));
it('should restore focus to the host element', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
const select = fixture.debugElement.nativeElement.querySelector('mat-select');
expect(document.activeElement).withContext('Expected trigger to be focused.').toBe(select);
}));
it('should not restore focus to the host element when clicking outside', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
const select = fixture.debugElement.nativeElement.querySelector('mat-select');
fixture.detectChanges();
select.focus(); // Focus manually since the programmatic click might not do it.
fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
fixture.detectChanges();
flush();
expect(document.activeElement).withContext('Expected trigger to be focused.').toBe(select);
select.blur(); // Blur manually since the programmatic click might not do it.
(overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(document.activeElement).not.toBe(select, 'Expected trigger not to be focused.');
}));
it('should update the data binding before emitting the change event', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
const instance = fixture.componentInstance;
const spy = jasmine.createSpy('change spy');
fixture.detectChanges();
instance.select.selectionChange.subscribe(() => spy(instance.selectedFood));
expect(instance.selectedFood).toBeFalsy();
fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(instance.selectedFood).toBe('steak-0');
expect(spy).toHaveBeenCalledWith('steak-0');
}));
it('should select the active option when tabbing away while open', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
const select = fixture.nativeElement.querySelector('.mat-mdc-select');
expect(fixture.componentInstance.selectedFood).toBeFalsy();
const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');
trigger.click();
fixture.detectChanges();
flush();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFood).toBe('sandwich-2');
expect(fixture.componentInstance.select.value).toBe('sandwich-2');
expect(trigger.textContent).toContain('Sandwich');
}));
it('should not select the active option when tabbing away while close', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
fixture.detectChanges();
const select = fixture.nativeElement.querySelector('.mat-mdc-select');
expect(fixture.componentInstance.selectedFood).toBeFalsy();
const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');
trigger.click();
fixture.detectChanges();
flush();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', ESCAPE);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFood).toBeFalsy();
}));
it('should not change the multiple value selection when tabbing away', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple);
fixture.detectChanges();
expect(fixture.componentInstance.selectedFoods)
.withContext('Expected no value on init.')
.toBeFalsy();
const select = fixture.nativeElement.querySelector('.mat-mdc-select');
const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');
trigger.click();
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.selectedFoods)
.withContext('Expected no value after tabbing away.')
.toBeFalsy();
}));
it('should emit once when a reset value is selected', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
const instance = fixture.componentInstance;
const spy = jasmine.createSpy('change spy');
instance.selectedFood = 'sandwich-2';
instance.foods[0].value = null;
fixture.detectChanges();
const subscription = instance.select.selectionChange.subscribe(spy);
fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
}));
it(
'should not emit the change event multiple times when a reset option is ' +
'selected twice in a row',
fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
const instance = fixture.componentInstance;
const spy = jasmine.createSpy('change spy');
instance.foods[0].value = null;
fixture.detectChanges();
const subscription = instance.select.selectionChange.subscribe(spy);
fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(spy).not.toHaveBeenCalled();
fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
fixture.detectChanges();
flush();
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
fixture.detectChanges();
flush();
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
}),
);
});
describe('with option centering disabled', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithoutOptionCentering])));
let fixture: ComponentFixture<SelectWithoutOptionCentering>;
let trigger: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(SelectWithoutOptionCentering);
fixture.detectChanges();
flush();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
}));
it('should not align the active option with the trigger if centering is disabled', fakeAsync(() => {
trigger.click();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-mdc-select-panel')!;
// The panel should be scrolled to 0 because centering the option disabled.
// The panel should be scrolled to 0 because centering the option disabled.
expect(scrollContainer.scrollTop)
.withContext(`Expected panel not to be scrolled.`)
.toEqual(0);
// The trigger should contain 'Pizza' because it was preselected
expect(trigger.textContent).toContain('Pizza');
// The selected index should be 1 because it was preselected
expect(fixture.componentInstance.options.toArray()[1].selected).toBe(true);
}));
});
describe('positioning', () => {
beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelect])));
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let formField: HTMLElement;
let formFieldWrapper: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
flush();
formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
formFieldWrapper = formField.querySelector('.mat-mdc-text-field-wrapper') as HTMLElement;
trigger = formFieldWrapper.querySelector('.mat-mdc-select-trigger') as HTMLElement;
}));
it('should position the panel under the form field by default', fakeAsync(() => {
formField.style.position = 'fixed';
formField.style.left = formField.style.top = '10%';
trigger.click();
fixture.detectChanges();
flush();
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
const paneRect = panel.getBoundingClientRect();
const formFieldWrapperRect = formFieldWrapper.getBoundingClientRect();
expect(panel.classList).not.toContain('mat-mdc-select-panel-above');
expect(Math.floor(paneRect.width)).toBe(Math.floor(formFieldWrapperRect.width));
expect(Math.floor(paneRect.left)).toBe(Math.floor(formFieldWrapperRect.left));
expect(Math.floor(paneRect.top)).toBe(Math.floor(formFieldWrapperRect.bottom));
}));
it('should position the panel under the form field by default', fakeAsync(() => {
formField.style.position = 'fixed';
formField.style.left = '10%';
formField.style.bottom = '0';
trigger.click();
fixture.detectChanges();
flush();
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
const paneRect = panel.getBoundingClientRect();
const formFieldWrapperRect = formFieldWrapper.getBoundingClientRect();
expect(panel.classList).toContain('mat-mdc-select-panel-above');
expect(Math.floor(paneRect.width)).toBe(Math.floor(formFieldWrapperRect.width));
expect(Math.floor(paneRect.left)).toBe(Math.floor(formFieldWrapperRect.left));
expect(Math.floor(paneRect.bottom)).toBe(Math.floor(formFieldWrapperRect.top));
}));
});
describe('with multiple selection', () => {
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([MultiSelect, MultiSelectWithLotsOfOptions])));
let fixture: ComponentFixture<MultiSelect>;
let testInstance: MultiSelect;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(MultiSelect);
testInstance = fixture.componentInstance;
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
});
it('should be able to select multiple values', () => {
trigger.click();
fixture.detectChanges();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
options[2].click();
options[5].click();
fixture.detectChanges();
expect(testInstance.control.value).toEqual(['steak-0', 'tacos-2', 'eggs-5']);
});
it('should be able to toggle an option on and off', () => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
expect(testInstance.control.value).toEqual(['steak-0']);
option.click();
fixture.detectChanges();
expect(testInstance.control.value).toEqual([]);
});
it('should update the label', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
options[2].click();
options[5].click();
fixture.detectChanges();
expect(trigger.textContent).toContain('Steak, Tacos, Eggs');
options[2].click();
fixture.detectChanges();
expect(trigger.textContent).toContain('Steak, Eggs');
}));
it('should be able to set the selected value by taking an array', () => {
trigger.click();
testInstance.control.setValue(['steak-0', 'eggs-5']);
fixture.detectChanges();
const optionNodes = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
const optionInstances = testInstance.options.toArray();
expect(optionNodes[0].classList).toContain('mdc-list-item--selected');
expect(optionNodes[5].classList).toContain('mdc-list-item--selected');
expect(optionInstances[0].selected).toBe(true);
expect(optionInstances[5].selected).toBe(true);
});
it('should override the previously-selected value when setting an array', () => {
trigger.click();
fixture.detectChanges();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
expect(options[0].classList).toContain('mdc-list-item--selected');
testInstance.control.setValue(['eggs-5']);
fixture.detectChanges();
expect(options[0].classList).not.toContain('mdc-list-item--selected');
expect(options[5].classList).toContain('mdc-list-item--selected');
});
it('should not close the panel when clicking on options', () => {
trigger.click();
fixture.detectChanges();
expect(testInstance.select.panelOpen).toBe(true);
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
options[1].click();
fixture.detectChanges();
expect(testInstance.select.panelOpen).toBe(true);
});
it('should sort the selected options based on their order in the panel', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[2].click();
options[0].click();
options[1].click();
fixture.detectChanges();
expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
}));
it('should sort the selected options in reverse in rtl', fakeAsync(() => {
dir.value = 'rtl';
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[2].click();
options[0].click();
options[1].click();
fixture.detectChanges();
expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
}));
it('should be able to customize the value sorting logic', fakeAsync(() => {
fixture.componentInstance.sortComparator = (a, b, optionsArray) => {
return optionsArray.indexOf(b) - optionsArray.indexOf(a);
};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
for (let i = 0; i < 3; i++) {
options[i].click();
}
fixture.detectChanges();
// Expect the items to be in reverse order.
expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
expect(fixture.componentInstance.control.value).toEqual(['tacos-2', 'pizza-1', 'steak-0']);
}));
it('should sort the values that get set via the model based on the panel order', () => {
trigger.click();
fixture.detectChanges();
testInstance.control.setValue(['tacos-2', 'steak-0', 'pizza-1']);
fixture.detectChanges();
expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
});
it('should reverse sort the values, that get set via the model in rtl', () => {
dir.value = 'rtl';
trigger.click();
fixture.detectChanges();
testInstance.control.setValue(['tacos-2', 'steak-0', 'pizza-1']);
fixture.detectChanges();
expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
});
it('should throw an exception when trying to set a non-array value', () => {
expect(() => {
testInstance.control.setValue('not-an-array' as any);
}).toThrowError(wrappedErrorMessage(getMatSelectNonArrayValueError()));
});
it('should throw an exception when trying to change multiple mode after init', () => {
expect(() => {
testInstance.select.multiple = false;
}).toThrowError(wrappedErrorMessage(getMatSelectDynamicMultipleError()));
});
it('should pass the `multiple` value to all of the option instances', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(testInstance.options.toArray().every(option => !!option.multiple))
.withContext('Expected `multiple` to have been added to initial set of options.')
.toBe(true);
testInstance.foods.push({value: 'cake-8', viewValue: 'Cake'});
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
expect(testInstance.options.toArray().every(option => !!option.multiple))
.withContext('Expected `multiple` to have been set on dynamically-added option.')
.toBe(true);
}));
it('should update the active item index on click', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[2].click();
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(2);
}));
it('should be to select an option with a `null` value', () => {
fixture.componentInstance.foods = [
{value: null, viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: null, viewValue: 'Tacos'},
];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
const options = overlayContainerElement.querySelectorAll(
'mat-option',
) as NodeListOf<HTMLElement>;
options[0].click();
options[1].click();
options[2].click();
fixture.detectChanges();
expect(testInstance.control.value).toEqual([null!, 'pizza-1', null!]);
});
it('should select all options when pressing ctrl + a', () => {
const selectElement = fixture.nativeElement.querySelector('mat-select');
const options = fixture.componentInstance.options.toArray();
expect(testInstance.control.value).toBeFalsy();
expect(options.every(option => option.selected)).toBe(false);
fixture.componentInstance.select.open();
fixture.detectChanges();
const event = createKeyboardEvent('keydown', A, undefined, {control: true});
dispatchEvent(selectElement, event);
fixture.detectChanges();
expect(options.every(option => option.selected)).toBe(true);
expect(testInstance.control.value).toEqual([
'steak-0',
'pizza-1',
'tacos-2',
'sandwich-3',
'chips-4',
'eggs-5',
'pasta-6',
'sushi-7',
]);
});
it('should skip disabled options when using ctrl + a', () => {
const selectElement = fixture.nativeElement.querySelector('mat-select');
const options = fixture.componentInstance.options.toArray();
for (let i = 0; i < 3; i++) {
options[i].disabled = true;
}
expect(testInstance.control.value).toBeFalsy();
fixture.componentInstance.select.open();
fixture.detectChanges();
const event = createKeyboardEvent('keydown', A, undefined, {control: true});
dispatchEvent(selectElement, event);
fixture.detectChanges();
expect(testInstance.control.value).toEqual([
'sandwich-3',
'chips-4',
'eggs-5',
'pasta-6',
'sushi-7',
]);
});
it('should select all options when pressing ctrl + a when some options are selected', () => {
const selectElement = fixture.nativeElement.querySelector('mat-select');
const options = fixture.componentInstance.options.toArray();
options[0].select();
fixture.detectChanges();
expect(testInstance.control.value).toEqual(['steak-0']);
expect(options.some(option => option.selected)).toBe(true);
fixture.componentInstance.select.open();
fixture.detectChanges();
const event = createKeyboardEvent('keydown', A, undefined, {control: true});
dispatchEvent(selectElement, event);
fixture.detectChanges();
expect(options.every(option => option.selected)).toBe(true);
expect(testInstance.control.value).toEqual([
'steak-0',
'pizza-1',
'tacos-2',
'sandwich-3',
'chips-4',
'eggs-5',
'pasta-6',
'sushi-7',
]);
});
it('should deselect all options with ctrl + a if all options are selected', () => {
const selectElement = fixture.nativeElement.querySelector('mat-select');
const options = fixture.componentInstance.options.toArray();
options.forEach(option => option.select());
fixture.detectChanges();
expect(testInstance.control.value).toEqual([
'steak-0',
'pizza-1',
'tacos-2',
'sandwich-3',
'chips-4',
'eggs-5',
'pasta-6',
'sushi-7',
]);
expect(options.every(option => option.selected)).toBe(true);
fixture.componentInstance.select.open();
fixture.detectChanges();
const event = createKeyboardEvent('keydown', A, undefined, {control: true});
dispatchEvent(selectElement, event);
fixture.detectChanges();
expect(options.some(option => option.selected)).toBe(false);
expect(testInstance.control.value).toEqual([]);
});
it('should not throw when selecting a large amount of options', fakeAsync(() => {
fixture.destroy();
const lotsOfOptionsFixture = TestBed.createComponent(MultiSelectWithLotsOfOptions);
expect(() => {
lotsOfOptionsFixture.componentInstance.checkAll();
lotsOfOptionsFixture.detectChanges();
flush();
}).not.toThrow();
}));
it('should be able to programmatically set an array with duplicate values', () => {
testInstance.foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'pizza-1', viewValue: 'Pizza'},
];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
testInstance.control.setValue(['steak-0', 'pizza-1', 'pizza-1', 'pizza-1']);
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
const optionNodes = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
const optionInstances = testInstance.options.toArray();
expect(optionNodes.map(node => node.classList.contains('mdc-list-item--selected'))).toEqual([
true,
true,
true,
true,
false,
false,
]);
expect(optionInstances.map(instance => instance.selected)).toEqual([
true,
true,
true,
true,
false,
false,
]);
});
it('should update the option selected state if the same array is mutated and passed back in', () => {
const value: string[] = [];
trigger.click();
testInstance.control.setValue(value);
fixture.detectChanges();
const optionNodes = Array.from<HTMLElement>(
overlayContainerElement.querySelectorAll('mat-option'),
);
const optionInstances = testInstance.options.toArray();
expect(
optionNodes.some(option => {
return option.classList.contains('mdc-list-item--selected');
}),
).toBe(false);
expect(optionInstances.some(option => option.selected)).toBe(false);
value.push('eggs-5');
testInstance.control.setValue(value);
fixture.detectChanges();
expect(optionNodes[5].classList).toContain('mdc-list-item--selected');
expect(optionInstances[5].selected).toBe(true);
});
});
it('should be able to provide default values through an injection token', fakeAsync(() => {
configureMatSelectTestingModule(
[NgModelSelect],
[
{
provide: MAT_SELECT_CONFIG,
useValue: {
disableOptionCentering: true,
typeaheadDebounceInterval: 1337,
overlayPanelClass: 'test-panel-class',
panelWidth: null,
} as MatSelectConfig,
},
],
);
const fixture = TestBed.createComponent(NgModelSelect);
fixture.detectChanges();
const select = fixture.componentInstance.select;
select.open();
fixture.detectChanges();
flush();
expect(select.disableOptionCentering).toBe(true);
expect(select.typeaheadDebounceInterval).toBe(1337);
expect(document.querySelector('.cdk-overlay-pane')?.classList).toContain('test-panel-class');
expect(select.panelWidth).toBeNull();
}));
it('should be able to hide checkmark icon through an injection token', () => {
const matSelectConfig: MatSelectConfig = {hideSingleSelectionIndicator: true};
configureMatSelectTestingModule(
[NgModelSelect],
[
{
provide: MAT_SELECT_CONFIG,
useValue: matSelectConfig,
},
],
);
const fixture = TestBed.createComponent(NgModelSelect);
fixture.detectChanges();
const select = fixture.componentInstance.select;
fixture.componentInstance.select.value = fixture.componentInstance.foods[0].value;
fixture.changeDetectorRef.markForCheck();
select.open();
fixture.detectChanges();
// Select the first value to ensure selection state is displayed. That way this test ensures
// that the selection state hides the checkmark icon, rather than hiding the checkmark icon
// because nothing is selected.
expect(document.querySelector('mat-option[aria-selected="true"]'))
.withContext('expecting selection state to be displayed')
.not.toBeNull();
const pseudoCheckboxes = document.querySelectorAll('.mat-pseudo-checkbox');
expect(pseudoCheckboxes.length)
.withContext('expecting not to display a pseudo-checkbox')
.toBe(0);
});
it('should not not throw if the select is inside an ng-container with ngIf', () => {
configureMatSelectTestingModule([SelectInNgContainer]);
const fixture = TestBed.createComponent(SelectInNgContainer);
expect(() => fixture.detectChanges()).not.toThrow();
});
describe('page up/down with disabled options', () => {
let fixture: ComponentFixture<BasicSelectWithFirstAndLastOptionDisabled>;
let host: HTMLElement;
beforeEach(waitForAsync(() =>
configureMatSelectTestingModule([BasicSelectWithFirstAndLastOptionDisabled])));
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelectWithFirstAndLastOptionDisabled);
fixture.detectChanges();
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
fixture.detectChanges();
host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
}));
it('should be able to scroll to disabled option when pressing PAGE_UP', fakeAsync(() => {
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(1);
dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
}));
it('should be able to scroll to disabled option when pressing PAGE_DOWN', fakeAsync(() => {
dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
}));
});
});
@Component({
selector: 'basic-select',
template: `
<div [style.height.px]="heightAbove"></div>
<mat-form-field>
@if (hasLabel) {
<mat-label>Select a food</mat-label>
}
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
[tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
[aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
[panelClass]="panelClass" [disableRipple]="disableRipple"
[typeaheadDebounceInterval]="typeaheadDebounceInterval"
[panelWidth]="panelWidth">
@for (food of foods; track food) {
<mat-option [value]="food.value" [disabled]="food.disabled">
{{ capitalize ? food.viewValue.toUpperCase() : food.viewValue }}
</mat-option>
}
</mat-select>
@if (hint) {
<mat-hint>{{ hint }}</mat-hint>
}
</mat-form-field>
<div [style.height.px]="heightBelow"></div>
`,
standalone: false,
})
class BasicSelect {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos', disabled: true},
{value: 'sandwich-3', viewValue: 'Sandwich'},
{value: 'chips-4', viewValue: 'Chips'},
{value: 'eggs-5', viewValue: 'Eggs'},
{value: 'pasta-6', viewValue: 'Pasta'},
{value: 'sushi-7', viewValue: 'Sushi'},
];
control = new FormControl<string | null>(null);
isRequired: boolean;
heightAbove = 0;
heightBelow = 0;
hasLabel = true;
hint: string;
tabIndexOverride: number;
ariaDescribedBy: string;
ariaLabel: string;
ariaLabelledby: string;
panelClass = ['custom-one', 'custom-two'];
disableRipple: boolean;
typeaheadDebounceInterval: number;
capitalize = false;
panelWidth: string | null | number = 'auto';
@ViewChild(MatSelect, {static: true}) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'ng-model-select',
template: `
<mat-form-field>
<mat-select placeholder="Food" ngModel [disabled]="isDisabled">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class NgModelSelect {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
isDisabled: boolean;
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'many-selects',
template: `
<mat-form-field>
<mat-select placeholder="First">
<mat-option value="one">one</mat-option>
<mat-option value="two">two</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Second">
<mat-option value="three">three</mat-option>
<mat-option value="four">four</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class ManySelects {}
@Component({
selector: 'ng-if-select',
template: `
@if (isShowing) {
<div>
<mat-form-field>
<mat-select placeholder="Food I want to eat right now" [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
`,
standalone: false,
})
class NgIfSelect {
isShowing = false;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
control = new FormControl('pizza-1');
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
selector: 'select-with-change-event',
template: `
<mat-form-field>
<mat-select (selectionChange)="changeListener($event)">
@for (food of foods; track food) {
<mat-option [value]="food">{{ food }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithChangeEvent {
foods: string[] = [
'steak-0',
'pizza-1',
'tacos-2',
'sandwich-3',
'chips-4',
'eggs-5',
'pasta-6',
'sushi-7',
];
changeListener = jasmine.createSpy('MatSelect change listener');
}
@Component({
selector: 'select-init-without-options',
template: `
<mat-form-field>
<mat-select placeholder="Food I want to eat right now" [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectInitWithoutOptions {
foods: any[];
control = new FormControl('pizza-1');
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
addOptions() {
this.foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
}
}
@Component({
selector: 'custom-select-accessor',
template: `<mat-form-field><mat-select></mat-select></mat-form-field>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomSelectAccessor,
multi: true,
},
],
standalone: false,
})
class CustomSelectAccessor implements ControlValueAccessor {
@ViewChild(MatSelect) select: MatSelect;
writeValue: (value?: any) => void = () => {};
registerOnChange: (changeFn?: (value: any) => void) => void = () => {};
registerOnTouched: (touchedFn?: () => void) => void = () => {};
}
@Component({
selector: 'comp-with-custom-select',
template: `<custom-select-accessor [formControl]="ctrl"></custom-select-accessor>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomSelectAccessor,
multi: true,
},
],
standalone: false,
})
class CompWithCustomSelect {
ctrl = new FormControl('initial value');
@ViewChild(CustomSelectAccessor, {static: true}) customAccessor: CustomSelectAccessor;
}
@Component({
selector: 'select-infinite-loop',
template: `
<mat-form-field>
<mat-select [(ngModel)]="value"></mat-select>
</mat-form-field>
<throws-error-on-init></throws-error-on-init>
`,
standalone: false,
})
class SelectWithErrorSibling {
value: string;
}
@Component({
selector: 'throws-error-on-init',
template: '',
standalone: false,
})
class ThrowsErrorOnInit implements OnInit {
ngOnInit() {
throw Error('Oh no!');
}
}
@Component({
selector: 'basic-select-on-push',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-form-field>
<mat-select placeholder="Food" [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectOnPush {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
control = new FormControl('');
}
@Component({
selector: 'basic-select-on-push-preselected',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-form-field>
<mat-select placeholder="Food" [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectOnPushPreselected {
@ViewChild(MatSelect) select: MatSelect;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
control = new FormControl('pizza-1');
}
@Component({
selector: 'floating-label-select',
template: `
<mat-form-field [floatLabel]="floatLabel">
<mat-label>Select a food</mat-label>
<mat-select [placeholder]="placeholder" [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class FloatLabelSelect {
floatLabel: FloatLabelType | null = 'auto';
control = new FormControl('');
placeholder = 'Food I want to eat right now';
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
selector: 'multi-select',
template: `
<mat-form-field>
<mat-select multiple placeholder="Food" [formControl]="control"
[sortComparator]="sortComparator">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class MultiSelect {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
{value: 'sandwich-3', viewValue: 'Sandwich'},
{value: 'chips-4', viewValue: 'Chips'},
{value: 'eggs-5', viewValue: 'Eggs'},
{value: 'pasta-6', viewValue: 'Pasta'},
{value: 'sushi-7', viewValue: 'Sushi'},
];
control = new FormControl<string[] | null>(null);
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
sortComparator: (a: MatOption, b: MatOption, options: MatOption[]) => number;
}
@Component({
selector: 'select-with-plain-tabindex',
template: `<mat-form-field><mat-select tabindex="5"></mat-select></mat-form-field>`,
standalone: false,
})
class SelectWithPlainTabindex {}
@Component({
selector: 'select-early-sibling-access',
template: `
<mat-form-field>
<mat-select #select="matSelect"></mat-select>
</mat-form-field>
@if (select.selected) {
<div></div>
}
`,
standalone: false,
})
class SelectEarlyAccessSibling {}
@Component({
selector: 'basic-select-initially-hidden',
template: `
<mat-form-field>
<mat-select [style.display]="isVisible ? 'block' : 'none'">
<mat-option value="value">There are no other options</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectInitiallyHidden {
isVisible = false;
}
@Component({
selector: 'basic-select-no-placeholder',
template: `
<mat-form-field>
<mat-select>
<mat-option value="value">There are no other options</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectNoPlaceholder {}
@Component({
selector: 'basic-select-with-theming',
template: `
<mat-form-field [color]="theme">
<mat-select placeholder="Food">
<mat-option value="steak-0">Steak</mat-option>
<mat-option value="pizza-1">Pizza</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectWithTheming {
@ViewChild(MatSelect) select: MatSelect;
theme: string;
}
@Component({
selector: 'reset-values-select',
template: `
<mat-form-field>
<mat-label>Select a food</mat-label>
<mat-select [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
<mat-option>None</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class ResetValuesSelect {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
{value: false, viewValue: 'Falsy'},
{viewValue: 'Undefined'},
{value: null, viewValue: 'Null'},
];
control = new FormControl('' as string | boolean | null);
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
template: `
<mat-form-field>
<mat-select [formControl]="control">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class FalsyValueSelect {
foods: any[] = [
{value: 0, viewValue: 'Steak'},
{value: 1, viewValue: 'Pizza'},
];
control = new FormControl<number | null>(null);
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'select-with-groups',
template: `
<mat-form-field>
<mat-select placeholder="Pokemon" [formControl]="control">
@for (group of pokemonTypes; track group) {
<mat-optgroup [label]="group.name" [disabled]="group.disabled">
@for (pokemon of group.pokemon; track pokemon) {
<mat-option [value]="pokemon.value">{{ pokemon.viewValue }}</mat-option>
}
</mat-optgroup>
}
<mat-option value="mime-11">Mr. Mime</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithGroups {
control = new FormControl('');
pokemonTypes = [
{
name: 'Grass',
pokemon: [
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
{value: 'oddish-1', viewValue: 'Oddish'},
{value: 'bellsprout-2', viewValue: 'Bellsprout'},
],
},
{
name: 'Water',
disabled: true,
pokemon: [
{value: 'squirtle-3', viewValue: 'Squirtle'},
{value: 'psyduck-4', viewValue: 'Psyduck'},
{value: 'horsea-5', viewValue: 'Horsea'},
],
},
{
name: 'Fire',
pokemon: [
{value: 'charmander-6', viewValue: 'Charmander'},
{value: 'vulpix-7', viewValue: 'Vulpix'},
{value: 'flareon-8', viewValue: 'Flareon'},
],
},
{
name: 'Psychic',
pokemon: [
{value: 'mew-9', viewValue: 'Mew'},
{value: 'mewtwo-10', viewValue: 'Mewtwo'},
],
},
];
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'select-with-groups',
template: `
<mat-form-field>
<mat-select placeholder="Pokemon" [formControl]="control">
@for (group of pokemonTypes; track group) {
<mat-optgroup [label]="group.name">
@for (pokemon of group.pokemon; track pokemon) {
<mat-option [value]="pokemon.value">{{ pokemon.viewValue }}</mat-option>
}
</mat-optgroup>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithGroupsAndNgContainer {
control = new FormControl('');
pokemonTypes = [
{
name: 'Grass',
pokemon: [{value: 'bulbasaur-0', viewValue: 'Bulbasaur'}],
},
];
}
@Component({
template: `
<form>
<mat-form-field>
<mat-select [(ngModel)]="value"></mat-select>
</mat-form-field>
</form>
`,
standalone: false,
})
class InvalidSelectInForm {
value: any;
}
@Component({
template: `
<form [formGroup]="formGroup">
<mat-form-field>
<mat-label>Food</mat-label>
<mat-select formControlName="food">
@for (option of options; track option) {
<mat-option [value]="option.value">{{option.viewValue}}</mat-option>
}
</mat-select>
<mat-error>This field is required</mat-error>
</mat-form-field>
</form>
`,
standalone: false,
})
class SelectInsideFormGroup {
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
@ViewChild(MatSelect) select: MatSelect;
options = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
];
formControl = new FormControl('', Validators.required);
formGroup = new FormGroup({
food: this.formControl,
});
}
@Component({
template: `
<mat-form-field>
<mat-select placeholder="Food" [(value)]="selectedFood">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectWithoutForms {
selectedFood: string | null;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'sandwich-2', viewValue: 'Sandwich'},
];
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
template: `
<mat-form-field>
<mat-select placeholder="Food" [(value)]="selectedFood">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectWithoutFormsPreselected {
selectedFood = 'pizza-1';
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
];
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
template: `
<mat-form-field>
<mat-select placeholder="Food" [(value)]="selectedFoods" multiple>
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class BasicSelectWithoutFormsMultiple {
selectedFoods: string[];
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'sandwich-2', viewValue: 'Sandwich'},
];
@ViewChild(MatSelect) select: MatSelect;
}
@Component({
selector: 'select-with-custom-trigger',
template: `
<mat-form-field>
<mat-select placeholder="Food" [formControl]="control" #select="matSelect">
<mat-select-trigger>
{{ select.selected?.viewValue.split('').reverse().join('') }}
</mat-select-trigger>
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithCustomTrigger {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
];
control = new FormControl('');
}
@Component({
selector: 'ng-model-compare-with',
template: `
<mat-form-field>
<mat-select [ngModel]="selectedFood" (ngModelChange)="setFoodByCopy($event)"
[compareWith]="comparator">
@for (food of foods; track food) {
<mat-option [value]="food">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class NgModelCompareWithSelect {
foods: {value: string; viewValue: string}[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
selectedFood: {value: string; viewValue: string} = {value: 'pizza-1', viewValue: 'Pizza'};
comparator: ((f1: any, f2: any) => boolean) | null = this.compareByValue;
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
useCompareByValue() {
this.comparator = this.compareByValue;
}
useCompareByReference() {
this.comparator = this.compareByReference;
}
useNullComparator() {
this.comparator = null;
}
compareByValue(f1: any, f2: any) {
return f1 && f2 && f1.value === f2.value;
}
compareByReference(f1: any, f2: any) {
return f1 === f2;
}
setFoodByCopy(newValue: {value: string; viewValue: string}) {
this.selectedFood = {...{}, ...newValue};
}
}
@Component({
template: `
<mat-select placeholder="Food" [formControl]="control" [errorStateMatcher]="errorStateMatcher">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
`,
standalone: false,
})
class CustomErrorBehaviorSelect {
@ViewChild(MatSelect) select: MatSelect;
control = new FormControl('');
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
];
errorStateMatcher: ErrorStateMatcher;
}
@Component({
template: `
<mat-form-field>
<mat-select placeholder="Food" [(ngModel)]="selectedFoods">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SingleSelectWithPreselectedArrayValues {
foods: any[] = [
{value: ['steak-0', 'steak-1'], viewValue: 'Steak'},
{value: ['pizza-1', 'pizza-2'], viewValue: 'Pizza'},
{value: ['tacos-2', 'tacos-3'], viewValue: 'Tacos'},
];
selectedFoods = this.foods[1].value;
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'select-without-option-centering',
template: `
<mat-form-field>
<mat-select placeholder="Food" [formControl]="control" disableOptionCentering>
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithoutOptionCentering {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
{value: 'sandwich-3', viewValue: 'Sandwich'},
{value: 'chips-4', viewValue: 'Chips'},
{value: 'eggs-5', viewValue: 'Eggs'},
{value: 'pasta-6', viewValue: 'Pasta'},
{value: 'sushi-7', viewValue: 'Sushi'},
];
control = new FormControl('pizza-1');
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
template: `
<mat-form-field>
<mat-label>Select a thing</mat-label>
<mat-select [placeholder]="placeholder">
<mat-option value="thing">A thing</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithFormFieldLabel {
placeholder: string;
}
@Component({
template: `
<mat-form-field appearance="fill">
<mat-label>Select something</mat-label>
@if (showSelect) {
<mat-select>
<mat-option value="1">One</mat-option>
</mat-select>
}
</mat-form-field>
`,
standalone: false,
})
class SelectWithNgIfAndLabel {
showSelect = true;
}
@Component({
template: `
<mat-form-field>
<mat-select multiple [ngModel]="value">
@for (item of items; track item) {
<mat-option [value]="item">{{item}}</mat-option>
}
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class MultiSelectWithLotsOfOptions {
items = new Array(100).fill(0).map((_, i) => i);
value: number[] = [];
checkAll() {
this.value = [...this.items];
}
uncheckAll() {
this.value = [];
}
}
@Component({
selector: 'basic-select-with-reset',
template: `
<mat-form-field>
<mat-select [formControl]="control">
<mat-option>Reset</mat-option>
<mat-option value="a">A</mat-option>
<mat-option value="b">B</mat-option>
<mat-option value="c">C</mat-option>
</mat-select>
</mat-form-field>
`,
standalone: false,
})
class SelectWithResetOptionAndFormControl {
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
control = new FormControl('');
}
@Component({
selector: 'select-with-placeholder-in-ngcontainer-with-ngIf',
template: `
<mat-form-field>
@if (true) {
<mat-select placeholder="Product Area">
<mat-option value="a">A</mat-option>
<mat-option value="b">B</mat-option>
<mat-option value="c">C</mat-option>
</mat-select>
}
</mat-form-field>
`,
standalone: false,
})
class SelectInNgContainer {}
@Component({
template: `
<form [formGroup]="form">
<mat-form-field>
<mat-select formControlName="control">
<mat-option value="1">One</mat-option>
</mat-select>
</mat-form-field>
</form>
`,
standalone: false,
})
class SelectInsideDynamicFormGroup {
private _formBuilder = inject(FormBuilder);
@ViewChild(MatSelect) select: MatSelect;
form: FormGroup;
private readonly _changeDetectorRef = inject(ChangeDetectorRef);
constructor() {
this.assignGroup(false);
}
assignGroup(isDisabled: boolean) {
this.form = this._formBuilder.group({
control: {value: '', disabled: isDisabled},
});
this._changeDetectorRef.markForCheck();
}
}
@Component({
selector: 'basic-select',
template: `
<div [style.height.px]="heightAbove"></div>
<mat-form-field>
@if (hasLabel) {
<mat-label>Select a food</mat-label>
}
<mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
[tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
[aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
[panelClass]="panelClass" [disableRipple]="disableRipple"
[typeaheadDebounceInterval]="typeaheadDebounceInterval">
@for (food of foods; track food) {
<mat-option [value]="food.value" [disabled]="food.disabled">
{{ food.viewValue }}
</mat-option>
}
</mat-select>
@if (hint) {
<mat-hint>{{ hint }}</mat-hint>
}
</mat-form-field>
<div [style.height.px]="heightBelow"></div>
`,
standalone: false,
})
class BasicSelectWithFirstAndLastOptionDisabled {
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak', disabled: true},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
{value: 'sandwich-3', viewValue: 'Sandwich'},
{value: 'chips-4', viewValue: 'Chips'},
{value: 'eggs-5', viewValue: 'Eggs'},
{value: 'pasta-6', viewValue: 'Pasta'},
{value: 'sushi-7', viewValue: 'Sushi', disabled: true},
];
control = new FormControl<string | null>(null);
isRequired: boolean;
heightAbove = 0;
heightBelow = 0;
hasLabel = true;
hint: string;
tabIndexOverride: number;
ariaDescribedBy: string;
ariaLabel: string;
ariaLabelledby: string;
panelClass = ['custom-one', 'custom-two'];
disableRipple: boolean;
typeaheadDebounceInterval: number;
@ViewChild(MatSelect, {static: true}) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
}
@Component({
selector: 'select-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>Select a food</mat-label>
<mat-select placeholder="Food" ngModel>
@for (food of foods; track food) {
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</ng-template>
`,
standalone: false,
})
class SelectInsideAModal {
foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
@ViewChild(MatSelect) select: MatSelect;
@ViewChildren(MatOption) options: QueryList<MatOption>;
@ViewChild('modal') modal: ElementRef;
}