From bfdb15620e3920ce897a11633879fab9c7dc3d55 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 13:08:07 -0400 Subject: [PATCH 01/33] build: Added required files to listbox directory. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts create mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts new file mode 100644 index 000000000000..e69de29bb2d1 From f8f63997acedd51ad0c935e616f10f152240a705 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 15:36:56 -0400 Subject: [PATCH 02/33] build: added listbox option directive and renamed listbox directive files. --- .../{listbox-directives.spec.ts => listbox-directive.spec.ts} | 0 .../listbox/{listbox-directives.ts => listbox-directive.ts} | 0 src/cdk-experimental/listbox/listbox-option-directive.spec.ts | 0 src/cdk-experimental/listbox/listbox-option-directive.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/cdk-experimental/listbox/{listbox-directives.spec.ts => listbox-directive.spec.ts} (100%) rename src/cdk-experimental/listbox/{listbox-directives.ts => listbox-directive.ts} (100%) create mode 100644 src/cdk-experimental/listbox/listbox-option-directive.spec.ts create mode 100644 src/cdk-experimental/listbox/listbox-option-directive.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directive.spec.ts similarity index 100% rename from src/cdk-experimental/listbox/listbox-directives.spec.ts rename to src/cdk-experimental/listbox/listbox-directive.spec.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directive.ts similarity index 100% rename from src/cdk-experimental/listbox/listbox-directives.ts rename to src/cdk-experimental/listbox/listbox-directive.ts diff --git a/src/cdk-experimental/listbox/listbox-option-directive.spec.ts b/src/cdk-experimental/listbox/listbox-option-directive.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/cdk-experimental/listbox/listbox-option-directive.ts b/src/cdk-experimental/listbox/listbox-option-directive.ts new file mode 100644 index 000000000000..e69de29bb2d1 From b45c6df209c0bb69a1e499bd2d750f53603a18f5 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 13:08:07 -0400 Subject: [PATCH 03/33] build: Added required files to listbox directory. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts create mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts new file mode 100644 index 000000000000..e69de29bb2d1 From 966225f685eb635eec1f9d086cf3d7bd4d78d3d4 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 15:36:56 -0400 Subject: [PATCH 04/33] build: added listbox option directive and renamed listbox directive files. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts delete mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From 3ae54109b39d3a22e7f7b90607c1be58c851a6bf Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 13:08:07 -0400 Subject: [PATCH 05/33] build: Added required files to listbox directory. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts create mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts new file mode 100644 index 000000000000..e69de29bb2d1 From 46f537ee8f80bcc9ecddb14e5ee58fdba016e6ee Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 15:36:56 -0400 Subject: [PATCH 06/33] build: added listbox option directive and renamed listbox directive files. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts delete mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From aa3abdbb43da376fb64f38c1315419e00aab9581 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 13:08:07 -0400 Subject: [PATCH 07/33] build: Added required files to listbox directory. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts create mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts new file mode 100644 index 000000000000..e69de29bb2d1 From 6ea67c564af9f4eafdb7ce988d8a2d108a79edff Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 11 Jun 2020 15:36:56 -0400 Subject: [PATCH 08/33] build: added listbox option directive and renamed listbox directive files. --- src/cdk-experimental/listbox/listbox-directives.spec.ts | 0 src/cdk-experimental/listbox/listbox-directives.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/cdk-experimental/listbox/listbox-directives.spec.ts delete mode 100644 src/cdk-experimental/listbox/listbox-directives.ts diff --git a/src/cdk-experimental/listbox/listbox-directives.spec.ts b/src/cdk-experimental/listbox/listbox-directives.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-directives.ts b/src/cdk-experimental/listbox/listbox-directives.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From db690d6110cd615f47094d6174ea6e5d01a971a9 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 15 Jul 2020 13:06:37 -0400 Subject: [PATCH 09/33] feat(dev-app/listbox): added cdk listbox example to the dev-app. --- src/dev-app/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index ebf2ea97b04c..dee886f5c418 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -26,6 +26,7 @@ ng_module( "//src/dev-app/cdk-experimental-combobox", "//src/dev-app/cdk-experimental-listbox", "//src/dev-app/cdk-experimental-menu", + "//src/dev-app/cdk-experimental-listbox", "//src/dev-app/checkbox", "//src/dev-app/chips", "//src/dev-app/clipboard", From dea59154917de409e0cb2d5dc8e05f04beabe275 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 22 Jul 2020 10:56:23 -0400 Subject: [PATCH 10/33] fix(listbox): removed duplicate dep in dev-app build file. --- src/dev-app/BUILD.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index dee886f5c418..ebf2ea97b04c 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -26,7 +26,6 @@ ng_module( "//src/dev-app/cdk-experimental-combobox", "//src/dev-app/cdk-experimental-listbox", "//src/dev-app/cdk-experimental-menu", - "//src/dev-app/cdk-experimental-listbox", "//src/dev-app/checkbox", "//src/dev-app/chips", "//src/dev-app/clipboard", From 97eee8b8908464d11bf04602345d242fef5b08ab Mon Sep 17 00:00:00 2001 From: niels Date: Tue, 4 Aug 2020 11:17:12 -0400 Subject: [PATCH 11/33] fix(listbox): deleted unused files. --- src/cdk-experimental/listbox/listbox-directive.spec.ts | 0 src/cdk-experimental/listbox/listbox-directive.ts | 0 src/cdk-experimental/listbox/listbox-option-directive.spec.ts | 0 src/cdk-experimental/listbox/listbox-option-directive.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/cdk-experimental/listbox/listbox-directive.spec.ts delete mode 100644 src/cdk-experimental/listbox/listbox-directive.ts delete mode 100644 src/cdk-experimental/listbox/listbox-option-directive.spec.ts delete mode 100644 src/cdk-experimental/listbox/listbox-option-directive.ts diff --git a/src/cdk-experimental/listbox/listbox-directive.spec.ts b/src/cdk-experimental/listbox/listbox-directive.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-directive.ts b/src/cdk-experimental/listbox/listbox-directive.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-option-directive.spec.ts b/src/cdk-experimental/listbox/listbox-option-directive.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/cdk-experimental/listbox/listbox-option-directive.ts b/src/cdk-experimental/listbox/listbox-option-directive.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From 78bfc99961389d343daa11a57eac8a9e535b2709 Mon Sep 17 00:00:00 2001 From: niels Date: Mon, 10 Aug 2020 17:46:13 -0400 Subject: [PATCH 12/33] refactor(combobox): changed names and made coerceOpenActionProperty simpler for this PR. --- src/cdk-experimental/combobox/combobox.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 5be8db219563..4886d9868f98 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -75,7 +75,7 @@ describe('Combobox', () => { @Component({ template: ` - + + + Panel Content + `, +}) +class ComboboxCoercion { +} From 5d7f8d5f5ee0bcbddfc0793352325e7774c9f7a0 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 12 Aug 2020 23:55:48 -0400 Subject: [PATCH 20/33] feat(combobox): added logic to close popup on click outside of combobox. --- .../combobox/combobox-panel.ts | 3 ++ src/cdk-experimental/combobox/combobox.ts | 30 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts index 51e6aa55cd7b..176cf358377f 100644 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ b/src/cdk-experimental/combobox/combobox-panel.ts @@ -12,6 +12,9 @@ import {Directive, TemplateRef} from '@angular/core'; import {Subject} from 'rxjs'; @Directive({ + host: { + 'class': 'cdk-combobox-panel' + }, selector: 'ng-template[cdkComboboxPanel]', exportAs: 'cdkComboboxPanel', }) diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index c121220bff6a..6bd3325bacf4 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -14,7 +14,7 @@ import { AfterContentInit, Directive, ElementRef, - EventEmitter, + EventEmitter, HostListener, Input, OnDestroy, Optional, @@ -31,7 +31,6 @@ import { } from '@angular/cdk/overlay'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {Key} from "readline"; import {DOWN_ARROW} from "@angular/cdk/keycodes"; @@ -40,10 +39,10 @@ import {DOWN_ARROW} from "@angular/cdk/keycodes"; exportAs: 'cdkCombobox', host: { 'role': 'combobox', + 'class': 'cdk-combobox', '(click)': 'onClick()', '(focus)': 'onFocus()', '(keydown)': 'keydown($event)', - '(blur)': 'attemptClose()', '[attr.aria-disabled]': 'disabled', '[attr.aria-owns]': 'contentId', '[attr.aria-haspopup]': 'contentType', @@ -134,10 +133,19 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } } - attemptClose() { - if (this.contentType !== 'dialog') { - this.close(); + @HostListener('document:click', ['$event']) + _attemptClose(event: MouseEvent) { + if (this.isOpen()) { + let target = event.composedPath ? event.composedPath()[0] : event.target; + while (target instanceof Element) { + if (target.className.indexOf('cdk-combobox') !== -1) { + return; + } + target = target.parentElement; + } } + + this.close(); } /** Toggles the open state of the panel. */ @@ -153,6 +161,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.opened.next(); this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); this._overlayRef.attach(this._getPanelContent()); + this._panel?._templateRef.elementRef.nativeElement.focus(); } } @@ -161,6 +170,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { if (this.isOpen() && !this.disabled) { this.closed.next(); this._overlayRef.detach(); + this._elementRef.nativeElement.focus(); } } @@ -185,9 +195,13 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } private _setTextContent(content: T) { - if (typeof content === 'string') { - this._elementRef.nativeElement.textContent = `${content}`; + const contentArray = coerceArray(content); + const contentString = ''; + for (const token of contentArray) { + contentString.concat(`${token} `); } + + this._elementRef.nativeElement.textContent = contentString; } private _getOverlayConfig() { From e0ddc23196471cbf4eb94c84b7ac49913de34156 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 11:37:41 -0400 Subject: [PATCH 21/33] test(combobox): added many tests for combobox open action features. --- src/cdk-experimental/combobox/BUILD.bazel | 1 + .../combobox/combobox-panel.ts | 15 +- .../combobox/combobox.spec.ts | 332 ++++++++++++++++-- src/cdk-experimental/combobox/combobox.ts | 21 +- 4 files changed, 336 insertions(+), 33 deletions(-) diff --git a/src/cdk-experimental/combobox/BUILD.bazel b/src/cdk-experimental/combobox/BUILD.bazel index a7fd868955e5..f6de49a05921 100644 --- a/src/cdk-experimental/combobox/BUILD.bazel +++ b/src/cdk-experimental/combobox/BUILD.bazel @@ -25,6 +25,7 @@ ng_test_library( ), deps = [ ":combobox", + "//src/cdk/keycodes", "//src/cdk/testing/private", "@npm//@angular/platform-browser", ], diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts index 176cf358377f..1383c450f932 100644 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ b/src/cdk-experimental/combobox/combobox-panel.ts @@ -8,7 +8,7 @@ export type AriaHasPopupValue = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; -import {Directive, TemplateRef} from '@angular/core'; +import {Directive, ElementRef, TemplateRef} from '@angular/core'; import {Subject} from 'rxjs'; @Directive({ @@ -27,13 +27,22 @@ export class CdkComboboxPanel { contentId: string = ''; contentType: AriaHasPopupValue; - constructor(readonly _templateRef: TemplateRef) {} + constructor( + readonly _templateRef: TemplateRef, + readonly _elementRef: ElementRef + ) {} - /** Tells the parent combobox to closet he panel and sends back the content value. */ + /** Tells the parent combobox to close the panel and sends back the content value. */ closePanel(data?: T) { + console.log('closing panel'); + console.log(`data ${data}`); this.valueUpdated.next(data); } + focus() { + this._elementRef.nativeElement.focus(); + } + /** Registers the content's id and the content type with the panel. */ _registerContent(contentId: string, contentType: AriaHasPopupValue) { this.contentId = contentId; diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 073794390211..ee6409c508d2 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -1,22 +1,41 @@ -import {Component, DebugElement} from '@angular/core'; -import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import { + Component, + DebugElement, + Directive, ElementRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + ViewChild +} from '@angular/core'; +import {ComponentFixture, TestBed, async, fakeAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {CdkComboboxModule} from './combobox-module'; import {CdkCombobox} from './combobox'; -import {dispatchMouseEvent} from '@angular/cdk/testing/private'; +import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; +import {AriaHasPopupValue, CdkComboboxPanel} from "@angular/cdk-experimental/combobox/combobox-panel"; +import {DOWN_ARROW, ESCAPE} from "@angular/cdk/keycodes"; describe('Combobox', () => { describe('with a basic toggle trigger', () => { let fixture: ComponentFixture; + let testComponent: ComboboxToggle; let combobox: DebugElement; let comboboxInstance: CdkCombobox; let comboboxElement: HTMLElement; + let panel: DebugElement; + let panelInstance: DialogContent; + + let applyButton: DebugElement; + let applyButtonElement: HTMLElement; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle], + declarations: [ComboboxToggle, DialogContent], }).compileComponents(); })); @@ -24,6 +43,8 @@ describe('Combobox', () => { fixture = TestBed.createComponent(ComboboxToggle); fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + combobox = fixture.debugElement.query(By.directive(CdkCombobox)); comboboxInstance = combobox.injector.get>(CdkCombobox); comboboxElement = combobox.nativeElement; @@ -45,6 +66,49 @@ describe('Combobox', () => { expect(comboboxElement.getAttribute('aria-disabled')).toBe('false'); }); + it('should have aria-owns and aria-haspopup attributes', () => { + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + panel = fixture.debugElement.query(By.directive(DialogContent)); + panelInstance = panel.injector.get>(DialogContent); + + expect(comboboxElement.getAttribute('aria-owns')).toBe(panelInstance.dialogId); + expect(comboboxElement.getAttribute('aria-haspopup')).toBe('dialog'); + }); + + it('should update aria-expanded attribute upon toggle of panel', () => { + expect(comboboxElement.getAttribute('aria-expanded')).toBe('false'); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxElement.getAttribute('aria-expanded')).toBe('true'); + + comboboxInstance.close(); + fixture.detectChanges(); + + expect(comboboxElement.getAttribute('aria-expanded')).toBe('false'); + }); + + // fit('should toggle focus upon toggling the panel', () => { + // comboboxElement.focus(); + // testComponent.actions = 'toggle'; + // fixture.detectChanges(); + // + // expect(document.activeElement).toEqual(comboboxElement); + // + // dispatchMouseEvent(comboboxElement, 'click'); + // fixture.detectChanges(); + // + // expect(document.activeElement).toEqual(panelElement); + // + // dispatchMouseEvent(comboboxElement, 'click'); + // fixture.detectChanges(); + // + // expect(document.activeElement).toEqual(comboboxElement); + // }); + it('should have a panel that is closed by default', () => { expect(comboboxInstance.hasPanel()).toBeTrue(); expect(comboboxInstance.isOpen()).toBeFalse(); @@ -69,10 +133,49 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeFalse(); }); + + // fit('should update textContent on close of panel', () => { + // expect(comboboxInstance.isOpen()).toBeFalse(); + // + // dispatchMouseEvent(comboboxElement, 'click'); + // fixture.detectChanges(); + // + // expect(comboboxInstance.isOpen()).toBeTrue(); + // + // testComponent.input.nativeElement.value = 'testing input'; + // fixture.detectChanges(); + // + // applyButton = fixture.debugElement.query(By.css('#applyButton')); + // applyButtonElement = applyButton.nativeElement; + // + // dispatchMouseEvent(applyButtonElement, 'click'); + // fixture.detectChanges(); + // + // expect(comboboxInstance.isOpen()).toBeFalse(); + // expect(comboboxElement.textContent).toEqual('testing input'); + // }); + + it('should close panel on outside click', () => { + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + + const otherDiv = fixture.debugElement.query(By.css('#other-content')); + const otherDivElement = otherDiv.nativeElement; + + dispatchMouseEvent(otherDivElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + }); }); describe('with a coerce open action property function', () => { let fixture: ComponentFixture; + let testComponent: ComboboxToggle; let combobox: DebugElement; let comboboxInstance: CdkCombobox; @@ -81,7 +184,7 @@ describe('Combobox', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle], + declarations: [ComboboxToggle, DialogContent], }).compileComponents(); })); @@ -89,45 +192,234 @@ describe('Combobox', () => { fixture = TestBed.createComponent(ComboboxToggle); fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + combobox = fixture.debugElement.query(By.directive(CdkCombobox)); comboboxInstance = combobox.injector.get>(CdkCombobox); comboboxElement = combobox.nativeElement; }); - it('should have the combobox role', () => { - expect(comboboxElement.getAttribute('role')).toBe('combobox'); + it('should coerce single string into open action', () => { + const openActions = comboboxInstance.openActions; + expect(openActions.length).toBe(1); + expect(openActions[0]).toBe('click'); }); + it('should coerce actions separated by space', () => { + testComponent.actions = 'focus click'; + fixture.detectChanges(); + + const openActions = comboboxInstance.openActions; + expect(openActions.length).toBe(2); + expect(openActions[0]).toBe('focus'); + expect(openActions[1]).toBe('click'); + }); + + it('should coerce actions separated by comma', () => { + testComponent.actions = 'focus,click,downKey'; + fixture.detectChanges(); + + const openActions = comboboxInstance.openActions; + expect(openActions.length).toBe(3); + expect(openActions[0]).toBe('focus'); + expect(openActions[1]).toBe('click'); + expect(openActions[2]).toBe('downKey'); + }); + + it('should coerce actions separated by commas and spaces', () => { + testComponent.actions = 'focus click,downKey'; + fixture.detectChanges(); + + const openActions = comboboxInstance.openActions; + expect(openActions.length).toBe(3); + expect(openActions[0]).toBe('focus'); + expect(openActions[1]).toBe('click'); + expect(openActions[2]).toBe('downKey'); + }); + + it('should throw error when given invalid open action', () => { + expect(() => { + testComponent.actions = 'invalidAction'; + fixture.detectChanges(); + }).toThrow(); + }); }); + describe('with various open actions', () => { + let fixture: ComponentFixture; + let testComponent: ComboboxToggle; + + let combobox: DebugElement; + let comboboxInstance: CdkCombobox; + let comboboxElement: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CdkComboboxModule], + declarations: [ComboboxToggle, DialogContent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ComboboxToggle); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + combobox = fixture.debugElement.query(By.directive(CdkCombobox)); + comboboxInstance = combobox.injector.get>(CdkCombobox); + comboboxElement = combobox.nativeElement; + }); + + it('should open panel with focus open action', () => { + testComponent.actions = 'focus'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + comboboxElement.focus(); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + }); + + it('should open panel with click open action', () => { + testComponent.actions = 'click'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + }); + + it('should open panel with downKey open action', () => { + testComponent.actions = 'downKey'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchKeyboardEvent(comboboxElement, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + }); + + it('should toggle panel with toggle open action', () => { + testComponent.actions = 'toggle'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + }); + + it('should close panel on escape key', () => { + testComponent.actions = 'click'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + + dispatchKeyboardEvent(comboboxElement, 'keydown', ESCAPE); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + }); + + it('should handle multiple open actions', () => { + testComponent.actions = 'click downKey'; + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + + dispatchKeyboardEvent(comboboxElement, 'keydown', ESCAPE); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchKeyboardEvent(comboboxElement, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + }); + }); }); @Component({ template: ` + +
- Panel Content +
+ + +
`, }) class ComboboxToggle { + @ViewChild('input') input: ElementRef; + + actions: string = 'click'; } -@Component({ - template: ` - +export const PANEL = new InjectionToken('CdkComboboxPanel'); - - Panel Content - `, +let id = 0; + +@Directive({ + selector: '[dialogContent]', + exportAs: 'dialogContent', + host: { + 'role': 'role', + '[id]': 'dialogId' + } }) -class ComboboxCoercion { +export class DialogContent 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 6bd3325bacf4..16e217c3d095 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -31,7 +31,7 @@ import { } from '@angular/cdk/overlay'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {DOWN_ARROW} from "@angular/cdk/keycodes"; +import {DOWN_ARROW, ESCAPE, TAB} from "@angular/cdk/keycodes"; @Directive({ @@ -110,15 +110,15 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } onClick() { - if (this._openActions.includes('toggle')) { + if (this._openActions.indexOf('toggle') !== -1) { this.toggle(); - } else if (this._openActions.includes('click')) { + } else if (this._openActions.indexOf('click') !== -1) { this.open(); } } onFocus() { - if (!this._openActions.includes('focus')) { + if (this._openActions.indexOf('focus') === -1) { return; } @@ -128,8 +128,11 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { keydown(event: KeyboardEvent) { const {keyCode} = event; - if (keyCode === DOWN_ARROW && this._openActions.includes('downKey')) { + if (keyCode === DOWN_ARROW && this._openActions.indexOf('downKey') !== -1) { this.toggle(); + } else if (keyCode === ESCAPE) { + event.preventDefault(); + this.close(); } } @@ -161,7 +164,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.opened.next(); this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); this._overlayRef.attach(this._getPanelContent()); - this._panel?._templateRef.elementRef.nativeElement.focus(); + // this._panel?.focus(); } } @@ -170,7 +173,6 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { if (this.isOpen() && !this.disabled) { this.closed.next(); this._overlayRef.detach(); - this._elementRef.nativeElement.focus(); } } @@ -244,15 +246,14 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { if (typeof input === 'string') { const tokens = input.trim().split(/[ ,]+/); for (const token of tokens) { - if (!viableActions.includes(token)) { + if (viableActions.indexOf(token) === -1) { throw Error(`${token} is not a supported open action`); } - actions.push(input as OpenAction); + actions.push(token as OpenAction); } } else { actions = coerceArray(input); } - return actions; } From 7c5b802193dd4abd101573023a23895bd58cb403 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 13:20:17 -0400 Subject: [PATCH 22/33] test(combobox): added back two failing tests, focus and sending input value to combobox. --- .../combobox/combobox.spec.ts | 78 ++++++++++--------- src/cdk-experimental/combobox/combobox.ts | 2 +- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index ee6409c508d2..f7c7f43cea0b 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -28,6 +28,7 @@ describe('Combobox', () => { let panel: DebugElement; let panelInstance: DialogContent; + let panelElement: HTMLElement; let applyButton: DebugElement; let applyButtonElement: HTMLElement; @@ -91,23 +92,26 @@ describe('Combobox', () => { expect(comboboxElement.getAttribute('aria-expanded')).toBe('false'); }); - // fit('should toggle focus upon toggling the panel', () => { - // comboboxElement.focus(); - // testComponent.actions = 'toggle'; - // fixture.detectChanges(); - // - // expect(document.activeElement).toEqual(comboboxElement); - // - // dispatchMouseEvent(comboboxElement, 'click'); - // fixture.detectChanges(); - // - // expect(document.activeElement).toEqual(panelElement); - // - // dispatchMouseEvent(comboboxElement, 'click'); - // fixture.detectChanges(); - // - // expect(document.activeElement).toEqual(comboboxElement); - // }); + fit('should toggle focus upon toggling the panel', () => { + comboboxElement.focus(); + testComponent.actions = 'toggle'; + fixture.detectChanges(); + + expect(document.activeElement).toEqual(comboboxElement); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + panel = fixture.debugElement.query(By.directive(DialogContent)); + panelElement = panel.nativeElement; + + expect(document.activeElement).toEqual(panelElement); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(document.activeElement).not.toEqual(panelElement); + }); it('should have a panel that is closed by default', () => { expect(comboboxInstance.hasPanel()).toBeTrue(); @@ -134,26 +138,26 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeFalse(); }); - // fit('should update textContent on close of panel', () => { - // expect(comboboxInstance.isOpen()).toBeFalse(); - // - // dispatchMouseEvent(comboboxElement, 'click'); - // fixture.detectChanges(); - // - // expect(comboboxInstance.isOpen()).toBeTrue(); - // - // testComponent.input.nativeElement.value = 'testing input'; - // fixture.detectChanges(); - // - // applyButton = fixture.debugElement.query(By.css('#applyButton')); - // applyButtonElement = applyButton.nativeElement; - // - // dispatchMouseEvent(applyButtonElement, 'click'); - // fixture.detectChanges(); - // - // expect(comboboxInstance.isOpen()).toBeFalse(); - // expect(comboboxElement.textContent).toEqual('testing input'); - // }); + fit('should update textContent on close of panel', () => { + expect(comboboxInstance.isOpen()).toBeFalse(); + + dispatchMouseEvent(comboboxElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeTrue(); + + testComponent.input.nativeElement.value = 'testing input'; + fixture.detectChanges(); + + applyButton = fixture.debugElement.query(By.css('#applyButton')); + applyButtonElement = applyButton.nativeElement; + + dispatchMouseEvent(applyButtonElement, 'click'); + fixture.detectChanges(); + + expect(comboboxInstance.isOpen()).toBeFalse(); + expect(comboboxElement.textContent).toEqual('testing input'); + }); it('should close panel on outside click', () => { expect(comboboxInstance.isOpen()).toBeFalse(); diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 16e217c3d095..88d78222f58e 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -164,7 +164,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.opened.next(); this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); this._overlayRef.attach(this._getPanelContent()); - // this._panel?.focus(); + this._panel?.focus(); } } From 517b42b53beae21b7e8abdf405185bc4a35edffc Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 15:20:19 -0400 Subject: [PATCH 23/33] refactor(combobox): further tests on focus. --- src/cdk-experimental/combobox/combobox.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index f7c7f43cea0b..54f03e99f1a4 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -102,6 +102,8 @@ describe('Combobox', () => { dispatchMouseEvent(comboboxElement, 'click'); fixture.detectChanges(); + expect(comboboxInstance.isOpen()).toBeTrue(); + panel = fixture.debugElement.query(By.directive(DialogContent)); panelElement = panel.nativeElement; @@ -138,7 +140,7 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeFalse(); }); - fit('should update textContent on close of panel', () => { + it('should update textContent on close of panel', () => { expect(comboboxInstance.isOpen()).toBeFalse(); dispatchMouseEvent(comboboxElement, 'click'); From 60d0079adca325e020b1f8f968a6e255d207b0df Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 16:47:16 -0400 Subject: [PATCH 24/33] refactor(combobox): added tabindex to allow focusing on content, and fixed the setTextContent function. --- .../combobox/combobox-panel.ts | 6 ++-- .../combobox/combobox.spec.ts | 31 ++++++++++--------- src/cdk-experimental/combobox/combobox.ts | 17 +++++----- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts index 1383c450f932..5ce1829a2d46 100644 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ b/src/cdk-experimental/combobox/combobox-panel.ts @@ -34,13 +34,11 @@ export class CdkComboboxPanel { /** Tells the parent combobox to close the panel and sends back the content value. */ closePanel(data?: T) { - console.log('closing panel'); - console.log(`data ${data}`); this.valueUpdated.next(data); } - focus() { - this._elementRef.nativeElement.focus(); + focusContent() { + document.getElementById(this.contentId)?.focus(); } /** Registers the content's id and the content type with the panel. */ diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 54f03e99f1a4..37818a428caa 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -26,9 +26,9 @@ describe('Combobox', () => { let comboboxInstance: CdkCombobox; let comboboxElement: HTMLElement; - let panel: DebugElement; - let panelInstance: DialogContent; - let panelElement: HTMLElement; + let dialog: DebugElement; + let dialogInstance: DialogContent; + let dialogElement: HTMLElement; let applyButton: DebugElement; let applyButtonElement: HTMLElement; @@ -71,10 +71,10 @@ describe('Combobox', () => { dispatchMouseEvent(comboboxElement, 'click'); fixture.detectChanges(); - panel = fixture.debugElement.query(By.directive(DialogContent)); - panelInstance = panel.injector.get>(DialogContent); + dialog = fixture.debugElement.query(By.directive(DialogContent)); + dialogInstance = dialog.injector.get>(DialogContent); - expect(comboboxElement.getAttribute('aria-owns')).toBe(panelInstance.dialogId); + expect(comboboxElement.getAttribute('aria-owns')).toBe(dialogInstance.dialogId); expect(comboboxElement.getAttribute('aria-haspopup')).toBe('dialog'); }); @@ -92,7 +92,7 @@ describe('Combobox', () => { expect(comboboxElement.getAttribute('aria-expanded')).toBe('false'); }); - fit('should toggle focus upon toggling the panel', () => { + it('should toggle focus upon toggling the panel', () => { comboboxElement.focus(); testComponent.actions = 'toggle'; fixture.detectChanges(); @@ -104,15 +104,15 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeTrue(); - panel = fixture.debugElement.query(By.directive(DialogContent)); - panelElement = panel.nativeElement; + dialog = fixture.debugElement.query(By.directive(DialogContent)); + dialogElement = dialog.nativeElement; - expect(document.activeElement).toEqual(panelElement); + expect(document.activeElement).toBe(dialogElement); dispatchMouseEvent(comboboxElement, 'click'); fixture.detectChanges(); - expect(document.activeElement).not.toEqual(panelElement); + expect(document.activeElement).not.toEqual(dialogElement); }); it('should have a panel that is closed by default', () => { @@ -148,7 +148,7 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeTrue(); - testComponent.input.nativeElement.value = 'testing input'; + testComponent.inputElement.nativeElement.value = 'testing input'; fixture.detectChanges(); applyButton = fixture.debugElement.query(By.css('#applyButton')); @@ -389,7 +389,7 @@ describe('Combobox', () => { `, }) class ComboboxToggle { - @ViewChild('input') input: ElementRef; + @ViewChild('input') inputElement: ElementRef; actions: string = 'click'; } @@ -402,8 +402,9 @@ let id = 0; selector: '[dialogContent]', exportAs: 'dialogContent', host: { - 'role': 'role', - '[id]': 'dialogId' + '[attr.role]': 'role', + '[id]': 'dialogId', + 'tabIndex': '-1' } }) export class DialogContent implements OnInit { diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 88d78222f58e..d58ed0a00623 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -46,7 +46,8 @@ import {DOWN_ARROW, ESCAPE, TAB} from "@angular/cdk/keycodes"; '[attr.aria-disabled]': 'disabled', '[attr.aria-owns]': 'contentId', '[attr.aria-haspopup]': 'contentType', - '[attr.aria-expanded]': 'isOpen()' + '[attr.aria-expanded]': 'isOpen()', + '[attr.tabindex]': '_getTabIndex()' } }) export class CdkCombobox implements OnDestroy, AfterContentInit { @@ -164,7 +165,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.opened.next(); this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); this._overlayRef.attach(this._getPanelContent()); - this._panel?.focus(); + this._panel?.focusContent(); } } @@ -186,7 +187,12 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { return !!this.panel; } + _getTabIndex(): string | null { + return this.disabled ? null : '0'; + } + private _setComboboxValue(value: T) { + const valueChanged = (this.value !== value); this.value = value; @@ -198,12 +204,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { private _setTextContent(content: T) { const contentArray = coerceArray(content); - const contentString = ''; - for (const token of contentArray) { - contentString.concat(`${token} `); - } - - this._elementRef.nativeElement.textContent = contentString; + this._elementRef.nativeElement.textContent = contentArray.join(' '); } private _getOverlayConfig() { From 64dd2f6c7f452169baf306bf405cd27be8b1e916 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 17:52:19 -0400 Subject: [PATCH 25/33] fix(combobox): removed separate coerceArray import. --- src/cdk-experimental/combobox/combobox.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index d58ed0a00623..07dbb27a4da4 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -5,7 +5,6 @@ * 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 {coerceArray} from "@angular/cdk/coercion/array"; export type OpenAction = 'focus' | 'click' | 'downKey' | 'toggle'; export type OpenActionInput = OpenAction | OpenAction[] | string | null | undefined; @@ -30,8 +29,8 @@ import { OverlayRef } from '@angular/cdk/overlay'; import {Directionality} from '@angular/cdk/bidi'; -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {DOWN_ARROW, ESCAPE, TAB} from "@angular/cdk/keycodes"; +import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion'; +import {DOWN_ARROW, ESCAPE} from "@angular/cdk/keycodes"; @Directive({ From 7ffc07ffc2f73154f2cf2030ce89be7411ffc8ca Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 19:25:24 -0400 Subject: [PATCH 26/33] fix(combobox): lint errors. --- src/cdk-experimental/combobox/combobox.spec.ts | 11 +++++------ src/cdk-experimental/combobox/combobox.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 37818a428caa..1df03312d75e 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -9,13 +9,15 @@ import { Optional, ViewChild } from '@angular/core'; -import {ComponentFixture, TestBed, async, fakeAsync} from '@angular/core/testing'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {CdkComboboxModule} from './combobox-module'; import {CdkCombobox} from './combobox'; import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; -import {AriaHasPopupValue, CdkComboboxPanel} from "@angular/cdk-experimental/combobox/combobox-panel"; -import {DOWN_ARROW, ESCAPE} from "@angular/cdk/keycodes"; +import { + AriaHasPopupValue, + CdkComboboxPanel} from '@angular/cdk-experimental/combobox/combobox-panel'; +import {DOWN_ARROW, ESCAPE} from '@angular/cdk/keycodes'; describe('Combobox', () => { describe('with a basic toggle trigger', () => { @@ -185,7 +187,6 @@ describe('Combobox', () => { let combobox: DebugElement; let comboboxInstance: CdkCombobox; - let comboboxElement: HTMLElement; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -202,7 +203,6 @@ describe('Combobox', () => { combobox = fixture.debugElement.query(By.directive(CdkCombobox)); comboboxInstance = combobox.injector.get>(CdkCombobox); - comboboxElement = combobox.nativeElement; }); it('should coerce single string into open action', () => { @@ -380,7 +380,6 @@ describe('Combobox', () => {
-
diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 07dbb27a4da4..9d4ccc4cd210 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -30,7 +30,7 @@ import { } from '@angular/cdk/overlay'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion'; -import {DOWN_ARROW, ESCAPE} from "@angular/cdk/keycodes"; +import {DOWN_ARROW, ESCAPE} from '@angular/cdk/keycodes'; @Directive({ From 1b28ca71528614641caedadb9dfa0848f5bfcd19 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 20:39:42 -0400 Subject: [PATCH 27/33] refactor(combobox): only throw error in coercing open action in dev mode, condense click and focus handlers into one. --- .../combobox/combobox-panel.ts | 5 +- src/cdk-experimental/combobox/combobox.ts | 58 ++++++++----------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts index 5ce1829a2d46..7925e1a97b6c 100644 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ b/src/cdk-experimental/combobox/combobox-panel.ts @@ -28,8 +28,7 @@ export class CdkComboboxPanel { contentType: AriaHasPopupValue; constructor( - readonly _templateRef: TemplateRef, - readonly _elementRef: ElementRef + readonly _templateRef: TemplateRef ) {} /** Tells the parent combobox to close the panel and sends back the content value. */ @@ -37,7 +36,9 @@ export class CdkComboboxPanel { 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(); } diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 9d4ccc4cd210..1c7e163312ef 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -6,6 +6,7 @@ * 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; @@ -14,7 +15,7 @@ import { Directive, ElementRef, EventEmitter, HostListener, - Input, + Input, isDevMode, OnDestroy, Optional, Output, ViewContainerRef @@ -32,6 +33,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion'; import {DOWN_ARROW, ESCAPE} from '@angular/cdk/keycodes'; +const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle']; @Directive({ selector: '[cdkCombobox]', @@ -39,9 +41,10 @@ import {DOWN_ARROW, ESCAPE} from '@angular/cdk/keycodes'; host: { 'role': 'combobox', 'class': 'cdk-combobox', - '(click)': 'onClick()', - '(focus)': 'onFocus()', + '(click)': '_handleInteractions("click")', + '(focus)': '_handleInteractions("focus")', '(keydown)': 'keydown($event)', + '(document:click)': '_attemptClose($event)', '[attr.aria-disabled]': 'disabled', '[attr.aria-owns]': 'contentId', '[attr.aria-haspopup]': 'contentType', @@ -109,22 +112,6 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.panelValueChanged.complete(); } - onClick() { - if (this._openActions.indexOf('toggle') !== -1) { - this.toggle(); - } else if (this._openActions.indexOf('click') !== -1) { - this.open(); - } - } - - onFocus() { - if (this._openActions.indexOf('focus') === -1) { - return; - } - - this.open(); - } - keydown(event: KeyboardEvent) { const {keyCode} = event; @@ -136,7 +123,20 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } } - @HostListener('document:click', ['$event']) + _handleInteractions(interaction: string) { + if (interaction === 'click') { + if (this._openActions.indexOf('toggle') !== -1) { + this.toggle(); + } else if (this._openActions.indexOf('click') !== -1) { + this.open(); + } + } else if (interaction === 'focus') { + if (this._openActions.indexOf('focus') !== -1) { + this.open(); + } + } + } + _attemptClose(event: MouseEvent) { if (this.isOpen()) { let target = event.composedPath ? event.composedPath()[0] : event.target; @@ -240,21 +240,11 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } private _coerceOpenActionProperty(input: string | OpenAction[]): OpenAction[] { - let actions: OpenAction[] = []; - const viableActions = ['focus', 'click', 'downKey', 'toggle']; - - if (typeof input === 'string') { - const tokens = input.trim().split(/[ ,]+/); - for (const token of tokens) { - if (viableActions.indexOf(token) === -1) { - throw Error(`${token} is not a supported open action`); - } - actions.push(token as OpenAction); - } - } else { - actions = coerceArray(input); + let actions = typeof input === 'string' ? input.trim().split(/[ ,]+/) : input; + if (isDevMode() && actions.some(a => allowedOpenActions.indexOf(a) === -1)) { + throw Error(`${input} is not a support open action for CdkCombobox`); } - return actions; + return actions as OpenAction[]; } static ngAcceptInputType_openActions: OpenActionInput; From d04feb9a32e2b7d41a1a2db4de9527ab38a41c32 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 20:40:16 -0400 Subject: [PATCH 28/33] nit(combobox): removed whitespace. --- src/cdk-experimental/combobox/combobox.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 1df03312d75e..4c71754b24f2 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -378,7 +378,6 @@ describe('Combobox', () => { [openActions]="actions"> No Value -
From 4671b892aadd1a935083b519fa9ac0bb80216807 Mon Sep 17 00:00:00 2001 From: niels Date: Thu, 13 Aug 2020 20:52:08 -0400 Subject: [PATCH 29/33] fix(combobox): removed unused imports. --- src/cdk-experimental/combobox/combobox-panel.ts | 2 +- src/cdk-experimental/combobox/combobox.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-panel.ts b/src/cdk-experimental/combobox/combobox-panel.ts index 7925e1a97b6c..cf2dedef638c 100644 --- a/src/cdk-experimental/combobox/combobox-panel.ts +++ b/src/cdk-experimental/combobox/combobox-panel.ts @@ -8,7 +8,7 @@ export type AriaHasPopupValue = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; -import {Directive, ElementRef, TemplateRef} from '@angular/core'; +import {Directive, TemplateRef} from '@angular/core'; import {Subject} from 'rxjs'; @Directive({ diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 1c7e163312ef..2543081c0117 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -14,7 +14,7 @@ import { AfterContentInit, Directive, ElementRef, - EventEmitter, HostListener, + EventEmitter, Input, isDevMode, OnDestroy, Optional, From 75c78255ed63c012ca517c43a4dc8fd4f076db04 Mon Sep 17 00:00:00 2001 From: niels Date: Fri, 14 Aug 2020 12:50:57 -0400 Subject: [PATCH 30/33] feat(combobox): added combobox popup directive to handle registering panel content with panel. --- .../combobox/combobox-module.ts | 3 +- .../combobox/combobox-popup.ts | 55 +++++++++++++++++++ src/cdk-experimental/combobox/combobox.ts | 2 +- src/cdk-experimental/combobox/public-api.ts | 1 + 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/cdk-experimental/combobox/combobox-popup.ts diff --git a/src/cdk-experimental/combobox/combobox-module.ts b/src/cdk-experimental/combobox/combobox-module.ts index 58e1cf7a911a..0225b23bb751 100644 --- a/src/cdk-experimental/combobox/combobox-module.ts +++ b/src/cdk-experimental/combobox/combobox-module.ts @@ -10,8 +10,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]; +const EXPORTED_DECLARATIONS = [CdkCombobox, CdkComboboxPanel, CdkComboboxPopup]; @NgModule({ imports: [OverlayModule], exports: EXPORTED_DECLARATIONS, diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts new file mode 100644 index 000000000000..3457eaf11ff1 --- /dev/null +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -0,0 +1,55 @@ +/** + * @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 nextId = 0; + +@Directive({ + selector: '[cdkComboboxPopup]', + exportAs: 'cdkComboboxPopup', + host: { + 'class': 'cdk-combobox-popup', + '[attr.role]': 'role', + '[id]': 'id', + 'tabindex': '-1' + } +}) +export class CdkComboboxPopup implements OnInit { + @Input() + get role(): AriaHasPopupValue { + return this._role; + } + set role(value: AriaHasPopupValue) { + this._role = value; + } + private _role: AriaHasPopupValue = 'dialog'; + + @Input() id = `cdk-combobox-popup-${nextId++}`; + + @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.id, this._role); + } else { + this._parentPanel._registerContent(this.id, this._role); + } + } +} diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index 2543081c0117..c0034ab29f76 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -116,7 +116,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { const {keyCode} = event; if (keyCode === DOWN_ARROW && this._openActions.indexOf('downKey') !== -1) { - this.toggle(); + this.open(); } else if (keyCode === ESCAPE) { event.preventDefault(); this.close(); diff --git a/src/cdk-experimental/combobox/public-api.ts b/src/cdk-experimental/combobox/public-api.ts index ddc82a9468b3..07eadcfa2ec1 100644 --- a/src/cdk-experimental/combobox/public-api.ts +++ b/src/cdk-experimental/combobox/public-api.ts @@ -9,3 +9,4 @@ export * from './combobox-module'; export * from './combobox'; export * from './combobox-panel'; +export * from './combobox-popup'; From 065adf19103029fd6ba13e58c85d3d610fe8676f Mon Sep 17 00:00:00 2001 From: niels Date: Fri, 14 Aug 2020 12:51:35 -0400 Subject: [PATCH 31/33] nit(combobox): changed double quotes to single quotes. --- src/cdk-experimental/combobox/combobox-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk-experimental/combobox/combobox-module.ts b/src/cdk-experimental/combobox/combobox-module.ts index 0225b23bb751..b8ac8c7782bc 100644 --- a/src/cdk-experimental/combobox/combobox-module.ts +++ b/src/cdk-experimental/combobox/combobox-module.ts @@ -10,7 +10,7 @@ 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"; +import {CdkComboboxPopup} from './combobox-popup'; const EXPORTED_DECLARATIONS = [CdkCombobox, CdkComboboxPanel, CdkComboboxPopup]; @NgModule({ From ddff626c5089995ee3d88494ceeb955f4ac3f6e4 Mon Sep 17 00:00:00 2001 From: niels Date: Fri, 14 Aug 2020 12:55:31 -0400 Subject: [PATCH 32/33] nit(combobox): changed class names and parameter types. --- src/cdk-experimental/combobox/combobox.spec.ts | 16 ++++++++-------- src/cdk-experimental/combobox/combobox.ts | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox.spec.ts b/src/cdk-experimental/combobox/combobox.spec.ts index 4c71754b24f2..cba1ab7c1562 100644 --- a/src/cdk-experimental/combobox/combobox.spec.ts +++ b/src/cdk-experimental/combobox/combobox.spec.ts @@ -29,7 +29,7 @@ describe('Combobox', () => { let comboboxElement: HTMLElement; let dialog: DebugElement; - let dialogInstance: DialogContent; + let dialogInstance: FakeDialogContent; let dialogElement: HTMLElement; let applyButton: DebugElement; @@ -38,7 +38,7 @@ describe('Combobox', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, DialogContent], + declarations: [ComboboxToggle, FakeDialogContent], }).compileComponents(); })); @@ -73,8 +73,8 @@ describe('Combobox', () => { dispatchMouseEvent(comboboxElement, 'click'); fixture.detectChanges(); - dialog = fixture.debugElement.query(By.directive(DialogContent)); - dialogInstance = dialog.injector.get>(DialogContent); + dialog = fixture.debugElement.query(By.directive(FakeDialogContent)); + dialogInstance = dialog.injector.get>(FakeDialogContent); expect(comboboxElement.getAttribute('aria-owns')).toBe(dialogInstance.dialogId); expect(comboboxElement.getAttribute('aria-haspopup')).toBe('dialog'); @@ -106,7 +106,7 @@ describe('Combobox', () => { expect(comboboxInstance.isOpen()).toBeTrue(); - dialog = fixture.debugElement.query(By.directive(DialogContent)); + dialog = fixture.debugElement.query(By.directive(FakeDialogContent)); dialogElement = dialog.nativeElement; expect(document.activeElement).toBe(dialogElement); @@ -191,7 +191,7 @@ describe('Combobox', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, DialogContent], + declarations: [ComboboxToggle, FakeDialogContent], }).compileComponents(); })); @@ -262,7 +262,7 @@ describe('Combobox', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CdkComboboxModule], - declarations: [ComboboxToggle, DialogContent], + declarations: [ComboboxToggle, FakeDialogContent], }).compileComponents(); })); @@ -405,7 +405,7 @@ let id = 0; 'tabIndex': '-1' } }) -export class DialogContent implements OnInit { +export class FakeDialogContent implements OnInit { dialogId = `dialog-${id++}`; role = 'dialog'; diff --git a/src/cdk-experimental/combobox/combobox.ts b/src/cdk-experimental/combobox/combobox.ts index c0034ab29f76..11c06a663ce6 100644 --- a/src/cdk-experimental/combobox/combobox.ts +++ b/src/cdk-experimental/combobox/combobox.ts @@ -43,7 +43,7 @@ const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle']; 'class': 'cdk-combobox', '(click)': '_handleInteractions("click")', '(focus)': '_handleInteractions("focus")', - '(keydown)': 'keydown($event)', + '(keydown)': '_keydown($event)', '(document:click)': '_attemptClose($event)', '[attr.aria-disabled]': 'disabled', '[attr.aria-owns]': 'contentId', @@ -112,7 +112,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { this.panelValueChanged.complete(); } - keydown(event: KeyboardEvent) { + _keydown(event: KeyboardEvent) { const {keyCode} = event; if (keyCode === DOWN_ARROW && this._openActions.indexOf('downKey') !== -1) { @@ -123,7 +123,7 @@ export class CdkCombobox implements OnDestroy, AfterContentInit { } } - _handleInteractions(interaction: string) { + _handleInteractions(interaction: OpenAction) { if (interaction === 'click') { if (this._openActions.indexOf('toggle') !== -1) { this.toggle(); From 4ce24aa5899cf2d205a4cb5dc7b40e0f00425189 Mon Sep 17 00:00:00 2001 From: niels Date: Fri, 14 Aug 2020 13:19:28 -0400 Subject: [PATCH 33/33] fix(combobox): changed import to relative import. --- src/cdk-experimental/combobox/combobox-popup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts index 3457eaf11ff1..f00b14d7fcec 100644 --- a/src/cdk-experimental/combobox/combobox-popup.ts +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -7,7 +7,7 @@ */ import {Directive, Inject, InjectionToken, Input, OnInit, Optional} from '@angular/core'; -import {AriaHasPopupValue, CdkComboboxPanel} from '@angular/cdk-experimental/combobox'; +import {AriaHasPopupValue, CdkComboboxPanel} from './combobox-panel'; export const PANEL = new InjectionToken('CdkComboboxPanel');