diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts
index cba1ab7c1562..e2604f0f68fa 100644
--- a/src/cdk-experimental/combobox/combobox.spec.ts
+++ b/src/cdk-experimental/combobox/combobox.spec.ts
@@ -379,6 +379,7 @@ describe('Combobox', () => {
No Value
diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts
index 11c06a663ce6..dfb734181286 100644
--- a/src/cdk-experimental/combobox/combobox.ts
+++ b/src/cdk-experimental/combobox/combobox.ts
@@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-
export type OpenAction = 'focus' | 'click' | 'downKey' | 'toggle';
export type OpenActionInput = OpenAction | OpenAction[] | string | null | undefined;
diff --git a/src/cdk-experimental/listbox/BUILD.bazel b/src/cdk-experimental/listbox/BUILD.bazel
index 41d32d2db0fe..7de4cdfa79e0 100644
--- a/src/cdk-experimental/listbox/BUILD.bazel
+++ b/src/cdk-experimental/listbox/BUILD.bazel
@@ -10,6 +10,7 @@ ng_module(
),
module_name = "@angular/cdk-experimental/listbox",
deps = [
+ "//src/cdk-experimental/combobox",
"//src/cdk/a11y",
"//src/cdk/collections",
"//src/cdk/keycodes",
@@ -25,6 +26,7 @@ ng_test_library(
),
deps = [
":listbox",
+ "//src/cdk-experimental/combobox",
"//src/cdk/keycodes",
"//src/cdk/testing/private",
"@npm//@angular/forms",
diff --git a/src/cdk-experimental/listbox/listbox.spec.ts b/src/cdk-experimental/listbox/listbox.spec.ts
index 12b58e06b687..1cc657054cc1 100644
--- a/src/cdk-experimental/listbox/listbox.spec.ts
+++ b/src/cdk-experimental/listbox/listbox.spec.ts
@@ -16,6 +16,8 @@ import {
} from '@angular/cdk/testing/private';
import {A, DOWN_ARROW, END, HOME, SPACE} from '@angular/cdk/keycodes';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {CdkCombobox, CdkComboboxModule} from '@angular/cdk-experimental/combobox';
+
describe('CdkOption and CdkListbox', () => {
@@ -657,6 +659,11 @@ describe('CdkOption and CdkListbox', () => {
listboxInstance.writeValue(['arc', 'stasis']);
fixture.detectChanges();
+ const selectedValues = listboxInstance.getSelectedValues();
+ expect(selectedValues.length).toBe(2);
+ expect(selectedValues[0]).toBe('arc');
+ expect(selectedValues[1]).toBe('stasis');
+
expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
expect(optionElements[1].hasAttribute('aria-selected')).toBeFalse();
@@ -762,6 +769,118 @@ describe('CdkOption and CdkListbox', () => {
expect(testComponent.form.value).toEqual(['solar']);
});
});
+
+ describe('inside a combobox', () => {
+ let fixture: ComponentFixture;
+ let testComponent: ListboxInsideCombobox;
+
+ let listbox: DebugElement;
+ let listboxInstance: CdkListbox;
+ let listboxElement: HTMLElement;
+
+ let combobox: DebugElement;
+ let comboboxInstance: CdkCombobox;
+ let comboboxElement: HTMLElement;
+
+ let options: DebugElement[];
+ let optionInstances: CdkOption[];
+ let optionElements: HTMLElement[];
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CdkListboxModule, CdkComboboxModule],
+ declarations: [ListboxInsideCombobox],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ListboxInsideCombobox);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+
+ combobox = fixture.debugElement.query(By.directive(CdkCombobox));
+ comboboxInstance = combobox.injector.get>(CdkCombobox);
+ comboboxElement = combobox.nativeElement;
+
+ });
+
+ it('should update combobox value on selection of an option', () => {
+ expect(comboboxInstance.value).toBeUndefined();
+ expect(comboboxInstance.isOpen()).toBeFalse();
+
+ dispatchMouseEvent(comboboxElement, 'click');
+ fixture.detectChanges();
+
+ listbox = fixture.debugElement.query(By.directive(CdkListbox));
+ listboxInstance = listbox.injector.get>(CdkListbox);
+
+ options = fixture.debugElement.queryAll(By.directive(CdkOption));
+ optionInstances = options.map(o => o.injector.get(CdkOption));
+ optionElements = options.map(o => o.nativeElement);
+
+ expect(comboboxInstance.isOpen()).toBeTrue();
+
+ dispatchMouseEvent(optionElements[0], 'click');
+ fixture.detectChanges();
+
+ expect(comboboxInstance.isOpen()).toBeFalse();
+ expect(comboboxInstance.value).toBe('purple');
+ });
+
+ it('should update combobox value on selection via keyboard', () => {
+ expect(comboboxInstance.value).toBeUndefined();
+ expect(comboboxInstance.isOpen()).toBeFalse();
+
+ dispatchMouseEvent(comboboxElement, 'click');
+ fixture.detectChanges();
+
+ listbox = fixture.debugElement.query(By.directive(CdkListbox));
+ listboxInstance = listbox.injector.get>(CdkListbox);
+ listboxElement = listbox.nativeElement;
+
+ options = fixture.debugElement.queryAll(By.directive(CdkOption));
+ optionInstances = options.map(o => o.injector.get(CdkOption));
+ optionElements = options.map(o => o.nativeElement);
+
+ expect(comboboxInstance.isOpen()).toBeTrue();
+
+ listboxInstance.setActiveOption(optionInstances[1]);
+ dispatchKeyboardEvent(listboxElement, 'keydown', SPACE);
+ fixture.detectChanges();
+
+ expect(comboboxInstance.isOpen()).toBeFalse();
+ expect(comboboxInstance.value).toBe('solar');
+ });
+
+ it('should not update combobox if listbox is in multiple mode', () => {
+ expect(comboboxInstance.value).toBeUndefined();
+ expect(comboboxInstance.isOpen()).toBeFalse();
+
+ dispatchMouseEvent(comboboxElement, 'click');
+ fixture.detectChanges();
+
+ listbox = fixture.debugElement.query(By.directive(CdkListbox));
+ listboxInstance = listbox.injector.get>(CdkListbox);
+ listboxElement = listbox.nativeElement;
+
+ testComponent.isMultiselectable = true;
+ fixture.detectChanges();
+
+ options = fixture.debugElement.queryAll(By.directive(CdkOption));
+ optionInstances = options.map(o => o.injector.get(CdkOption));
+ optionElements = options.map(o => o.nativeElement);
+
+ expect(comboboxInstance.isOpen()).toBeTrue();
+
+ listboxInstance.setActiveOption(optionInstances[1]);
+ dispatchKeyboardEvent(listboxElement, 'keydown', SPACE);
+ fixture.detectChanges();
+
+ expect(comboboxInstance.isOpen()).toBeTrue();
+ expect(comboboxInstance.value).toBeUndefined();
+ });
+ });
});
@Component({
@@ -862,3 +981,36 @@ class ListboxControlValueAccessor {
this.changedOption = event.option;
}
}
+
+@Component({
+ template: `
+
+
+
+
+
+ `
+})
+class ListboxInsideCombobox {
+ changedOption: CdkOption;
+ isDisabled: boolean = false;
+ isMultiselectable: boolean = false;
+ @ViewChild(CdkListbox) listbox: CdkListbox;
+
+ onSelectionChange(event: ListboxSelectionChangeEvent) {
+ this.changedOption = event.option;
+ }
+}
diff --git a/src/cdk-experimental/listbox/listbox.ts b/src/cdk-experimental/listbox/listbox.ts
index a48ce103a3fb..9d68137682a3 100644
--- a/src/cdk-experimental/listbox/listbox.ts
+++ b/src/cdk-experimental/listbox/listbox.ts
@@ -11,8 +11,8 @@ import {
ContentChildren,
Directive,
ElementRef, EventEmitter, forwardRef,
- Inject,
- Input, OnDestroy, OnInit, Output,
+ Inject, InjectionToken,
+ Input, OnDestroy, OnInit, Optional, Output,
QueryList
} from '@angular/core';
import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y';
@@ -22,8 +22,10 @@ import {SelectionChange, SelectionModel} from '@angular/cdk/collections';
import {defer, merge, Observable, Subject} from 'rxjs';
import {startWith, switchMap, takeUntil} from 'rxjs/operators';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {CdkComboboxPanel} from '@angular/cdk-experimental/combobox';
let nextId = 0;
+let listboxId = 0;
export const CDK_LISTBOX_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
@@ -31,6 +33,8 @@ export const CDK_LISTBOX_VALUE_ACCESSOR: any = {
multi: true
};
+export const PANEL = new InjectionToken('CdkComboboxPanel');
+
@Directive({
selector: '[cdkOption]',
exportAs: 'cdkOption',
@@ -172,6 +176,10 @@ export class CdkOption implements ListKeyManagerOption, Highlightab
}
}
+ getElementRef() {
+ return this._elementRef;
+ }
+
/** Sets the active property to true to enable the active css class. */
setActiveStyles() {
this._active = true;
@@ -191,6 +199,7 @@ export class CdkOption implements ListKeyManagerOption, Highlightab
exportAs: 'cdkListbox',
host: {
'role': 'listbox',
+ '[id]': 'id',
'(keydown)': '_keydown($event)',
'[attr.tabindex]': '_tabIndex',
'[attr.aria-disabled]': 'disabled',
@@ -231,6 +240,8 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
@Output() readonly selectionChange: EventEmitter> =
new EventEmitter>();
+ @Input() id = `cdk-option-${listboxId++}`;
+
/**
* Whether the listbox allows multiple options to be selected.
* If `multiple` switches from `true` to `false`, all options are deselected.
@@ -263,6 +274,10 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
@Input() compareWith: (o1: T, o2: T) => boolean = (a1, a2) => a1 === a2;
+ @Input('parentPanel') private readonly _explicitPanel: CdkComboboxPanel;
+
+ constructor(@Optional() @Inject(PANEL) readonly _parentPanel?: CdkComboboxPanel) { }
+
ngOnInit() {
this._selectionModel = new SelectionModel>(this.multiple);
}
@@ -270,11 +285,13 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
ngAfterContentInit() {
this._initKeyManager();
this._initSelectionModel();
+ this._registerWithPanel();
this.optionSelectionChanges.subscribe(event => {
this._emitChangeEvent(event.source);
this._updateSelectionModel(event.source);
this.setActiveOption(event.source);
+ this._updatePanelForSelection(event.source);
});
}
@@ -284,6 +301,11 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
this._destroyed.complete();
}
+ private _registerWithPanel(): void {
+ const panel = this._parentPanel || this._explicitPanel;
+ panel?._registerContent(this.id, 'listbox');
+ }
+
private _initKeyManager() {
this._listKeyManager = new ActiveDescendantKeyManager(this._options)
.withWrap()
@@ -358,6 +380,13 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
this._selectionModel.deselect(option);
}
+ _updatePanelForSelection(option: CdkOption) {
+ if (!this.multiple) {
+ const panel = this._parentPanel || this._explicitPanel;
+ option.selected ? panel?.closePanel(option.value) : panel?.closePanel();
+ }
+ }
+
/** Toggles the selected state of the active option if not disabled. */
private _toggleActiveOption() {
const activeOption = this._listKeyManager.activeItem;
@@ -420,6 +449,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
/** Updates the key manager's active item to the given option. */
setActiveOption(option: CdkOption) {
this._listKeyManager.updateActiveItem(option);
+ this._updateActiveOption();
}
/**
@@ -450,6 +480,11 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr
this.disabled = isDisabled;
}
+ /** Returns the values of the currently selected options. */
+ getSelectedValues(): T[] {
+ return this._options.filter(option => option.selected).map(option => option.value);
+ }
+
/** Selects an option that has the corresponding given value. */
private _setSelectionByValue(values: T | T[]) {
for (const option of this._options.toArray()) {