sass-references/angular-material/material/chips/chip-listbox.spec.ts

1076 lines
37 KiB
TypeScript

import {Direction, Directionality} from '@angular/cdk/bidi';
import {END, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
patchElementFocus,
} from '@angular/cdk/testing/private';
import {
Component,
DebugElement,
EventEmitter,
QueryList,
Type,
ViewChild,
ViewChildren,
} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {MatChipListbox, MatChipOption, MatChipsModule} from './index';
describe('MatChipListbox', () => {
let fixture: ComponentFixture<any>;
let chipListboxDebugElement: DebugElement;
let chipListboxNativeElement: HTMLElement;
let chipListboxInstance: MatChipListbox;
let testComponent: StandardChipListbox;
let chips: QueryList<MatChipOption>;
let directionality: {value: Direction; change: EventEmitter<Direction>};
let primaryActions: NodeListOf<HTMLElement>;
describe('StandardChipList', () => {
describe('basic behaviors', () => {
beforeEach(() => {
createComponent(StandardChipListbox);
});
it('should add the `mat-mdc-chip-set` class', () => {
expect(chipListboxNativeElement.classList).toContain('mat-mdc-chip-set');
});
it('should not have the aria-selected attribute when it is not selectable', fakeAsync(() => {
testComponent.selectable = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
const chipsValid = chips
.toArray()
.every(
chip =>
!chip.selectable && !chip._elementRef.nativeElement.hasAttribute('aria-selected'),
);
expect(chipsValid).toBe(true);
}));
it('should toggle the chips disabled state based on whether it is disabled', () => {
expect(chips.toArray().every(chip => chip.disabled)).toBe(false);
chipListboxInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chips.toArray().every(chip => chip.disabled)).toBe(true);
chipListboxInstance.disabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chips.toArray().every(chip => chip.disabled)).toBe(false);
});
it('should disable a chip that is added after the listbox became disabled', fakeAsync(() => {
expect(chips.toArray().every(chip => chip.disabled)).toBe(false);
chipListboxInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chips.toArray().every(chip => chip.disabled)).toBe(true);
fixture.componentInstance.chips.push(5, 6);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(chips.toArray().every(chip => chip.disabled)).toBe(true);
}));
it('should not set a role on the grid when the list is empty', () => {
testComponent.chips = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.hasAttribute('role')).toBe(false);
});
it('should be able to set a custom role', () => {
testComponent.role = 'grid';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.getAttribute('role')).toBe('grid');
});
it('should not set aria-required when it does not have a role', () => {
testComponent.chips = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.hasAttribute('role')).toBe(false);
expect(chipListboxNativeElement.hasAttribute('aria-required')).toBe(false);
});
it('should toggle the chips disabled state based on whether it is disabled', fakeAsync(() => {
fixture.destroy();
TestBed.resetTestingModule();
const disabledFixture = createComponent(IndividuallyDisabledChipInsideForm);
disabledFixture.detectChanges();
flush();
expect(disabledFixture.componentInstance.chip.disabled).toBe(true);
}));
});
describe('with selected chips', () => {
beforeEach(() => {
fixture = createComponent(SelectedChipListbox);
});
it('should not override chips selected', () => {
const instanceChips = fixture.componentInstance.chips.toArray();
expect(instanceChips[0].selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(instanceChips[1].selected)
.withContext('Expected second option to be not selected.')
.toBe(false);
expect(instanceChips[2].selected)
.withContext('Expected third option to be selected.')
.toBe(true);
});
it('should have role listbox', () => {
expect(chipListboxNativeElement.getAttribute('role')).toBe('listbox');
});
it('should not have role when empty', () => {
fixture.componentInstance.foods = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.getAttribute('role'))
.withContext('Expect no role attribute')
.toBeNull();
});
});
describe('focus behaviors', () => {
beforeEach(() => {
createComponent(StandardChipListbox);
});
it('should focus the first chip on focus', () => {
chipListboxInstance.focus();
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[0]);
});
it('should focus the primary action when calling the `focus` method', () => {
chips.last.focus();
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
});
it('should not be able to become focused when disabled', () => {
expect(chipListboxInstance.focused)
.withContext('Expected listbox to not be focused.')
.toBe(false);
chipListboxInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
chipListboxInstance.focus();
fixture.detectChanges();
expect(chipListboxInstance.focused)
.withContext('Expected listbox to continue not to be focused')
.toBe(false);
});
it('should remove the tabindex from the listbox if it is disabled', () => {
expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('0');
chipListboxInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('-1');
});
describe('on chip destroy', () => {
it('should focus the next item', () => {
const midItem = chips.get(2)!;
// Focus the middle item
patchElementFocus(midItem.primaryAction!._elementRef.nativeElement);
midItem.focus();
// Destroy the middle item
testComponent.chips.splice(2, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// It focuses the 4th item
expect(document.activeElement).toBe(primaryActions[3]);
});
it('should focus the previous item', () => {
// Focus the last item
patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
chips.last.focus();
// Destroy the last item
testComponent.chips.pop();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// It focuses the next-to-last item
expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
});
it('should not focus if chip listbox is not focused', fakeAsync(() => {
const midItem = chips.get(2)!;
// Focus and blur the middle item
midItem.focus();
(document.activeElement as HTMLElement).blur();
tick();
// Destroy the middle item
testComponent.chips.splice(2, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
// Should not have focus
expect(chipListboxNativeElement.contains(document.activeElement)).toBe(false);
}));
it('should focus the listbox if the last focused item is removed', fakeAsync(() => {
testComponent.chips = [0];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
spyOn(chipListboxInstance, 'focus');
patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
chips.last.focus();
testComponent.chips.pop();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxInstance.focus).toHaveBeenCalled();
}));
});
});
describe('keyboard behavior', () => {
describe('LTR (default)', () => {
beforeEach(() => {
createComponent(StandardChipListbox);
});
it('should focus previous item when press LEFT ARROW', () => {
const lastIndex = primaryActions.length - 1;
// Focus the last item in the array
chips.last.focus();
expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the LEFT arrow
dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next item when press RIGHT ARROW', () => {
// Focus the last item in the array
chips.first.focus();
expect(document.activeElement).toBe(primaryActions[0]);
// Press the RIGHT arrow
dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
expect(document.activeElement).toBe(primaryActions[1]);
});
it('should not handle arrow key events from non-chip elements', () => {
const previousActiveElement = document.activeElement;
dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement)
.withContext('Expected focused item not to have changed.')
.toBe(previousActiveElement);
});
it('should focus the first item when pressing HOME', () => {
const lastAction = primaryActions[primaryActions.length - 1];
chips.last.focus();
expect(document.activeElement).toBe(lastAction);
const event = dispatchKeyboardEvent(lastAction, 'keydown', HOME);
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[0]);
expect(event.defaultPrevented).toBe(true);
});
it('should focus the last item when pressing END', () => {
chips.first.focus();
expect(document.activeElement).toBe(primaryActions[0]);
const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END);
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
expect(event.defaultPrevented).toBe(true);
});
});
describe('RTL', () => {
beforeEach(() => {
createComponent(StandardChipListbox, 'rtl');
});
it('should focus previous item when press RIGHT ARROW', () => {
const lastIndex = primaryActions.length - 1;
// Focus the last item in the array
chips.last.focus();
expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the RIGHT arrow
dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next item when press LEFT ARROW', () => {
// Focus the last item in the array
chips.first.focus();
expect(document.activeElement).toBe(primaryActions[0]);
// Press the LEFT arrow
dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
expect(document.activeElement).toBe(primaryActions[1]);
});
it('should allow focus to escape when tabbing away', fakeAsync(() => {
dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);
expect(chipListboxNativeElement.tabIndex)
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
flush();
expect(chipListboxNativeElement.tabIndex)
.withContext('Expected tabIndex to be reset back to 0')
.toBe(0);
}));
it('should use user defined tabIndex', fakeAsync(() => {
chipListboxInstance.tabIndex = 4;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipListboxNativeElement.tabIndex)
.withContext('Expected tabIndex to be set to user defined value 4.')
.toBe(4);
dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);
expect(chipListboxNativeElement.tabIndex)
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
flush();
expect(chipListboxNativeElement.tabIndex)
.withContext('Expected tabIndex to be reset back to 4')
.toBe(4);
}));
});
it('should account for the direction changing', () => {
createComponent(StandardChipListbox);
chips.first.focus();
expect(document.activeElement).toBe(primaryActions[0]);
dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[1]);
directionality.value = 'rtl';
directionality.change.next('rtl');
fixture.detectChanges();
dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(document.activeElement).toBe(primaryActions[0]);
});
});
describe('selection logic', () => {
it('should remove selection if chip has been removed', fakeAsync(() => {
fixture = createComponent(BasicChipListbox);
const instanceChips = fixture.componentInstance.chips;
const chipListbox = fixture.componentInstance.chipListbox;
dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
fixture.detectChanges();
expect(instanceChips.first.selected)
.withContext('Expected first option to be selected.')
.toBe(true);
expect(chipListbox.selected)
.withContext('Expected first option to be selected.')
.toBe(chips.first);
fixture.componentInstance.foods = [];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(chipListbox.selected)
.withContext('Expected selection to be removed when option no longer exists.')
.toBe(undefined);
}));
it('should select an option that was added after initialization', () => {
fixture = createComponent(BasicChipListbox);
fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
'.mdc-evolution-chip__action--primary',
);
dispatchKeyboardEvent(primaryActions[8], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.chipListbox.value)
.withContext('Expect value contain the value of the last option')
.toContain('potatoes-8');
expect(fixture.componentInstance.chips.last.selected)
.withContext('Expect last option selected')
.toBeTruthy();
});
it('should not select disabled chips', () => {
fixture = createComponent(BasicChipListbox);
const array = chips.toArray();
dispatchKeyboardEvent(primaryActions[2], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.chipListbox.value)
.withContext('Expect value to be undefined')
.toBeUndefined();
expect(array[2].selected).withContext('Expect disabled chip not selected').toBeFalsy();
expect(fixture.componentInstance.chipListbox.selected)
.withContext('Expect no selected chips')
.toBeUndefined();
});
it('should not select when is not selectable', fakeAsync(() => {
const falsyFixture = createComponent(FalsyBasicChipListbox);
falsyFixture.detectChanges();
tick();
falsyFixture.detectChanges();
const chipListboxElement = falsyFixture.debugElement.query(By.directive(MatChipListbox))!;
const _chips = chipListboxElement.componentInstance._chips;
const nativeChips = (
chipListboxElement.nativeElement as HTMLElement
).querySelectorAll<HTMLElement>('.mdc-evolution-chip__action--primary');
expect(_chips.first.selected)
.withContext('Expected first option not to be selected')
.toBe(false);
dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
falsyFixture.detectChanges();
flush();
expect(_chips.first.selected)
.withContext('Expected first option not to be selected.')
.toBe(false);
}));
it('should set `aria-selected` based on the selection state in single selection mode', fakeAsync(() => {
const getAriaSelected = () =>
Array.from(primaryActions).map(action => action.getAttribute('aria-selected'));
fixture = createComponent(BasicChipListbox);
// Use a shorter list so we can keep the assertions smaller
fixture.componentInstance.foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
fixture.componentInstance.selectable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
'.mdc-evolution-chip__action--primary',
);
expect(getAriaSelected()).toEqual(['false', 'false', 'false']);
primaryActions[1].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['false', 'true', 'false']);
primaryActions[2].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['false', 'false', 'true']);
primaryActions[0].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['true', 'false', 'false']);
}));
it('should set `aria-selected` based on the selection state in multi-selection mode', fakeAsync(() => {
const getAriaSelected = () =>
Array.from(primaryActions).map(action => action.getAttribute('aria-selected'));
fixture = createComponent(MultiSelectionChipListbox);
fixture.detectChanges();
// Use a shorter list so we can keep the assertions smaller
fixture.componentInstance.foods = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];
fixture.componentInstance.selectable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
'.mdc-evolution-chip__action--primary',
);
expect(getAriaSelected()).toEqual(['false', 'false', 'false']);
primaryActions[1].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['false', 'true', 'false']);
primaryActions[2].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['false', 'true', 'true']);
primaryActions[0].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['true', 'true', 'true']);
primaryActions[1].click();
fixture.detectChanges();
flush();
expect(getAriaSelected()).toEqual(['true', 'false', 'true']);
}));
});
describe('chip list with chip input', () => {
describe('single selection', () => {
beforeEach(() => {
fixture = createComponent(BasicChipListbox);
});
it('should take an initial view value with reactive forms', fakeAsync(() => {
fixture.componentInstance.control = new FormControl('pizza-1');
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
const array = chips.toArray();
expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBeTruthy();
dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
flush();
expect(array[1].selected)
.withContext('Expect chip to be not selected after toggle selected')
.toBeFalsy();
}));
it('should set the view value from the form', () => {
const chipListbox = fixture.componentInstance.chipListbox;
const array = chips.toArray();
expect(chipListbox.value)
.withContext('Expect chip listbox to have no initial value')
.toBeFalsy();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy();
});
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);
dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
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', () => {
const array = chips.toArray();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the value to be selected.`)
.toBeTruthy();
fixture.componentInstance.control.setValue('gibberish');
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the old value not to be selected.`)
.toBeFalsy();
});
it('should clear the selection when the control is reset', () => {
const array = chips.toArray();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the old value not to be selected.`)
.toBeFalsy();
});
it('should set the control to touched when the chip listbox is touched', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to start off as untouched.')
.toBe(false);
const nativeChipListbox = fixture.debugElement.query(
By.css('mat-chip-listbox'),
)!.nativeElement;
dispatchFakeEvent(nativeChipListbox, 'blur');
tick();
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to be touched.')
.toBe(true);
}));
it('should not set touched when a disabled chip listbox is touched', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to start off as untouched.')
.toBe(false);
fixture.componentInstance.control.disable();
const nativeChipListbox = fixture.debugElement.query(
By.css('mat-chip-listbox'),
)!.nativeElement;
dispatchFakeEvent(nativeChipListbox, 'blur');
tick();
expect(fixture.componentInstance.control.touched)
.withContext('Expected the control to stay untouched.')
.toBe(false);
}));
it("should set the control to dirty when the chip listbox's value changes in the DOM", () => {
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to start out pristine.`)
.toEqual(false);
dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
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 be able to programmatically select a falsy option', () => {
fixture.destroy();
TestBed.resetTestingModule();
const falsyFixture = createComponent(FalsyValueChipListbox);
falsyFixture.componentInstance.control.setValue([0]);
falsyFixture.detectChanges();
falsyFixture.detectChanges();
expect(falsyFixture.componentInstance.chips.first.selected)
.withContext('Expected first option to be selected')
.toBe(true);
});
it('should not focus the active chip when the value is set programmatically', () => {
const chipArray = fixture.componentInstance.chips.toArray();
spyOn(chipArray[4], 'focus').and.callThrough();
fixture.componentInstance.control.setValue('chips-4');
fixture.detectChanges();
expect(chipArray[4].focus).not.toHaveBeenCalled();
});
});
describe('multiple selection', () => {
it('should take an initial view value with reactive forms', fakeAsync(() => {
fixture = createComponent(MultiSelectionChipListbox, undefined, initFixture => {
initFixture.componentInstance.control = new FormControl(['pizza-1', 'pasta-6']);
initFixture.componentInstance.selectable = true;
});
fixture.detectChanges();
flush();
const array = fixture.componentInstance.chips.toArray();
expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBe(true);
expect(array[6].selected).withContext('Expect pasta-6 chip to be selected').toBe(true);
dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
flush();
expect(array[1].selected)
.withContext('Expect pizza-1 chip to no longer be selected')
.toBe(false);
expect(array[6].selected)
.withContext('Expect pasta-6 chip to remain selected')
.toBe(true);
}));
it('should set the view value from the form', () => {
fixture = createComponent(MultiSelectionChipListbox);
const chipListbox = fixture.componentInstance.chipListbox;
const array = fixture.componentInstance.chips.toArray();
expect(chipListbox.value)
.withContext('Expect chip listbox to have no initial value')
.toBeFalsy();
fixture.componentInstance.control.setValue(['pizza-1']);
fixture.detectChanges();
expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy();
});
it('should update the form value when the view changes', () => {
fixture = createComponent(MultiSelectionChipListbox);
expect(fixture.componentInstance.control.value)
.withContext(`Expected the control's value to be empty initially.`)
.toEqual(null);
dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
fixture.detectChanges();
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', () => {
fixture = createComponent(MultiSelectionChipListbox);
chips = fixture.componentInstance.chips;
const array = fixture.componentInstance.chips.toArray();
fixture.componentInstance.control.setValue(['pizza-1']);
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the value to be selected.`)
.toBeTruthy();
fixture.componentInstance.control.setValue(['gibberish']);
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the old value not to be selected.`)
.toBeFalsy();
});
it('should clear the selection when the control is reset', () => {
fixture = createComponent(MultiSelectionChipListbox);
const array = fixture.componentInstance.chips.toArray();
fixture.componentInstance.control.setValue(['pizza-1']);
fixture.detectChanges();
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(array[1].selected)
.withContext(`Expected chip with the old value not to be selected.`)
.toBeFalsy();
});
});
});
});
function createComponent<T>(
component: Type<T>,
direction: Direction = 'ltr',
beforeInitialChangeDetection?: (fixture: ComponentFixture<T>) => void,
): ComponentFixture<T> {
directionality = {
value: direction,
change: new EventEmitter(),
};
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule, MatChipsModule],
providers: [{provide: Directionality, useValue: directionality}],
declarations: [component],
});
fixture = TestBed.createComponent<T>(component);
beforeInitialChangeDetection?.(fixture);
fixture.detectChanges();
chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!;
chipListboxNativeElement = chipListboxDebugElement.nativeElement;
chipListboxInstance = chipListboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
chips = chipListboxInstance._chips;
primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
'.mdc-evolution-chip__action--primary',
);
return fixture;
}
});
@Component({
template: `
<mat-chip-listbox [tabIndex]="tabIndex" [selectable]="selectable" [role]="role">
@for (i of chips; track i) {
<mat-chip-option (select)="chipSelect(i)" (deselect)="chipDeselect(i)">
{{name}} {{i + 1}}
</mat-chip-option>
}
</mat-chip-listbox>`,
standalone: false,
})
class StandardChipListbox {
name: string = 'Test';
selectable: boolean = true;
chipSelect: (index?: number) => void = () => {};
chipDeselect: (index?: number) => void = () => {};
tabIndex: number = 0;
chips = [0, 1, 2, 3, 4];
role: string | null = null;
}
@Component({
template: `
<mat-chip-listbox [formControl]="control" [required]="isRequired"
[tabIndex]="tabIndexOverride" [selectable]="selectable">
@for (food of foods; track food) {
<mat-chip-option [value]="food.value" [disabled]="food.disabled">
{{ food.viewValue }}
</mat-chip-option>
}
</mat-chip-listbox>
`,
standalone: false,
})
class BasicChipListbox {
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;
tabIndexOverride: number;
selectable: boolean;
@ViewChild(MatChipListbox) chipListbox: MatChipListbox;
@ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}
@Component({
template: `
<mat-chip-listbox [multiple]="true" [formControl]="control"
[required]="isRequired"
[tabIndex]="tabIndexOverride" [selectable]="selectable">
@for (food of foods; track food) {
<mat-chip-option [value]="food.value" [disabled]="food.disabled">
{{ food.viewValue }}
</mat-chip-option>
}
</mat-chip-listbox>
`,
standalone: false,
})
class MultiSelectionChipListbox {
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;
tabIndexOverride: number;
selectable: boolean;
@ViewChild(MatChipListbox) chipListbox: MatChipListbox;
@ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}
@Component({
template: `
<mat-chip-listbox [formControl]="control">
@for (food of foods; track food) {
<mat-chip-option [value]="food.value">{{ food.viewValue }}</mat-chip-option>
}
</mat-chip-listbox>
`,
standalone: false,
})
class FalsyValueChipListbox {
foods: any[] = [
{value: 0, viewValue: 'Steak'},
{value: 1, viewValue: 'Pizza'},
];
control = new FormControl([] as number[]);
@ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}
@Component({
template: `
<mat-chip-listbox>
@for (food of foods; track food) {
<mat-chip-option [value]="food.value" [selected]="food.selected">
{{ food.viewValue }}
</mat-chip-option>
}
</mat-chip-listbox>
`,
standalone: false,
})
class SelectedChipListbox {
foods: any[] = [
{value: 0, viewValue: 'Steak', selected: true},
{value: 1, viewValue: 'Pizza', selected: false},
{value: 2, viewValue: 'Pasta', selected: true},
];
@ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}
@Component({
template: `
<mat-chip-listbox [formControl]="control" [required]="isRequired"
[tabIndex]="tabIndexOverride" [selectable]="selectable">
@for (food of foods; track food) {
<mat-chip-option [value]="food.value" [disabled]="food.disabled">
{{ food.viewValue }}
</mat-chip-option>
}
</mat-chip-listbox>
`,
standalone: false,
})
class FalsyBasicChipListbox {
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;
tabIndexOverride: number;
selectable: boolean = false;
@ViewChild(MatChipListbox) chipListbox: MatChipListbox;
@ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}
// Based on #29783.
@Component({
template: `
<form>
<mat-chip-listbox name="test" [ngModel]="null">
<mat-chip-option value="1" disabled>Hello</mat-chip-option>
</mat-chip-listbox>
</form>
`,
standalone: false,
})
class IndividuallyDisabledChipInsideForm {
@ViewChild(MatChipOption) chip: MatChipOption;
}