Skip to content

Cdk listbox control accessor #20071

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 26 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c11c3a
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
7525a2c
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
26af9c9
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
67f3aad
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
61513bb
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
29b8513
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
12f1e80
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
bc8e583
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
fe81e8c
feat(listbox): added support for non-multiple listbox and aria active…
nielsr98 Jul 9, 2020
4414737
fix(listbox): formatted BUILD.bazel.
nielsr98 Jul 9, 2020
3bdfa83
feat(dev-app/listbox): added cdk listbox example to the dev-app.
nielsr98 Jul 15, 2020
cbf7c2d
feat(listbox): implemented ControlValueAccessor.
nielsr98 Jul 15, 2020
75c0dfa
nit(listbox): removed unused error class.
nielsr98 Jul 22, 2020
1d88375
fix(listbox): removed duplicate dep in dev-app build file.
nielsr98 Jul 22, 2020
047077a
fix(listbox): changed QueryList to array before iterating and fixed l…
nielsr98 Jul 22, 2020
5802c7d
fix(listbox): coreced array from values to ensure for loop does not i…
nielsr98 Jul 23, 2020
732d7c6
refactor(listbox): added a type T to CdkOption.
nielsr98 Jul 23, 2020
516354a
refactor(listbox): added tests for writeValue and setSelectedByValue.
nielsr98 Jul 24, 2020
eb2405d
fix(listbox): changed the coerceArray import path.
nielsr98 Jul 24, 2020
3b07bcc
nit(listbox): removed unused variables.
nielsr98 Jul 24, 2020
bf04326
fix(listbox): removed reference to undeclared variable.
nielsr98 Jul 24, 2020
4c4a24c
refactor(listbox): made listbox and option generic typed and added un…
nielsr98 Jul 28, 2020
c0aaac6
fix(listbox): removed unneccessary import and change detection refere…
nielsr98 Jul 28, 2020
4ccfbd6
nit(listbox): fixed formatting of BUILD file.
nielsr98 Jul 28, 2020
d00df03
fix(listbox): fixed lint errors.
nielsr98 Jul 28, 2020
d567cd0
fix(listbox): changed types of any to the generic type T.
nielsr98 Jul 29, 2020
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
2 changes: 2 additions & 0 deletions src/cdk-experimental/listbox/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ng_module(
"//src/cdk/a11y",
"//src/cdk/collections",
"//src/cdk/keycodes",
"@npm//@angular/forms",
],
)

Expand All @@ -26,6 +27,7 @@ ng_test_library(
":listbox",
"//src/cdk/keycodes",
"//src/cdk/testing/private",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
],
)
Expand Down
229 changes: 218 additions & 11 deletions src/cdk-experimental/listbox/listbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
async,
TestBed, tick, fakeAsync,
} from '@angular/core/testing';
import {Component, DebugElement} from '@angular/core';
import {Component, DebugElement, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {
CdkOption,
Expand All @@ -15,16 +15,17 @@ import {
dispatchMouseEvent
} from '@angular/cdk/testing/private';
import {A, DOWN_ARROW, END, HOME, SPACE} from '@angular/cdk/keycodes';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';

describe('CdkOption', () => {
describe('CdkOption and CdkListbox', () => {

describe('selection state change', () => {
let fixture: ComponentFixture<ListboxWithOptions>;

let testComponent: ListboxWithOptions;

let listbox: DebugElement;
let listboxInstance: CdkListbox;
let listboxInstance: CdkListbox<unknown>;
let listboxElement: HTMLElement;

let options: DebugElement[];
Expand All @@ -45,7 +46,7 @@ describe('CdkOption', () => {
testComponent = fixture.debugElement.componentInstance;

listbox = fixture.debugElement.query(By.directive(CdkListbox));
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);
listboxElement = listbox.nativeElement;

options = fixture.debugElement.queryAll(By.directive(CdkOption));
Expand Down Expand Up @@ -360,7 +361,7 @@ describe('CdkOption', () => {

let testComponent: ListboxMultiselect;
let listbox: DebugElement;
let listboxInstance: CdkListbox;
let listboxInstance: CdkListbox<unknown>;

let options: DebugElement[];
let optionInstances: CdkOption[];
Expand All @@ -379,7 +380,7 @@ describe('CdkOption', () => {

testComponent = fixture.debugElement.componentInstance;
listbox = fixture.debugElement.query(By.directive(CdkListbox));
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);

options = fixture.debugElement.queryAll(By.directive(CdkOption));
optionInstances = options.map(o => o.injector.get<CdkOption>(CdkOption));
Expand Down Expand Up @@ -502,7 +503,7 @@ describe('CdkOption', () => {
let testComponent: ListboxActiveDescendant;

let listbox: DebugElement;
let listboxInstance: CdkListbox;
let listboxInstance: CdkListbox<unknown>;
let listboxElement: HTMLElement;

let options: DebugElement[];
Expand All @@ -523,7 +524,7 @@ describe('CdkOption', () => {
testComponent = fixture.debugElement.componentInstance;

listbox = fixture.debugElement.query(By.directive(CdkListbox));
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);
listboxElement = listbox.nativeElement;

options = fixture.debugElement.queryAll(By.directive(CdkOption));
Expand Down Expand Up @@ -582,6 +583,185 @@ describe('CdkOption', () => {

});
});

describe('with control value accessor implemented', () => {
let fixture: ComponentFixture<ListboxControlValueAccessor>;
let testComponent: ListboxControlValueAccessor;

let listbox: DebugElement;
let listboxInstance: CdkListbox<string>;

let options: DebugElement[];
let optionInstances: CdkOption[];
let optionElements: HTMLElement[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkListboxModule, FormsModule, ReactiveFormsModule],
declarations: [ListboxControlValueAccessor],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(ListboxControlValueAccessor);
fixture.detectChanges();

testComponent = fixture.debugElement.componentInstance;

listbox = fixture.debugElement.query(By.directive(CdkListbox));
listboxInstance = listbox.injector.get<CdkListbox<string>>(CdkListbox);

options = fixture.debugElement.queryAll(By.directive(CdkOption));
optionInstances = options.map(o => o.injector.get<CdkOption>(CdkOption));
optionElements = options.map(o => o.nativeElement);
});

it('should be able to set the disabled state via setDisabledState', () => {
expect(listboxInstance.disabled)
.toBe(false, 'Expected the selection list to be enabled.');
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');

listboxInstance.setDisabledState(true);
fixture.detectChanges();

expect(listboxInstance.disabled)
.toBe(true, 'Expected the selection list to be disabled.');
for (const option of optionElements) {
expect(option.getAttribute('aria-disabled')).toBe('true');
}
});

it('should be able to select options via writeValue', () => {
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');

listboxInstance.writeValue('arc');
fixture.detectChanges();

expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
expect(optionElements[1].hasAttribute('aria-selected')).toBeFalse();
expect(optionElements[3].hasAttribute('aria-selected')).toBeFalse();

expect(optionInstances[2].selected).toBeTrue();
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
});

it('should be select multiple options by their values', () => {
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');

testComponent.isMultiselectable = true;
fixture.detectChanges();

listboxInstance.writeValue(['arc', 'stasis']);
fixture.detectChanges();

expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
expect(optionElements[1].hasAttribute('aria-selected')).toBeFalse();

expect(optionInstances[2].selected).toBeTrue();
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
expect(optionInstances[3].selected).toBeTrue();
expect(optionElements[3].getAttribute('aria-selected')).toBe('true');
});

it('should be able to disable options from the control', () => {
expect(testComponent.listbox.disabled).toBeFalse();
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');

testComponent.form.disable();
fixture.detectChanges();

expect(testComponent.listbox.disabled).toBeTrue();
for (const option of optionElements) {
expect(option.getAttribute('aria-disabled')).toBe('true');
}
});

it('should be able to toggle disabled state after form control is disabled', () => {
expect(testComponent.listbox.disabled).toBeFalse();
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');

testComponent.form.disable();
fixture.detectChanges();

expect(testComponent.listbox.disabled).toBeTrue();
for (const option of optionElements) {
expect(option.getAttribute('aria-disabled')).toBe('true');
}

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

expect(testComponent.listbox.disabled).toBeFalse();
expect(optionInstances.every(option => !option.disabled))
.toBe(true, 'Expected every list option to be enabled.');
});

it('should be able to select options via setting the value in form control', () => {
expect(optionInstances.every(option => option.selected)).toBeFalse();

testComponent.isMultiselectable = true;
fixture.detectChanges();

testComponent.form.setValue(['purple', 'arc']);
fixture.detectChanges();

expect(optionElements[0].getAttribute('aria-selected')).toBe('true');
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
expect(optionInstances[0].selected).toBeTrue();
expect(optionInstances[2].selected).toBeTrue();

testComponent.form.setValue(null);
fixture.detectChanges();

expect(optionInstances.every(option => option.selected)).toBeFalse();
});

it('should only select the first matching option if multiple is not enabled', () => {
expect(optionInstances.every(option => option.selected)).toBeFalse();

testComponent.form.setValue(['solar', 'arc']);
fixture.detectChanges();

expect(optionElements[1].getAttribute('aria-selected')).toBe('true');
expect(optionElements[2].hasAttribute('aria-selected')).toBeFalse();
expect(optionInstances[1].selected).toBeTrue();
expect(optionInstances[2].selected).toBeFalse();
});

it('should deselect an option selected via form control once its value changes', () => {
const option = optionInstances[1];
const element = optionElements[1];

testComponent.form.setValue(['solar']);
fixture.detectChanges();

expect(element.getAttribute('aria-selected')).toBe('true');
expect(option.selected).toBeTrue();

option.value = 'new-value';
fixture.detectChanges();

expect(element.hasAttribute('aria-selected')).toBeFalse();
expect(option.selected).toBeFalse();
});

it('should maintain the form control on listbox destruction', function () {
testComponent.form.setValue(['solar']);
fixture.detectChanges();

expect(testComponent.form.value).toEqual(['solar']);

testComponent.showListbox = false;
fixture.detectChanges();

expect(testComponent.form.value).toEqual(['solar']);
});
});
});

@Component({
Expand All @@ -607,7 +787,7 @@ class ListboxWithOptions {
isPurpleDisabled: boolean = false;
isSolarDisabled: boolean = false;

onSelectionChange(event: ListboxSelectionChangeEvent) {
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
this.changedOption = event.option;
}
}
Expand All @@ -627,7 +807,7 @@ class ListboxMultiselect {
changedOption: CdkOption;
isMultiselectable: boolean = false;

onSelectionChange(event: ListboxSelectionChangeEvent) {
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
this.changedOption = event.option;
}
}
Expand All @@ -647,11 +827,38 @@ class ListboxActiveDescendant {
isActiveDescendant: boolean = true;
focusedOption: string;

onSelectionChange(event: ListboxSelectionChangeEvent) {
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
this.changedOption = event.option;
}

onFocus(option: string) {
this.focusedOption = option;
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add test cases for actually using the listbox with a FormControl, and then testing that the interaction works as expected via that FormControl

@Component({
template: `
<select cdkListbox
[disabled]="isDisabled"
[multiple]="isMultiselectable"
(selectionChange)="onSelectionChange($event)"
[formControl]="form"
*ngIf="showListbox" ngDefaultControl>
<option cdkOption [value]="'purple'">Purple</option>
<option cdkOption [value]="'solar'">Solar</option>
<option cdkOption [value]="'arc'">Arc</option>
<option cdkOption [value]="'stasis'">Stasis</option>
</select>`
})
class ListboxControlValueAccessor {
form = new FormControl();
changedOption: CdkOption<string>;
isDisabled: boolean = false;
isMultiselectable: boolean = false;
showListbox: boolean = true;
@ViewChild(CdkListbox) listbox: CdkListbox<string>;

onSelectionChange(event: ListboxSelectionChangeEvent<string>) {
this.changedOption = event.option;
}
}
Loading