diff --git a/src/cdk-experimental/combobox/combobox-module.ts b/src/cdk-experimental/combobox/combobox-module.ts index b8ac8c7782bc..075c6770394b 100644 --- a/src/cdk-experimental/combobox/combobox-module.ts +++ b/src/cdk-experimental/combobox/combobox-module.ts @@ -9,10 +9,9 @@ import {NgModule} from '@angular/core'; import {OverlayModule} from '@angular/cdk/overlay'; import {CdkCombobox} from './combobox'; -import {CdkComboboxPanel} from './combobox-panel'; import {CdkComboboxPopup} from './combobox-popup'; -const EXPORTED_DECLARATIONS = [CdkCombobox, CdkComboboxPanel, CdkComboboxPopup]; +const EXPORTED_DECLARATIONS = [CdkCombobox, CdkComboboxPopup]; @NgModule({ imports: [OverlayModule], exports: EXPORTED_DECLARATIONS, diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts deleted file mode 100644 index bb8b148e95a2..000000000000 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -export type AriaHasPopupValue = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; - -import {Directive, TemplateRef} from '@angular/core'; -import {Subject} from 'rxjs'; - -@Directive({ - host: { - 'class': 'cdk-combobox-panel', - }, - selector: 'ng-template[cdkComboboxPanel]', - exportAs: 'cdkComboboxPanel', -}) -export class CdkComboboxPanel { - valueUpdated: Subject = new Subject(); - contentIdUpdated: Subject = new Subject(); - contentTypeUpdated: Subject = new Subject(); - - contentId: string = ''; - contentType: AriaHasPopupValue; - - constructor(readonly _templateRef: TemplateRef) {} - - /** Tells the parent combobox to close the panel and sends back the content value. */ - closePanel(data?: T | T[]) { - this.valueUpdated.next(data || []); - } - - // TODO: instead of using a focus function, potentially use cdk/a11y focus trapping - focusContent() { - // TODO: Use an injected document here - document.getElementById(this.contentId)?.focus(); - } - - /** Registers the content's id and the content type with the panel. */ - _registerContent(contentId: string, contentType: AriaHasPopupValue) { - // If content has already been registered, no further contentIds are registered. - if (this.contentType && this.contentType !== contentType) { - return; - } - - this.contentId = contentId; - if (contentType !== 'listbox' && contentType !== 'dialog') { - throw Error('CdkComboboxPanel currently only supports listbox or dialog content.'); - } - this.contentType = contentType; - - this.contentIdUpdated.next(this.contentId); - this.contentTypeUpdated.next(this.contentType); - } -} diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts index c00e609cb406..b1bd515006a2 100644 --- a/src/cdk-experimental/combobox/combobox-popup.ts +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -6,18 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { - Directive, - ElementRef, - Inject, - InjectionToken, - Input, - OnInit, - Optional, -} from '@angular/core'; -import {AriaHasPopupValue, CdkComboboxPanel} from './combobox-panel'; - -export const PANEL = new InjectionToken('CdkComboboxPanel'); +import {Directive, ElementRef, Inject, Input, OnInit} from '@angular/core'; +import {AriaHasPopupValue, CDK_COMBOBOX, CdkCombobox} from './combobox'; let nextId = 0; @@ -53,11 +43,9 @@ export class CdkComboboxPopup implements OnInit { @Input() id = `cdk-combobox-popup-${nextId++}`; - @Input('parentPanel') private readonly _explicitPanel: CdkComboboxPanel; - constructor( private readonly _elementRef: ElementRef, - @Optional() @Inject(PANEL) readonly _parentPanel?: CdkComboboxPanel, + @Inject(CDK_COMBOBOX) private readonly _combobox: CdkCombobox, ) {} ngOnInit() { @@ -65,11 +53,7 @@ export class CdkComboboxPopup implements OnInit { } registerWithPanel(): void { - if (this._parentPanel === null || this._parentPanel === undefined) { - this._explicitPanel._registerContent(this.id, this._role); - } else { - this._parentPanel._registerContent(this.id, this._role); - } + this._combobox._registerContent(this.id, this._role); } focusFirstElement() { diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 277ad6521650..32a705e7c9f6 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -1,25 +1,11 @@ -import { - Component, - DebugElement, - Directive, - ElementRef, - Inject, - InjectionToken, - Input, - OnInit, - Optional, - ViewChild, -} from '@angular/core'; +import {Component, DebugElement, ElementRef, ViewChild} from '@angular/core'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {CdkComboboxModule} from './combobox-module'; import {CdkCombobox} from './combobox'; import {dispatchKeyboardEvent, dispatchMouseEvent} from '../../cdk/testing/private'; -import { - AriaHasPopupValue, - CdkComboboxPanel, -} from '@angular/cdk-experimental/combobox/combobox-panel'; import {DOWN_ARROW, ESCAPE} from '@angular/cdk/keycodes'; +import {CdkComboboxPopup} from '@angular/cdk-experimental/combobox/combobox-popup'; describe('Combobox', () => { describe('with a basic toggle trigger', () => { @@ -27,11 +13,11 @@ describe('Combobox', () => { let testComponent: ComboboxToggle; let combobox: DebugElement; - let comboboxInstance: CdkCombobox; + let comboboxInstance: CdkCombobox; let comboboxElement: HTMLElement; let dialog: DebugElement; - let dialogInstance: FakeDialogContent; + let dialogInstance: CdkComboboxPopup; let dialogElement: HTMLElement; let applyButton: DebugElement; @@ -41,7 +27,7 @@ describe('Combobox', () => { waitForAsync(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, FakeDialogContent], + declarations: [ComboboxToggle], }).compileComponents(); }), ); @@ -53,7 +39,7 @@ describe('Combobox', () => { testComponent = fixture.debugElement.componentInstance; combobox = fixture.debugElement.query(By.directive(CdkCombobox)); - comboboxInstance = combobox.injector.get>(CdkCombobox); + comboboxInstance = combobox.injector.get(CdkCombobox); comboboxElement = combobox.nativeElement; }); @@ -77,10 +63,10 @@ describe('Combobox', () => { dispatchMouseEvent(comboboxElement, 'click'); fixture.detectChanges(); - dialog = fixture.debugElement.query(By.directive(FakeDialogContent)); - dialogInstance = dialog.injector.get>(FakeDialogContent); + dialog = fixture.debugElement.query(By.directive(CdkComboboxPopup)); + dialogInstance = dialog.injector.get(CdkComboboxPopup); - expect(comboboxElement.getAttribute('aria-owns')).toBe(dialogInstance.dialogId); + expect(comboboxElement.getAttribute('aria-owns')).toBe(dialogInstance.id); expect(comboboxElement.getAttribute('aria-haspopup')).toBe('dialog'); }); @@ -110,7 +96,7 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeTrue(); - dialog = fixture.debugElement.query(By.directive(FakeDialogContent)); + dialog = fixture.debugElement.query(By.directive(CdkComboboxPopup)); dialogElement = dialog.nativeElement; expect(document.activeElement).toBe(dialogElement); @@ -201,13 +187,13 @@ describe('Combobox', () => { let testComponent: ComboboxToggle; let combobox: DebugElement; - let comboboxInstance: CdkCombobox; + let comboboxInstance: CdkCombobox; beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, FakeDialogContent], + declarations: [ComboboxToggle], }).compileComponents(); }), ); @@ -219,7 +205,7 @@ describe('Combobox', () => { testComponent = fixture.debugElement.componentInstance; combobox = fixture.debugElement.query(By.directive(CdkCombobox)); - comboboxInstance = combobox.injector.get>(CdkCombobox); + comboboxInstance = combobox.injector.get(CdkCombobox); }); it('should coerce single string into open action', () => { @@ -273,14 +259,14 @@ describe('Combobox', () => { let testComponent: ComboboxToggle; let combobox: DebugElement; - let comboboxInstance: CdkCombobox; + let comboboxInstance: CdkCombobox; let comboboxElement: HTMLElement; beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, FakeDialogContent], + declarations: [ComboboxToggle], }).compileComponents(); }), ); @@ -292,7 +278,7 @@ describe('Combobox', () => { testComponent = fixture.debugElement.componentInstance; combobox = fixture.debugElement.query(By.directive(CdkCombobox)); - comboboxInstance = combobox.injector.get>(CdkCombobox); + comboboxInstance = combobox.injector.get(CdkCombobox); comboboxElement = combobox.nativeElement; }); @@ -392,17 +378,17 @@ describe('Combobox', () => { @Component({ template: ` -
- -
+ +
- +
`, }) @@ -411,37 +397,3 @@ class ComboboxToggle { actions: string = 'click'; } - -export const PANEL = new InjectionToken('CdkComboboxPanel'); - -let id = 0; - -@Directive({ - selector: '[dialogContent]', - exportAs: 'dialogContent', - host: { - '[attr.role]': 'role', - '[id]': 'dialogId', - 'tabIndex': '-1', - }, -}) -export class FakeDialogContent implements OnInit { - dialogId = `dialog-${id++}`; - role = 'dialog'; - - @Input('parentPanel') private readonly _explicitPanel: CdkComboboxPanel; - - constructor(@Optional() @Inject(PANEL) readonly _parentPanel?: CdkComboboxPanel) {} - - ngOnInit() { - this.registerWithPanel(); - } - - registerWithPanel(): void { - if (this._parentPanel === null || this._parentPanel === undefined) { - this._explicitPanel._registerContent(this.dialogId, this.role as AriaHasPopupValue); - } else { - this._parentPanel._registerContent(this.dialogId, this.role as AriaHasPopupValue); - } - } -} diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 049576ea199b..2b22d860559d 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -5,22 +5,21 @@ * Use of this source code is governed by an MIT-style license that can be * 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; - +import {DOCUMENT} from '@angular/common'; import { - AfterContentInit, Directive, ElementRef, EventEmitter, + Inject, + InjectionToken, + Injector, Input, OnDestroy, Optional, Output, + TemplateRef, ViewContainerRef, } from '@angular/core'; -import {CdkComboboxPanel, AriaHasPopupValue} from './combobox-panel'; import {TemplatePortal} from '@angular/cdk/portal'; import { ConnectedPosition, @@ -30,12 +29,18 @@ import { OverlayRef, } from '@angular/cdk/overlay'; import {Directionality} from '@angular/cdk/bidi'; -import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion'; +import {BooleanInput, coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion'; import {_getEventTarget} from '@angular/cdk/platform'; import {DOWN_ARROW, ENTER, ESCAPE, TAB} from '@angular/cdk/keycodes'; +export type AriaHasPopupValue = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; +export type OpenAction = 'focus' | 'click' | 'downKey' | 'toggle'; +export type OpenActionInput = OpenAction | OpenAction[] | string | null | undefined; + const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle']; +export const CDK_COMBOBOX = new InjectionToken('CDK_COMBOBOX'); + @Directive({ selector: '[cdkCombobox]', exportAs: 'cdkCombobox', @@ -52,16 +57,11 @@ const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle']; '[attr.aria-expanded]': 'isOpen()', '[attr.tabindex]': '_getTabIndex()', }, + providers: [{provide: CDK_COMBOBOX, useExisting: CdkCombobox}], }) -export class CdkCombobox implements OnDestroy, AfterContentInit { +export class CdkCombobox implements OnDestroy { @Input('cdkComboboxTriggerFor') - get panel(): CdkComboboxPanel | undefined { - return this._panel; - } - set panel(panel: CdkComboboxPanel | undefined) { - this._panel = panel; - } - private _panel: CdkComboboxPanel | undefined; + _panelTemplateRef: TemplateRef; @Input() value: T | T[]; @@ -101,7 +101,8 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { >(); private _overlayRef: OverlayRef; - private _panelContent: TemplatePortal; + private _panelPortal: TemplatePortal; + contentId: string = ''; contentType: AriaHasPopupValue; @@ -109,24 +110,11 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { private readonly _elementRef: ElementRef, private readonly _overlay: Overlay, protected readonly _viewContainerRef: ViewContainerRef, + private readonly _injector: Injector, + @Inject(DOCUMENT) private readonly _doc: any, @Optional() private readonly _directionality?: Directionality, ) {} - ngAfterContentInit() { - this._panel?.valueUpdated.subscribe(data => { - this._setComboboxValue(data); - this.close(); - }); - - this._panel?.contentIdUpdated.subscribe(id => { - this.contentId = id; - }); - - this._panel?.contentTypeUpdated.subscribe(type => { - this.contentType = type; - }); - } - ngOnDestroy() { if (this._overlayRef) { this._overlayRef.dispose(); @@ -142,7 +130,8 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { if (keyCode === DOWN_ARROW) { if (this.isOpen()) { - this._panel?.focusContent(); + // TODO: instead of using a focus function, potentially use cdk/a11y focus trapping + this._doc.getElementById(this.contentId)?.focus(); } else if (this._openActions.indexOf('downKey') !== -1) { this.open(); } @@ -204,7 +193,8 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); this._overlayRef.attach(this._getPanelContent()); if (!this._isTextTrigger()) { - this._panel?.focusContent(); + // TODO: instead of using a focus function, potentially use cdk/a11y focus trapping + this._doc.getElementById(this.contentId)?.focus(); } } } @@ -224,7 +214,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { /** Returns true if combobox has a child panel. */ hasPanel(): boolean { - return !!this.panel; + return !!this._panelTemplateRef; } _getTabIndex(): string | null { @@ -243,6 +233,11 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } } + updateAndClose(value: T | T[]) { + this._setComboboxValue(value); + this.close(); + } + private _setTextContent(content: T | T[]) { const contentArray = coerceArray(content); this._elementRef.nativeElement.textContent = contentArray.join(' '); @@ -278,13 +273,22 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { ]; } + private _getPanelInjector() { + return this._injector; + } + private _getPanelContent() { - const hasPanelChanged = this._panel?._templateRef !== this._panelContent?.templateRef; - if (this._panel && (!this._panel || hasPanelChanged)) { - this._panelContent = new TemplatePortal(this._panel._templateRef, this._viewContainerRef); + const hasPanelChanged = this._panelTemplateRef !== this._panelPortal?.templateRef; + if (this._panelTemplateRef && (!this._panelPortal || hasPanelChanged)) { + this._panelPortal = new TemplatePortal( + this._panelTemplateRef, + this._viewContainerRef, + undefined, + this._getPanelInjector(), + ); } - return this._panelContent; + return this._panelPortal; } private _coerceOpenActionProperty(input: OpenActionInput): OpenAction[] { @@ -297,4 +301,17 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } return actions as OpenAction[]; } + + /** Registers the content's id and the content type with the panel. */ + _registerContent(contentId: string, contentType: AriaHasPopupValue) { + if ( + (typeof ngDevMode === 'undefined' || ngDevMode) && + contentType !== 'listbox' && + contentType !== 'dialog' + ) { + throw Error('CdkComboboxPanel currently only supports listbox or dialog content.'); + } + this.contentId = contentId; + this.contentType = contentType; + } } diff --git a/src/cdk-experimental/combobox/public-api.ts b/src/cdk-experimental/combobox/public-api.ts index 07eadcfa2ec1..39f92a795d37 100644 --- a/src/cdk-experimental/combobox/public-api.ts +++ b/src/cdk-experimental/combobox/public-api.ts @@ -8,5 +8,4 @@ export * from './combobox-module'; export * from './combobox'; -export * from './combobox-panel'; export * from './combobox-popup'; diff --git a/src/cdk-experimental/listbox/listbox.spec.ts b/src/cdk-experimental/listbox/listbox.spec.ts index cc68637f35ec..19fa36bfdf00 100644 --- a/src/cdk-experimental/listbox/listbox.spec.ts +++ b/src/cdk-experimental/listbox/listbox.spec.ts @@ -1,7 +1,7 @@ -import {ComponentFixture, waitForAsync, TestBed, tick, fakeAsync} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; import {Component, DebugElement, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; -import {CdkOption, CdkListboxModule, ListboxSelectionChangeEvent, CdkListbox} from './index'; +import {CdkListbox, CdkListboxModule, CdkOption, ListboxSelectionChangeEvent} from './index'; import { createKeyboardEvent, dispatchKeyboardEvent, @@ -9,7 +9,7 @@ import { } from '../../cdk/testing/private'; import {A, DOWN_ARROW, END, HOME, SPACE} from '@angular/cdk/keycodes'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {CdkCombobox, CdkComboboxModule, CdkComboboxPanel} from '@angular/cdk-experimental/combobox'; +import {CdkCombobox, CdkComboboxModule} from '@angular/cdk-experimental/combobox'; describe('CdkOption and CdkListbox', () => { describe('selection state change', () => { @@ -887,7 +887,7 @@ describe('CdkOption and CdkListbox', () => { listboxInstance.setActiveOption(optionInstances[1]); dispatchKeyboardEvent(listboxElement, 'keydown', SPACE); - testComponent.panel.closePanel(testComponent.listbox.getSelectedValues()); + testComponent.combobox.updateAndClose(testComponent.listbox.getSelectedValues()); fixture.detectChanges(); expect(comboboxInstance.isOpen()).toBeFalse(); @@ -1003,9 +1003,8 @@ class ListboxControlValueAccessor { No Value - + - +
diff --git a/src/dev-app/cdk-experimental-combobox/panel-content.ts b/src/dev-app/cdk-experimental-combobox/panel-content.ts deleted file mode 100644 index a4512cc23dae..000000000000 --- a/src/dev-app/cdk-experimental-combobox/panel-content.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Directive, Inject, InjectionToken, Input, OnInit, Optional} from '@angular/core'; -import {AriaHasPopupValue, CdkComboboxPanel} from '@angular/cdk-experimental/combobox'; - -export const PANEL = new InjectionToken('CdkComboboxPanel'); - -let id = 0; - -@Directive({ - selector: '[panelContent]', - exportAs: 'panelContent', - host: { - 'role': 'role', - '[id]': 'dialogId', - }, -}) -export class PanelContent implements OnInit { - dialogId = `dialog-${id++}`; - role = 'dialog'; - - @Input('parentPanel') private readonly _explicitPanel: CdkComboboxPanel; - - constructor(@Optional() @Inject(PANEL) readonly _parentPanel?: CdkComboboxPanel) {} - - ngOnInit() { - this.registerWithPanel(); - } - - registerWithPanel(): void { - if (this._parentPanel === null || this._parentPanel === undefined) { - this._explicitPanel._registerContent(this.dialogId, this.role as AriaHasPopupValue); - } else { - this._parentPanel._registerContent(this.dialogId, this.role as AriaHasPopupValue); - } - } -}