diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 068b346be6ed..5ab52e5b28f3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -89,6 +89,7 @@ # Material experimental package /src/material-experimental/* @jelbourn +/src/material-experimental/mdc-autocomplete/** @crisbeto /src/material-experimental/mdc-button/** @andrewseguin /src/material-experimental/mdc-card/** @mmalerba /src/material-experimental/mdc-checkbox/** @mmalerba diff --git a/src/material-experimental/mdc-autocomplete/BUILD.bazel b/src/material-experimental/mdc-autocomplete/BUILD.bazel new file mode 100644 index 000000000000..fb16523f53cd --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/BUILD.bazel @@ -0,0 +1,99 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library") +load("//src/e2e-app:test_suite.bzl", "e2e_test_suite") +load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite", "ts_library") + +ng_module( + name = "mdc-autocomplete", + srcs = glob( + ["**/*.ts"], + exclude = [ + "**/*.spec.ts", + "harness/**", + ], + ), + assets = [ + ":autocomplete_scss", + ] + glob(["**/*.html"]), + module_name = "@angular/material-experimental/mdc-autocomplete", + deps = [ + "//src/material/core", + ], +) + +ts_library( + name = "harness", + srcs = glob( + ["harness/**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src/cdk-experimental/testing", + "//src/cdk/coercion", + ], +) + +sass_library( + name = "mdc_autocomplete_scss_lib", + srcs = glob(["**/_*.scss"]), + deps = [ + "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib", + "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib", + "//src/material/core:core_scss_lib", + ], +) + +sass_binary( + name = "autocomplete_scss", + src = "autocomplete.scss", + include_paths = [ + "external/npm/node_modules", + ], + deps = [ + "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib", + "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib", + "//src/material/core:all_themes", + ], +) + +ng_test_library( + name = "autocomplete_tests_lib", + srcs = [ + "harness/autocomplete-harness.spec.ts", + ], + deps = [ + ":harness", + ":mdc-autocomplete", + "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/testbed", + "//src/cdk/platform", + "//src/cdk/testing", + "//src/material/autocomplete", + "@npm//@angular/platform-browser", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":autocomplete_tests_lib", + "//src/material-experimental:mdc_require_config.js", + ], +) + +ng_e2e_test_library( + name = "e2e_test_sources", + srcs = glob(["**/*.e2e.spec.ts"]), + deps = [ + "//src/cdk/private/testing/e2e", + ], +) + +e2e_test_suite( + name = "e2e_tests", + deps = [ + ":e2e_test_sources", + "//src/cdk/private/testing/e2e", + ], +) diff --git a/src/material-experimental/mdc-autocomplete/README.md b/src/material-experimental/mdc-autocomplete/README.md new file mode 100644 index 000000000000..cf873b4c072b --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/README.md @@ -0,0 +1 @@ + diff --git a/src/material-experimental/mdc-autocomplete/_mdc-autocomplete.scss b/src/material-experimental/mdc-autocomplete/_mdc-autocomplete.scss new file mode 100644 index 000000000000..67218ecb6a68 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/_mdc-autocomplete.scss @@ -0,0 +1,13 @@ +@import '../mdc-helpers/mdc-helpers'; + +@mixin mat-autocomplete-theme-mdc($theme) { + @include mat-using-mdc-theme($theme) { + // TODO: implement MDC-based autocomplete. + } +} + +@mixin mat-autocomplete-typography-mdc($config) { + @include mat-using-mdc-typography($config) { + // TODO: implement MDC-based autocomplete. + } +} diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.e2e.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.e2e.spec.ts new file mode 100644 index 000000000000..da2033b44a86 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/autocomplete.e2e.spec.ts @@ -0,0 +1 @@ +// TODO: copy tests from existing mat-autocomplete, update as necessary to fix. diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.scss b/src/material-experimental/mdc-autocomplete/autocomplete.scss new file mode 100644 index 000000000000..08c2291905e4 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/autocomplete.scss @@ -0,0 +1 @@ +// TODO: implement MDC-based autocomplete diff --git a/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness-filters.ts b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness-filters.ts new file mode 100644 index 000000000000..249805c9cb83 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness-filters.ts @@ -0,0 +1,12 @@ +/** + * @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 AutocompleteHarnessFilters = { + id?: string; + name?: string, +}; diff --git a/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.spec.ts b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.spec.ts new file mode 100644 index 000000000000..4df8dca68247 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.spec.ts @@ -0,0 +1,186 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatAutocompleteModule as MatMdcAutocompleteModule} from '../index'; +import {MatAutocompleteHarness} from './autocomplete-harness'; +import {MatAutocompleteHarness as MatMdcAutocompleteHarness} from './mdc-autocomplete-harness'; + +let fixture: ComponentFixture; +let loader: HarnessLoader; +let harness: typeof MatAutocompleteHarness; + +describe('MatAutocompleteHarness', () => { + describe('non-MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatAutocompleteModule], + declarations: [AutocompleteHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(AutocompleteHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + harness = MatAutocompleteHarness; + }); + + runTests(); + }); + + describe('MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatMdcAutocompleteModule], + declarations: [AutocompleteHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(AutocompleteHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + // Public APIs are the same as MatAutocompleteHarness, but cast + // is necessary because of different private fields. + harness = MatMdcAutocompleteHarness as any; + }); + + // TODO: enable after MDC autocomplete is implemented + // runTests(); + }); +}); + +/** Shared tests to run on both the original and MDC-based autocomplete. */ +function runTests() { + it('should load all autocomplete harnesses', async () => { + const inputs = await loader.getAllHarnesses(harness); + expect(inputs.length).toBe(5); + }); + + it('should be able to get text inside the input', async () => { + const input = await loader.getHarness(harness.with({id: 'prefilled'})); + expect(await input.getText()).toBe('Prefilled value'); + }); + + it('should get disabled state', async () => { + const enabled = await loader.getHarness(harness.with({id: 'plain'})); + const disabled = await loader.getHarness(harness.with({id: 'disabled'})); + + expect(await enabled.isDisabled()).toBe(false); + expect(await disabled.isDisabled()).toBe(true); + }); + + it('should focus and blur an input', async () => { + const input = await loader.getHarness(harness.with({id: 'plain'})); + expect(getActiveElementId()).not.toBe('plain'); + await input.focus(); + expect(getActiveElementId()).toBe('plain'); + await input.blur(); + expect(getActiveElementId()).not.toBe('plain'); + }); + + it('should be able to type in an input', async () => { + const input = await loader.getHarness(harness.with({id: 'plain'})); + await input.enterText('Hello there'); + expect(await input.getText()).toBe('Hello there'); + }); + + it('should be able to get the autocomplete panel', async () => { + const input = await loader.getHarness(harness.with({id: 'plain'})); + await input.focus(); + expect(await input.getPanel()).toBeTruthy(); + }); + + it('should be able to get the autocomplete panel options', async () => { + const input = await loader.getHarness(harness.with({id: 'plain'})); + await input.focus(); + const options = await input.getOptions(); + + expect(options.length).toBe(11); + expect(await options[5].text()).toBe('New York'); + }); + + it('should be able to get the autocomplete panel groups', async () => { + const input = await loader.getHarness(harness.with({id: 'grouped'})); + await input.focus(); + const groups = await input.getOptionGroups(); + const options = await input.getOptions(); + + expect(groups.length).toBe(3); + expect(options.length).toBe(11); + }); + + it('should be able to get the autocomplete panel', async () => { + // Focusing without any options will render the panel, but it'll be invisible. + fixture.componentInstance.states = []; + fixture.detectChanges(); + + const input = await loader.getHarness(harness.with({id: 'plain'})); + await input.focus(); + expect(await input.isPanelVisible()).toBe(false); + }); + + it('should be able to get whether the autocomplete is open', async () => { + const input = await loader.getHarness(harness.with({id: 'plain'})); + + expect(await input.isOpen()).toBe(false); + await input.focus(); + expect(await input.isOpen()).toBe(true); + }); + +} + +function getActiveElementId() { + return document.activeElement ? document.activeElement.id : ''; +} + +@Component({ + template: ` + + {{ state.name }} + + + + + {{ state.name }} + + + + + + + + + ` +}) +class AutocompleteHarnessTest { + states = [ + {code: 'AL', name: 'Alabama'}, + {code: 'CA', name: 'California'}, + {code: 'FL', name: 'Florida'}, + {code: 'KS', name: 'Kansas'}, + {code: 'MA', name: 'Massachusetts'}, + {code: 'NY', name: 'New York'}, + {code: 'OR', name: 'Oregon'}, + {code: 'PA', name: 'Pennsylvania'}, + {code: 'TN', name: 'Tennessee'}, + {code: 'VA', name: 'Virginia'}, + {code: 'WY', name: 'Wyoming'}, + ]; + + stateGroups = [ + { + name: 'One', + states: this.states.slice(0, 3) + }, + { + name: 'Two', + states: this.states.slice(3, 7) + }, + { + name: 'Three', + states: this.states.slice(7) + } + ]; +} + diff --git a/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.ts b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.ts new file mode 100644 index 000000000000..eb1995039a41 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/harness/autocomplete-harness.ts @@ -0,0 +1,98 @@ +/** + * @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 {ComponentHarness, HarnessPredicate, TestElement} from '@angular/cdk-experimental/testing'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {AutocompleteHarnessFilters} from './autocomplete-harness-filters'; + +/** Selector for the autocomplete panel. */ +const PANEL_SELECTOR = '.mat-autocomplete-panel'; + +/** + * Harness for interacting with a standard mat-autocomplete in tests. + * @dynamic + */ +export class MatAutocompleteHarness extends ComponentHarness { + private _documentRootLocator = this.documentRootLocatorFactory(); + private _panel = this._documentRootLocator.locatorFor(PANEL_SELECTOR); + private _optionalPanel = this._documentRootLocator.locatorForOptional(PANEL_SELECTOR); + private _options = this._documentRootLocator.locatorForAll(`${PANEL_SELECTOR} .mat-option`); + private _groups = this._documentRootLocator.locatorForAll(`${PANEL_SELECTOR} .mat-optgroup`); + + static hostSelector = '.mat-autocomplete-trigger'; + + /** + * Gets a `HarnessPredicate` that can be used to search for an autocomplete with + * specific attributes. + * @param options Options for narrowing the search: + * - `name` finds an autocomplete with a specific name. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: AutocompleteHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatAutocompleteHarness) + .addOption('name', options.name, + async (harness, name) => (await harness.getAttribute('name')) === name) + .addOption('id', options.id, + async (harness, id) => (await harness.getAttribute('id')) === id); + } + + async getAttribute(attributeName: string): Promise { + return (await this.host()).getAttribute(attributeName); + } + + /** Gets a boolean promise indicating if the autocomplete input is disabled. */ + async isDisabled(): Promise { + const disabled = (await this.host()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets a promise for the autocomplete's text. */ + async getText(): Promise { + return (await this.host()).getProperty('value'); + } + + /** Focuses the input and returns a void promise that indicates when the action is complete. */ + async focus(): Promise { + return (await this.host()).focus(); + } + + /** Blurs the input and returns a void promise that indicates when the action is complete. */ + async blur(): Promise { + return (await this.host()).blur(); + } + + /** Enters text into the autocomplete. */ + async enterText(value: string): Promise { + return (await this.host()).sendKeys(value); + } + + /** Gets the autocomplete panel. */ + async getPanel(): Promise { + return this._panel(); + } + + /** Gets the options inside the autocomplete panel. */ + async getOptions(): Promise { + return this._options(); + } + + /** Gets the groups of options inside the panel. */ + async getOptionGroups(): Promise { + return this._groups(); + } + + /** Gets whether the autocomplete panel is visible. */ + async isPanelVisible(): Promise { + return (await this._panel()).hasClass('mat-autocomplete-visible'); + } + + /** Gets whether the autocomplete is open. */ + async isOpen(): Promise { + return !!(await this._optionalPanel()); + } +} diff --git a/src/material-experimental/mdc-autocomplete/harness/mdc-autocomplete-harness.ts b/src/material-experimental/mdc-autocomplete/harness/mdc-autocomplete-harness.ts new file mode 100644 index 000000000000..b2b7291af27c --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/harness/mdc-autocomplete-harness.ts @@ -0,0 +1,18 @@ +/** + * @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 {ComponentHarness} from '@angular/cdk-experimental/testing'; + + +/** + * Harness for interacting with a MDC-based mat-autocomplete in tests. + * @dynamic + */ +export class MatAutocompleteHarness extends ComponentHarness { + // TODO: implement once MDC autocomplete is done. +} diff --git a/src/material-experimental/mdc-autocomplete/index.ts b/src/material-experimental/mdc-autocomplete/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/index.ts @@ -0,0 +1,9 @@ +/** + * @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 * from './public-api'; diff --git a/src/material-experimental/mdc-autocomplete/module.ts b/src/material-experimental/mdc-autocomplete/module.ts new file mode 100644 index 000000000000..37655c4efb8f --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/module.ts @@ -0,0 +1,18 @@ +/** + * @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 {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {MatCommonModule} from '@angular/material/core'; + +@NgModule({ + imports: [MatCommonModule, CommonModule], + exports: [MatCommonModule], +}) +export class MatAutocompleteModule { +} diff --git a/src/material-experimental/mdc-autocomplete/public-api.ts b/src/material-experimental/mdc-autocomplete/public-api.ts new file mode 100644 index 000000000000..508adc834fb3 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/public-api.ts @@ -0,0 +1,9 @@ +/** + * @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 * from './module'; diff --git a/src/material-experimental/mdc-autocomplete/tsconfig-build.json b/src/material-experimental/mdc-autocomplete/tsconfig-build.json new file mode 100644 index 000000000000..d476c940b7a9 --- /dev/null +++ b/src/material-experimental/mdc-autocomplete/tsconfig-build.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig-build", + "files": [ + "public-api.ts", + "../typings.d.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/material-experimental/mdc-autocomplete", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 2b1dead71518..2ab382278601 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -105,6 +105,7 @@ export function getMatAutocompleteMissingPanelError(): Error { @Directive({ selector: `input[matAutocomplete], textarea[matAutocomplete]`, host: { + 'class': 'mat-autocomplete-trigger', '[attr.autocomplete]': 'autocompleteAttribute', '[attr.role]': 'autocompleteDisabled ? null : "combobox"', '[attr.aria-autocomplete]': 'autocompleteDisabled ? null : "list"',