Skip to content

fix(selection-list): model not updated when option is selected programmatically #7334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/demo-app/list/list-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ <h2>Selection list</h2>
<mat-selection-list #groceries>
<h3 mat-subheader>Groceries</h3>

<mat-list-option *ngFor="let item of items" [value]="item">
{{item}}
</mat-list-option>
<mat-list-option value="bananas">Bananas</mat-list-option>
<mat-list-option selected value="oranges">Oranges</mat-list-option>
<mat-list-option value="apples">Apples</mat-list-option>
<mat-list-option value="strawberries">Strawberries</mat-list-option>
</mat-selection-list>

<p>Selected: {{groceries.selectedOptions.selected.length}}</p>
Expand Down
70 changes: 42 additions & 28 deletions src/lib/list/selection-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {MatListModule, MatListOption, MatSelectionList} from './index';
describe('MatSelectionList', () => {
describe('with list option', () => {
let fixture: ComponentFixture<SelectionListWithListOptions>;
let listOption: DebugElement[];
let listOptions: DebugElement[];
let listItemEl: DebugElement;
let selectionList: DebugElement;

Expand All @@ -33,14 +33,14 @@ describe('MatSelectionList', () => {
fixture = TestBed.createComponent(SelectionListWithListOptions);
fixture.detectChanges();

listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
}));

it('should add and remove focus class on focus/blur', () => {
// Use the second list item, because the first one is always disabled.
const listItem = listOption[1].nativeElement;
const listItem = listOptions[1].nativeElement;

expect(listItem.classList).not.toContain('mat-list-item-focus');

Expand All @@ -57,35 +57,35 @@ describe('MatSelectionList', () => {
const optionValues = ['inbox', 'starred', 'sent-mail', 'drafts'];

optionValues.forEach((optionValue, index) => {
expect(listOption[index].componentInstance.value).toBe(optionValue);
expect(listOptions[index].componentInstance.value).toBe(optionValue);
});
});

it('should be able to dispatch one selected item', () => {
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
let selectList =
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

expect(selectList.selected.length).toBe(0);
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');

testListItem.toggle();
fixture.detectChanges();

expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(selectList.selected.length).toBe(1);
});

it('should be able to dispatch multiple selected items', () => {
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
let testListItem2 = listOption[1].injector.get<MatListOption>(MatListOption);
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
let testListItem2 = listOptions[1].injector.get<MatListOption>(MatListOption);
let selectList =
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

expect(selectList.selected.length).toBe(0);
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('false');
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('false');

testListItem.toggle();
fixture.detectChanges();
Expand All @@ -94,14 +94,14 @@ describe('MatSelectionList', () => {
fixture.detectChanges();

expect(selectList.selected.length).toBe(2);
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOption[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('true');
expect(listOptions[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
});

it('should be able to deselect an option', () => {
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
let selectList =
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

Expand All @@ -119,12 +119,12 @@ describe('MatSelectionList', () => {
});

it('should not allow selection of disabled items', () => {
let testListItem = listOption[0].injector.get<MatListOption>(MatListOption);
let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);
let selectList =
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

expect(selectList.selected.length).toBe(0);
expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');

testListItem._handleClick();
fixture.detectChanges();
Expand All @@ -133,18 +133,18 @@ describe('MatSelectionList', () => {
});

it('should be able to un-disable disabled items', () => {
let testListItem = listOption[0].injector.get<MatListOption>(MatListOption);
let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);

expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');

testListItem.disabled = false;
fixture.detectChanges();

expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
});

it('should be able to use keyboard select with SPACE', () => {
let testListItem = listOption[1].nativeElement as HTMLElement;
let testListItem = listOptions[1].nativeElement as HTMLElement;
let SPACE_EVENT: KeyboardEvent =
createKeyboardEvent('keydown', SPACE, testListItem);
let selectList =
Expand All @@ -162,7 +162,7 @@ describe('MatSelectionList', () => {
it('should restore focus if active option is destroyed', () => {
const manager = selectionList.componentInstance._keyManager;

listOption[3].componentInstance._handleFocus();
listOptions[3].componentInstance._handleFocus();

expect(manager.activeItemIndex).toBe(3);

Expand All @@ -173,12 +173,12 @@ describe('MatSelectionList', () => {
});

it('should focus previous item when press UP ARROW', () => {
let testListItem = listOption[2].nativeElement as HTMLElement;
let testListItem = listOptions[2].nativeElement as HTMLElement;
let UP_EVENT: KeyboardEvent =
createKeyboardEvent('keydown', UP_ARROW, testListItem);
let manager = selectionList.componentInstance._keyManager;

dispatchFakeEvent(listOption[2].nativeElement, 'focus');
dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
expect(manager.activeItemIndex).toEqual(2);

selectionList.componentInstance._keydown(UP_EVENT);
Expand All @@ -189,12 +189,12 @@ describe('MatSelectionList', () => {
});

it('should focus next item when press DOWN ARROW', () => {
let testListItem = listOption[2].nativeElement as HTMLElement;
let testListItem = listOptions[2].nativeElement as HTMLElement;
let DOWN_EVENT: KeyboardEvent =
createKeyboardEvent('keydown', DOWN_ARROW, testListItem);
let manager = selectionList.componentInstance._keyManager;

dispatchFakeEvent(listOption[2].nativeElement, 'focus');
dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
expect(manager.activeItemIndex).toEqual(2);

selectionList.componentInstance._keydown(DOWN_EVENT);
Expand Down Expand Up @@ -226,6 +226,20 @@ describe('MatSelectionList', () => {

expect(list.options.toArray().every(option => option.selected)).toBe(false);
});

it('should update the list value when an item is selected programmatically', () => {
const list: MatSelectionList = selectionList.componentInstance;

expect(list.selectedOptions.isEmpty()).toBe(true);

listOptions[0].componentInstance.selected = true;
listOptions[2].componentInstance.selected = true;
fixture.detectChanges();

expect(list.selectedOptions.isEmpty()).toBe(false);
expect(list.selectedOptions.isSelected(listOptions[0].componentInstance)).toBe(true);
expect(list.selectedOptions.isSelected(listOptions[2].componentInstance)).toBe(true);
});
});

describe('with list option selected', () => {
Expand Down
14 changes: 11 additions & 3 deletions src/lib/list/selection-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,17 @@ export class MatListOption extends _MatListOptionMixinBase
/** Whether the option is selected. */
@Input()
get selected() { return this._selected; }
set selected(value: boolean) { this._selected = coerceBooleanProperty(value); }
set selected(value: boolean) {
const isSelected = coerceBooleanProperty(value);

if (isSelected !== this._selected) {
const selectionModel = this.selectionList.selectedOptions;

this._selected = isSelected;
isSelected ? selectionModel.select(this) : selectionModel.deselect(this);
this._changeDetector.markForCheck();
}
}

/** Emitted when the option is selected. */
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();
Expand Down Expand Up @@ -140,8 +150,6 @@ export class MatListOption extends _MatListOptionMixinBase
/** Toggles the selection state of the option. */
toggle(): void {
this.selected = !this.selected;
this.selectionList.selectedOptions.toggle(this);
this._changeDetector.markForCheck();
}

/** Allows for programmatic focusing of the option. */
Expand Down