Skip to content
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

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

Merged
merged 5 commits into from
Oct 12, 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
132 changes: 118 additions & 14 deletions src/lib/list/selection-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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">
Expand All @@ -507,35 +586,35 @@ 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>`})
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>`})
Expand All @@ -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>`})
Expand All @@ -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 = () => {};
}
51 changes: 30 additions & 21 deletions src/lib/list/selection-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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. */
Expand All @@ -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';

/** 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); }
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 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,
Expand Down Expand Up @@ -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;
Expand Down