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()) {