fix(selection-list): fix option value coercion and selection events (#6901)

* fix(selection-list): fix md-list-option value coercion and selection event emitters

* md-list-option value is no longer coerced to a boolean value
* md-list-option EventEmitters selectedChange and deselected now emit an event when an option is selected/deselected

Fixes #6864

* review feedback changes

* review feedback changes

* review feedback changes

* review feedback changes
This commit is contained in:
Derek Burgman
2017-10-12 16:14:01 -05:00
committed by Andrew Seguin
parent 074f6cea62
commit 80671bf2e8
2 changed files with 148 additions and 35 deletions
+118 -14
View File
@@ -4,7 +4,7 @@ import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
import {Component, DebugElement} from '@angular/core';
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {MatListModule, MatListOption, MatSelectionList} from './index';
import {MatListModule, MatListOption, MatSelectionList, MatListOptionChange} from './index';
describe('MatSelectionList', () => {
@@ -483,9 +483,88 @@ describe('MatSelectionList', () => {
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
});
});
});
describe('with multiple values', () => {
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
let listOption: DebugElement[];
let listItemEl: DebugElement;
let selectionList: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithMultipleValues
],
});
TestBed.compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(SelectionListWithMultipleValues);
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
fixture.detectChanges();
}));
it('should have a value for each item', () => {
expect(listOption[0].componentInstance.value).toBe(1);
expect(listOption[1].componentInstance.value).toBe('a');
expect(listOption[2].componentInstance.value).toBe(true);
});
});
describe('with option selected events', () => {
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
let testComponent: SelectionListWithOptionEvents;
let listOption: DebugElement[];
let selectionList: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithOptionEvents
],
});
TestBed.compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
testComponent = fixture.debugElement.componentInstance;
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
fixture.detectChanges();
}));
it('should trigger the selected and deselected events when clicked in succession.', () => {
let selected: boolean = false;
spyOn(testComponent, 'onOptionSelectionChange')
.and.callFake((event: MatListOptionChange) => {
selected = event.selected;
});
listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
expect(selected).toBe(true);
listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
expect(selected).toBe(false);
});
});
});
@Component({template: `
<mat-selection-list id="selection-list-1">
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
@@ -507,17 +586,17 @@ class SelectionListWithListOptions {
}
@Component({template: `
<mat-selection-list id = "selection-list-2">
<mat-list-option checkboxPosition = "after">
<mat-selection-list id="selection-list-2">
<mat-list-option checkboxPosition="after">
Inbox (disabled selection-option)
</mat-list-option>
<mat-list-option id = "testSelect" checkboxPosition = "after">
<mat-list-option id="testSelect" checkboxPosition="after">
Starred
</mat-list-option>
<mat-list-option checkboxPosition = "after">
<mat-list-option checkboxPosition="after">
Sent Mail
</mat-list-option>
<mat-list-option checkboxPosition = "after">
<mat-list-option checkboxPosition="after">
Drafts
</mat-list-option>
</mat-selection-list>`})
@@ -525,17 +604,17 @@ class SelectionListWithCheckboxPositionAfter {
}
@Component({template: `
<mat-selection-list id = "selection-list-3" [disabled] = true>
<mat-list-option checkboxPosition = "after">
<mat-selection-list id="selection-list-3" [disabled]=true>
<mat-list-option checkboxPosition="after">
Inbox (disabled selection-option)
</mat-list-option>
<mat-list-option id = "testSelect" checkboxPosition = "after">
<mat-list-option id="testSelect" checkboxPosition="after">
Starred
</mat-list-option>
<mat-list-option checkboxPosition = "after">
<mat-list-option checkboxPosition="after">
Sent Mail
</mat-list-option>
<mat-list-option checkboxPosition = "after">
<mat-list-option checkboxPosition="after">
Drafts
</mat-list-option>
</mat-selection-list>`})
@@ -559,8 +638,8 @@ class SelectionListWithSelectedOption {
}
@Component({template: `
<mat-selection-list id = "selection-list-4">
<mat-list-option checkboxPosition = "after" class="test-focus" id="123">
<mat-selection-list id="selection-list-4">
<mat-list-option checkboxPosition="after" class="test-focus" id="123">
Inbox
</mat-list-option>
</mat-selection-list>`})
@@ -579,3 +658,28 @@ class SelectionListWithTabindexBinding {
tabIndex: number;
disabled: boolean;
}
@Component({template: `
<mat-selection-list id="selection-list-5">
<mat-list-option [value]="1" checkboxPosition="after">
1
</mat-list-option>
<mat-list-option value="a" checkboxPosition="after">
a
</mat-list-option>
<mat-list-option [value]="true" checkboxPosition="after">
true
</mat-list-option>
</mat-selection-list>`})
class SelectionListWithMultipleValues {
}
@Component({template: `
<mat-selection-list id="selection-list-6">
<mat-list-option (selectionChange)="onOptionSelectionChange($event)">
Inbox
</mat-list-option>
</mat-selection-list>`})
class SelectionListWithOptionEvents {
onOptionSelectionChange: (event?: MatListOptionChange) => void = () => {};
}
+30 -21
View File
@@ -51,15 +51,18 @@ export const _MatSelectionListMixinBase =
export class MatListOptionBase {}
export const _MatListOptionMixinBase = mixinDisableRipple(MatListOptionBase);
/** Event emitted by a selection-list whenever the state of an option is changed. */
export interface MatSelectionListOptionEvent {
option: MatListOption;
/** Change event object emitted by MatListOption */
export class MatListOptionChange {
/** The source MatListOption of the event. */
source: MatListOption;
/** The new `selected` value of the option. */
selected: boolean;
}
/**
* Component for list-options of selection-list. Each list-option can automatically
* generate a checkbox and can put current item into the selectionModel of selection-list
* if the current item is checked.
* if the current item is selected.
*/
@Component({
moduleId: module.id,
@@ -86,7 +89,6 @@ export interface MatSelectionListOptionEvent {
export class MatListOption extends _MatListOptionMixinBase
implements AfterContentInit, OnInit, OnDestroy, FocusableOption, CanDisableRipple {
private _lineSetter: MatLineSetter;
private _selected: boolean = false;
private _disabled: boolean = false;
/** Whether the option has focus. */
@@ -97,34 +99,31 @@ export class MatListOption extends _MatListOptionMixinBase
/** Whether the label should appear before or after the checkbox. Defaults to 'after' */
@Input() checkboxPosition: 'before' | 'after' = 'after';
/** Whether the option is disabled. */
@Input()
get disabled(): boolean {
return (this.selectionList && this.selectionList.disabled) || this._disabled;
}
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); }
/** Value of the option */
@Input() value: any;
/** Whether the option is disabled. */
@Input()
get disabled() { return (this.selectionList && this.selectionList.disabled) || this._disabled; }
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
/** Whether the option is selected. */
@Input()
get selected() { return this._selected; }
get selected(): boolean { return this.selectionList.selectedOptions.isSelected(this); }
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);
if (isSelected !== this.selected) {
this.selectionList.selectedOptions.toggle(this);
this._changeDetector.markForCheck();
this.selectionChange.emit(this._createChangeEvent());
}
}
/** Emitted when the option is selected. */
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();
/** Emitted when the option is deselected. */
@Output() deselected = new EventEmitter<MatSelectionListOptionEvent>();
/** Emitted when the option is selected or deselected. */
@Output() selectionChange = new EventEmitter<MatListOptionChange>();
constructor(private _renderer: Renderer2,
private _element: ElementRef,
@@ -174,6 +173,16 @@ export class MatListOption extends _MatListOptionMixinBase
this.selectionList._setFocusedOption(this);
}
/** Creates a selection event object from the specified option. */
private _createChangeEvent(option: MatListOption = this): MatListOptionChange {
const event = new MatListOptionChange();
event.source = option;
event.selected = option.selected;
return event;
}
/** Retrieves the DOM element of the component host. */
_getHostElement(): HTMLElement {
return this._element.nativeElement;