Skip to content

Commit 5f5f5aa

Browse files
authored
Cdk listbox control accessor (#20071)
* build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * feat(listbox): added support for non-multiple listbox and aria activedescendant. * fix(listbox): formatted BUILD.bazel. * feat(dev-app/listbox): added cdk listbox example to the dev-app. * feat(listbox): implemented ControlValueAccessor. * nit(listbox): removed unused error class. * fix(listbox): removed duplicate dep in dev-app build file. * fix(listbox): changed QueryList to array before iterating and fixed lint errors. * fix(listbox): coreced array from values to ensure for loop does not iterate through string characters. * refactor(listbox): added a type T to CdkOption. * refactor(listbox): added tests for writeValue and setSelectedByValue. * fix(listbox): changed the coerceArray import path. * nit(listbox): removed unused variables. * fix(listbox): removed reference to undeclared variable. * refactor(listbox): made listbox and option generic typed and added unit tests for using form control. * fix(listbox): removed unneccessary import and change detection reference object. * nit(listbox): fixed formatting of BUILD file. * fix(listbox): fixed lint errors. * fix(listbox): changed types of any to the generic type T.
1 parent 28f8edf commit 5f5f5aa

File tree

6 files changed

+350
-56
lines changed

6 files changed

+350
-56
lines changed

src/cdk-experimental/listbox/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ng_module(
1313
"//src/cdk/a11y",
1414
"//src/cdk/collections",
1515
"//src/cdk/keycodes",
16+
"@npm//@angular/forms",
1617
],
1718
)
1819

@@ -26,6 +27,7 @@ ng_test_library(
2627
":listbox",
2728
"//src/cdk/keycodes",
2829
"//src/cdk/testing/private",
30+
"@npm//@angular/forms",
2931
"@npm//@angular/platform-browser",
3032
],
3133
)

src/cdk-experimental/listbox/listbox.spec.ts

Lines changed: 218 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
async,
44
TestBed, tick, fakeAsync,
55
} from '@angular/core/testing';
6-
import {Component, DebugElement} from '@angular/core';
6+
import {Component, DebugElement, ViewChild} from '@angular/core';
77
import {By} from '@angular/platform-browser';
88
import {
99
CdkOption,
@@ -15,16 +15,17 @@ import {
1515
dispatchMouseEvent
1616
} from '@angular/cdk/testing/private';
1717
import {A, DOWN_ARROW, END, HOME, SPACE} from '@angular/cdk/keycodes';
18+
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
1819

19-
describe('CdkOption', () => {
20+
describe('CdkOption and CdkListbox', () => {
2021

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

2425
let testComponent: ListboxWithOptions;
2526

2627
let listbox: DebugElement;
27-
let listboxInstance: CdkListbox;
28+
let listboxInstance: CdkListbox<unknown>;
2829
let listboxElement: HTMLElement;
2930

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

4748
listbox = fixture.debugElement.query(By.directive(CdkListbox));
48-
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
49+
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);
4950
listboxElement = listbox.nativeElement;
5051

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

361362
let testComponent: ListboxMultiselect;
362363
let listbox: DebugElement;
363-
let listboxInstance: CdkListbox;
364+
let listboxInstance: CdkListbox<unknown>;
364365

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

380381
testComponent = fixture.debugElement.componentInstance;
381382
listbox = fixture.debugElement.query(By.directive(CdkListbox));
382-
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
383+
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);
383384

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

504505
let listbox: DebugElement;
505-
let listboxInstance: CdkListbox;
506+
let listboxInstance: CdkListbox<unknown>;
506507
let listboxElement: HTMLElement;
507508

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

525526
listbox = fixture.debugElement.query(By.directive(CdkListbox));
526-
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
527+
listboxInstance = listbox.injector.get<CdkListbox<unknown>>(CdkListbox);
527528
listboxElement = listbox.nativeElement;
528529

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

583584
});
584585
});
586+
587+
describe('with control value accessor implemented', () => {
588+
let fixture: ComponentFixture<ListboxControlValueAccessor>;
589+
let testComponent: ListboxControlValueAccessor;
590+
591+
let listbox: DebugElement;
592+
let listboxInstance: CdkListbox<string>;
593+
594+
let options: DebugElement[];
595+
let optionInstances: CdkOption[];
596+
let optionElements: HTMLElement[];
597+
598+
beforeEach(async(() => {
599+
TestBed.configureTestingModule({
600+
imports: [CdkListboxModule, FormsModule, ReactiveFormsModule],
601+
declarations: [ListboxControlValueAccessor],
602+
}).compileComponents();
603+
}));
604+
605+
beforeEach(() => {
606+
fixture = TestBed.createComponent(ListboxControlValueAccessor);
607+
fixture.detectChanges();
608+
609+
testComponent = fixture.debugElement.componentInstance;
610+
611+
listbox = fixture.debugElement.query(By.directive(CdkListbox));
612+
listboxInstance = listbox.injector.get<CdkListbox<string>>(CdkListbox);
613+
614+
options = fixture.debugElement.queryAll(By.directive(CdkOption));
615+
optionInstances = options.map(o => o.injector.get<CdkOption>(CdkOption));
616+
optionElements = options.map(o => o.nativeElement);
617+
});
618+
619+
it('should be able to set the disabled state via setDisabledState', () => {
620+
expect(listboxInstance.disabled)
621+
.toBe(false, 'Expected the selection list to be enabled.');
622+
expect(optionInstances.every(option => !option.disabled))
623+
.toBe(true, 'Expected every list option to be enabled.');
624+
625+
listboxInstance.setDisabledState(true);
626+
fixture.detectChanges();
627+
628+
expect(listboxInstance.disabled)
629+
.toBe(true, 'Expected the selection list to be disabled.');
630+
for (const option of optionElements) {
631+
expect(option.getAttribute('aria-disabled')).toBe('true');
632+
}
633+
});
634+
635+
it('should be able to select options via writeValue', () => {
636+
expect(optionInstances.every(option => !option.disabled))
637+
.toBe(true, 'Expected every list option to be enabled.');
638+
639+
listboxInstance.writeValue('arc');
640+
fixture.detectChanges();
641+
642+
expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
643+
expect(optionElements[1].hasAttribute('aria-selected')).toBeFalse();
644+
expect(optionElements[3].hasAttribute('aria-selected')).toBeFalse();
645+
646+
expect(optionInstances[2].selected).toBeTrue();
647+
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
648+
});
649+
650+
it('should be select multiple options by their values', () => {
651+
expect(optionInstances.every(option => !option.disabled))
652+
.toBe(true, 'Expected every list option to be enabled.');
653+
654+
testComponent.isMultiselectable = true;
655+
fixture.detectChanges();
656+
657+
listboxInstance.writeValue(['arc', 'stasis']);
658+
fixture.detectChanges();
659+
660+
expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
661+
expect(optionElements[1].hasAttribute('aria-selected')).toBeFalse();
662+
663+
expect(optionInstances[2].selected).toBeTrue();
664+
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
665+
expect(optionInstances[3].selected).toBeTrue();
666+
expect(optionElements[3].getAttribute('aria-selected')).toBe('true');
667+
});
668+
669+
it('should be able to disable options from the control', () => {
670+
expect(testComponent.listbox.disabled).toBeFalse();
671+
expect(optionInstances.every(option => !option.disabled))
672+
.toBe(true, 'Expected every list option to be enabled.');
673+
674+
testComponent.form.disable();
675+
fixture.detectChanges();
676+
677+
expect(testComponent.listbox.disabled).toBeTrue();
678+
for (const option of optionElements) {
679+
expect(option.getAttribute('aria-disabled')).toBe('true');
680+
}
681+
});
682+
683+
it('should be able to toggle disabled state after form control is disabled', () => {
684+
expect(testComponent.listbox.disabled).toBeFalse();
685+
expect(optionInstances.every(option => !option.disabled))
686+
.toBe(true, 'Expected every list option to be enabled.');
687+
688+
testComponent.form.disable();
689+
fixture.detectChanges();
690+
691+
expect(testComponent.listbox.disabled).toBeTrue();
692+
for (const option of optionElements) {
693+
expect(option.getAttribute('aria-disabled')).toBe('true');
694+
}
695+
696+
listboxInstance.disabled = false;
697+
fixture.detectChanges();
698+
699+
expect(testComponent.listbox.disabled).toBeFalse();
700+
expect(optionInstances.every(option => !option.disabled))
701+
.toBe(true, 'Expected every list option to be enabled.');
702+
});
703+
704+
it('should be able to select options via setting the value in form control', () => {
705+
expect(optionInstances.every(option => option.selected)).toBeFalse();
706+
707+
testComponent.isMultiselectable = true;
708+
fixture.detectChanges();
709+
710+
testComponent.form.setValue(['purple', 'arc']);
711+
fixture.detectChanges();
712+
713+
expect(optionElements[0].getAttribute('aria-selected')).toBe('true');
714+
expect(optionElements[2].getAttribute('aria-selected')).toBe('true');
715+
expect(optionInstances[0].selected).toBeTrue();
716+
expect(optionInstances[2].selected).toBeTrue();
717+
718+
testComponent.form.setValue(null);
719+
fixture.detectChanges();
720+
721+
expect(optionInstances.every(option => option.selected)).toBeFalse();
722+
});
723+
724+
it('should only select the first matching option if multiple is not enabled', () => {
725+
expect(optionInstances.every(option => option.selected)).toBeFalse();
726+
727+
testComponent.form.setValue(['solar', 'arc']);
728+
fixture.detectChanges();
729+
730+
expect(optionElements[1].getAttribute('aria-selected')).toBe('true');
731+
expect(optionElements[2].hasAttribute('aria-selected')).toBeFalse();
732+
expect(optionInstances[1].selected).toBeTrue();
733+
expect(optionInstances[2].selected).toBeFalse();
734+
});
735+
736+
it('should deselect an option selected via form control once its value changes', () => {
737+
const option = optionInstances[1];
738+
const element = optionElements[1];
739+
740+
testComponent.form.setValue(['solar']);
741+
fixture.detectChanges();
742+
743+
expect(element.getAttribute('aria-selected')).toBe('true');
744+
expect(option.selected).toBeTrue();
745+
746+
option.value = 'new-value';
747+
fixture.detectChanges();
748+
749+
expect(element.hasAttribute('aria-selected')).toBeFalse();
750+
expect(option.selected).toBeFalse();
751+
});
752+
753+
it('should maintain the form control on listbox destruction', function () {
754+
testComponent.form.setValue(['solar']);
755+
fixture.detectChanges();
756+
757+
expect(testComponent.form.value).toEqual(['solar']);
758+
759+
testComponent.showListbox = false;
760+
fixture.detectChanges();
761+
762+
expect(testComponent.form.value).toEqual(['solar']);
763+
});
764+
});
585765
});
586766

587767
@Component({
@@ -607,7 +787,7 @@ class ListboxWithOptions {
607787
isPurpleDisabled: boolean = false;
608788
isSolarDisabled: boolean = false;
609789

610-
onSelectionChange(event: ListboxSelectionChangeEvent) {
790+
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
611791
this.changedOption = event.option;
612792
}
613793
}
@@ -627,7 +807,7 @@ class ListboxMultiselect {
627807
changedOption: CdkOption;
628808
isMultiselectable: boolean = false;
629809

630-
onSelectionChange(event: ListboxSelectionChangeEvent) {
810+
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
631811
this.changedOption = event.option;
632812
}
633813
}
@@ -647,11 +827,38 @@ class ListboxActiveDescendant {
647827
isActiveDescendant: boolean = true;
648828
focusedOption: string;
649829

650-
onSelectionChange(event: ListboxSelectionChangeEvent) {
830+
onSelectionChange(event: ListboxSelectionChangeEvent<unknown>) {
651831
this.changedOption = event.option;
652832
}
653833

654834
onFocus(option: string) {
655835
this.focusedOption = option;
656836
}
657837
}
838+
839+
@Component({
840+
template: `
841+
<select cdkListbox
842+
[disabled]="isDisabled"
843+
[multiple]="isMultiselectable"
844+
(selectionChange)="onSelectionChange($event)"
845+
[formControl]="form"
846+
*ngIf="showListbox" ngDefaultControl>
847+
<option cdkOption [value]="'purple'">Purple</option>
848+
<option cdkOption [value]="'solar'">Solar</option>
849+
<option cdkOption [value]="'arc'">Arc</option>
850+
<option cdkOption [value]="'stasis'">Stasis</option>
851+
</select>`
852+
})
853+
class ListboxControlValueAccessor {
854+
form = new FormControl();
855+
changedOption: CdkOption<string>;
856+
isDisabled: boolean = false;
857+
isMultiselectable: boolean = false;
858+
showListbox: boolean = true;
859+
@ViewChild(CdkListbox) listbox: CdkListbox<string>;
860+
861+
onSelectionChange(event: ListboxSelectionChangeEvent<string>) {
862+
this.changedOption = event.option;
863+
}
864+
}

0 commit comments

Comments
 (0)