Skip to content

Commit d298b7d

Browse files
committed
fix(cdk-experimental/listbox): clean up some TODOs
1 parent 3463c5a commit d298b7d

File tree

4 files changed

+466
-165
lines changed

4 files changed

+466
-165
lines changed

src/cdk-experimental/listbox/BUILD.bazel

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ ng_module(
99
exclude = ["**/*.spec.ts"],
1010
),
1111
deps = [
12-
"//src/cdk-experimental/combobox",
1312
"//src/cdk/a11y",
13+
"//src/cdk/bidi",
1414
"//src/cdk/collections",
1515
"//src/cdk/keycodes",
1616
"@npm//@angular/forms",
@@ -25,7 +25,6 @@ ng_test_library(
2525
),
2626
deps = [
2727
":listbox",
28-
"//src/cdk-experimental/combobox",
2928
"//src/cdk/keycodes",
3029
"//src/cdk/testing/private",
3130
"@npm//@angular/common",

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

Lines changed: 170 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@ import {Component, Type} from '@angular/core';
33
import {By} from '@angular/platform-browser';
44
import {CdkListbox, CdkListboxModule, CdkOption, ListboxValueChangeEvent} from './index';
55
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../../cdk/testing/private';
6-
import {B, DOWN_ARROW, END, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
6+
import {
7+
A,
8+
B,
9+
DOWN_ARROW,
10+
END,
11+
HOME,
12+
LEFT_ARROW,
13+
RIGHT_ARROW,
14+
SPACE,
15+
UP_ARROW,
16+
} from '@angular/cdk/keycodes';
717
import {FormControl, ReactiveFormsModule} from '@angular/forms';
818
import {CommonModule} from '@angular/common';
919

@@ -132,6 +142,27 @@ describe('CdkOption and CdkListbox', () => {
132142
expect(fixture.componentInstance.changedOption?.id).toBe(options[0].id);
133143
});
134144

145+
it('should select and deselect range on option SHIFT + click', async () => {
146+
const {testComponent, fixture, listbox, optionEls} = await setupComponent(ListboxWithOptions);
147+
testComponent.isMultiselectable = true;
148+
fixture.detectChanges();
149+
150+
dispatchMouseEvent(optionEls[1], 'click', undefined, undefined, undefined, {shift: true});
151+
fixture.detectChanges();
152+
153+
expect(listbox.value).toEqual(['orange']);
154+
155+
dispatchMouseEvent(optionEls[3], 'click', undefined, undefined, undefined, {shift: true});
156+
fixture.detectChanges();
157+
158+
expect(listbox.value).toEqual(['orange', 'banana', 'peach']);
159+
160+
dispatchMouseEvent(optionEls[2], 'click', undefined, undefined, undefined, {shift: true});
161+
fixture.detectChanges();
162+
163+
expect(listbox.value).toEqual(['orange']);
164+
});
165+
135166
it('should update on option activated via keyboard', async () => {
136167
const {fixture, listbox, listboxEl, options, optionEls} = await setupComponent(
137168
ListboxWithOptions,
@@ -260,20 +291,19 @@ describe('CdkOption and CdkListbox', () => {
260291
expect(options[1].isSelected()).toBeFalse();
261292
});
262293

263-
// TODO(mmalerba): Fix this case.
264-
// Currently banana gets booted because the option isn't loaded yet,
265-
// but then when the option loads the value is already lost.
266-
// it('should allow binding to listbox value', async () => {
267-
// const {testComponent, fixture, listbox, options} = await setupComponent(ListboxWithBoundValue);
268-
// expect(listbox.value).toEqual(['banana']);
269-
// expect(options[2].isSelected()).toBeTrue();
270-
//
271-
// testComponent.value = ['orange'];
272-
// fixture.detectChanges();
273-
//
274-
// expect(listbox.value).toEqual(['orange']);
275-
// expect(options[1].isSelected()).toBeTrue();
276-
// });
294+
it('should allow binding to listbox value', async () => {
295+
const {testComponent, fixture, listbox, options} = await setupComponent(
296+
ListboxWithBoundValue,
297+
);
298+
expect(listbox.value).toEqual(['banana']);
299+
expect(options[2].isSelected()).toBeTrue();
300+
301+
testComponent.value = ['orange'];
302+
fixture.detectChanges();
303+
304+
expect(listbox.value).toEqual(['orange']);
305+
expect(options[1].isSelected()).toBeTrue();
306+
});
277307
});
278308

279309
describe('disabled state', () => {
@@ -482,7 +512,105 @@ describe('CdkOption and CdkListbox', () => {
482512
expect(fixture.componentInstance.changedOption?.id).toBe(options[1].id);
483513
});
484514

485-
// TODO(mmalerba): ensure all keys covered
515+
it('should update active item on arrow key presses in horizontal mode', async () => {
516+
const {testComponent, fixture, listbox, listboxEl, options} = await setupComponent(
517+
ListboxWithOptions,
518+
);
519+
testComponent.orientation = 'horizontal';
520+
fixture.detectChanges();
521+
522+
expect(listboxEl.getAttribute('aria-orientation')).toBe('horizontal');
523+
524+
listbox.focus();
525+
dispatchKeyboardEvent(listboxEl, 'keydown', RIGHT_ARROW);
526+
fixture.detectChanges();
527+
528+
expect(options[1].isActive()).toBeTrue();
529+
530+
dispatchKeyboardEvent(listboxEl, 'keydown', LEFT_ARROW);
531+
fixture.detectChanges();
532+
533+
expect(options[0].isActive()).toBeTrue();
534+
});
535+
536+
it('should select and deselect all option with CONTROL + A', async () => {
537+
const {testComponent, fixture, listbox, listboxEl} = await setupComponent(ListboxWithOptions);
538+
testComponent.isMultiselectable = true;
539+
fixture.detectChanges();
540+
541+
listbox.focus();
542+
dispatchKeyboardEvent(listboxEl, 'keydown', A, undefined, {control: true});
543+
fixture.detectChanges();
544+
545+
expect(listbox.value).toEqual(['apple', 'orange', 'banana', 'peach']);
546+
547+
dispatchKeyboardEvent(listboxEl, 'keydown', A, undefined, {control: true});
548+
fixture.detectChanges();
549+
550+
expect(listbox.value).toEqual([]);
551+
});
552+
553+
it('should select and deselect range with CONTROL + SPACE', async () => {
554+
const {testComponent, fixture, listbox, listboxEl} = await setupComponent(ListboxWithOptions);
555+
testComponent.isMultiselectable = true;
556+
fixture.detectChanges();
557+
558+
listbox.focus();
559+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
560+
dispatchKeyboardEvent(listboxEl, 'keydown', SPACE, undefined, {shift: true});
561+
fixture.detectChanges();
562+
563+
expect(listbox.value).toEqual(['orange']);
564+
565+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
566+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
567+
dispatchKeyboardEvent(listboxEl, 'keydown', SPACE, undefined, {shift: true});
568+
fixture.detectChanges();
569+
570+
expect(listbox.value).toEqual(['orange', 'banana', 'peach']);
571+
572+
dispatchKeyboardEvent(listboxEl, 'keydown', UP_ARROW);
573+
dispatchKeyboardEvent(listboxEl, 'keydown', SPACE, undefined, {shift: true});
574+
575+
expect(listbox.value).toEqual(['orange']);
576+
});
577+
578+
it('should select and deselect range with CONTROL + SHIFT + HOME', async () => {
579+
const {testComponent, fixture, listbox, listboxEl} = await setupComponent(ListboxWithOptions);
580+
testComponent.isMultiselectable = true;
581+
listbox.focus();
582+
fixture.detectChanges();
583+
584+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
585+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
586+
dispatchKeyboardEvent(listboxEl, 'keydown', HOME, undefined, {control: true, shift: true});
587+
588+
expect(listbox.value).toEqual(['apple', 'orange', 'banana']);
589+
590+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
591+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
592+
dispatchKeyboardEvent(listboxEl, 'keydown', HOME, undefined, {control: true, shift: true});
593+
594+
expect(listbox.value).toEqual([]);
595+
});
596+
597+
it('should select and deselect range with CONTROL + SHIFT + END', async () => {
598+
const {testComponent, fixture, listbox, listboxEl} = await setupComponent(ListboxWithOptions);
599+
testComponent.isMultiselectable = true;
600+
listbox.focus();
601+
fixture.detectChanges();
602+
603+
dispatchKeyboardEvent(listboxEl, 'keydown', DOWN_ARROW);
604+
dispatchKeyboardEvent(listboxEl, 'keydown', END, undefined, {control: true, shift: true});
605+
606+
expect(listbox.value).toEqual(['orange', 'banana', 'peach']);
607+
608+
dispatchKeyboardEvent(listboxEl, 'keydown', UP_ARROW);
609+
dispatchKeyboardEvent(listboxEl, 'keydown', UP_ARROW);
610+
dispatchKeyboardEvent(listboxEl, 'keydown', END, undefined, {control: true, shift: true});
611+
612+
expect(listbox.value).toEqual([]);
613+
});
486614
});
487615

488616
describe('with roving tabindex', () => {
@@ -639,15 +767,15 @@ describe('CdkOption and CdkListbox', () => {
639767
subscription.unsubscribe();
640768
});
641769

642-
it('should have FormControl error multiple values selected in single-select listbox', async () => {
770+
it('should have FormControl error when multiple values selected in single-select listbox', async () => {
643771
const {testComponent, fixture} = await setupComponent(ListboxWithFormControl, [
644772
ReactiveFormsModule,
645773
]);
646774
testComponent.formControl.setValue(['orange', 'banana']);
647775
fixture.detectChanges();
648776

649-
expect(testComponent.formControl.hasError('cdkListboxMultipleValues')).toBeTrue();
650-
expect(testComponent.formControl.hasError('cdkListboxInvalidValues')).toBeFalse();
777+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeTrue();
778+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeFalse();
651779
});
652780

653781
it('should have FormControl error when non-option value selected', async () => {
@@ -658,9 +786,9 @@ describe('CdkOption and CdkListbox', () => {
658786
testComponent.formControl.setValue(['orange', 'dragonfruit', 'mango']);
659787
fixture.detectChanges();
660788

661-
expect(testComponent.formControl.hasError('cdkListboxInvalidValues')).toBeTrue();
662-
expect(testComponent.formControl.hasError('cdkListboxMultipleValues')).toBeFalse();
663-
expect(testComponent.formControl.errors?.['cdkListboxInvalidValues']).toEqual({
789+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeTrue();
790+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeFalse();
791+
expect(testComponent.formControl.errors?.['cdkListboxUnexpectedOptionValues']).toEqual({
664792
'values': ['dragonfruit', 'mango'],
665793
});
666794
});
@@ -672,9 +800,9 @@ describe('CdkOption and CdkListbox', () => {
672800
testComponent.formControl.setValue(['dragonfruit', 'mango']);
673801
fixture.detectChanges();
674802

675-
expect(testComponent.formControl.hasError('cdkListboxInvalidValues')).toBeTrue();
676-
expect(testComponent.formControl.hasError('cdkListboxMultipleValues')).toBeTrue();
677-
expect(testComponent.formControl.errors?.['cdkListboxInvalidValues']).toEqual({
803+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeTrue();
804+
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeTrue();
805+
expect(testComponent.formControl.errors?.['cdkListboxUnexpectedOptionValues']).toEqual({
678806
'values': ['dragonfruit', 'mango'],
679807
});
680808
});
@@ -689,6 +817,7 @@ describe('CdkOption and CdkListbox', () => {
689817
[cdkListboxMultiple]="isMultiselectable"
690818
[cdkListboxDisabled]="isListboxDisabled"
691819
[cdkListboxUseActiveDescendant]="isActiveDescendant"
820+
[cdkListboxOrientation]="orientation"
692821
(cdkListboxValueChange)="onSelectionChange($event)">
693822
<div cdkOption="apple"
694823
[cdkOptionDisabled]="isAppleDisabled"
@@ -703,7 +832,7 @@ describe('CdkOption and CdkListbox', () => {
703832
`,
704833
})
705834
class ListboxWithOptions {
706-
changedOption: CdkOption;
835+
changedOption: CdkOption | null;
707836
isListboxDisabled = false;
708837
isAppleDisabled = false;
709838
isOrangeDisabled = false;
@@ -713,6 +842,7 @@ class ListboxWithOptions {
713842
listboxTabindex: number;
714843
appleId: string;
715844
appleTabindex: number;
845+
orientation: 'horizontal' | 'vertical' = 'vertical';
716846

717847
onSelectionChange(event: ListboxValueChangeEvent<unknown>) {
718848
this.changedOption = event.option;
@@ -755,20 +885,20 @@ class ListboxWithFormControl {
755885
})
756886
class ListboxWithCustomTypeahead {}
757887

758-
// @Component({
759-
// template: `
760-
// <div cdkListbox
761-
// [cdkListboxValue]="value">
762-
// <div cdkOption="apple">Apple</div>
763-
// <div cdkOption="orange">Orange</div>
764-
// <div cdkOption="banana">Banana</div>
765-
// <div cdkOption="peach">Peach</div>
766-
// </div>
767-
// `,
768-
// })
769-
// class ListboxWithBoundValue {
770-
// value = ['banana'];
771-
// }
888+
@Component({
889+
template: `
890+
<div cdkListbox
891+
[cdkListboxValue]="value">
892+
<div cdkOption="apple">Apple</div>
893+
<div cdkOption="orange">Orange</div>
894+
<div cdkOption="banana">Banana</div>
895+
<div cdkOption="peach">Peach</div>
896+
</div>
897+
`,
898+
})
899+
class ListboxWithBoundValue {
900+
value = ['banana'];
901+
}
772902

773903
@Component({
774904
template: `

0 commit comments

Comments
 (0)